Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
bb7a599
Port CLARIN backend data layer to DSpace 9 (migrations + entities/ser…
milanmajchrak Jun 18, 2026
8836cc6
Wire CLARIN backend Spring beans (services runtime-active)
milanmajchrak Jun 18, 2026
e8d96df
docs: update CLARIN v9 progress (tranche 1+2 CI-green, conflict strat…
milanmajchrak Jun 18, 2026
7117af3
Resolve BE review findings: defer PIDService landmine + document not-…
milanmajchrak Jun 22, 2026
85b260f
Port CLARIN BE config + entity columns (resolves review C1 partial + C2)
milanmajchrak Jun 22, 2026
7219832
Port CLARIN License REST layer to DSpace 9 (dspace-server-webapp)
milanmajchrak Jun 22, 2026
060e4fc
Port CLARIN handle/user/token/featured-service REST to DSpace 9
milanmajchrak Jun 22, 2026
09d726c
Fix CLARIN REST endpoints 404 in DSpace 9 (plural bean-name) + runtim…
milanmajchrak Jun 23, 2026
3f75e45
Document runtime endpoint sweep + verificationtokens 500 quirk (faith…
milanmajchrak Jun 23, 2026
98771fe
Fix CLARIN REST conversion crash in DSpace 9 (findOne @PreAuthorize) …
milanmajchrak Jun 23, 2026
4871683
Document findOne @PreAuthorize fix (tranche 7) + write-path validatio…
milanmajchrak Jun 23, 2026
d28db8c
Fix CI-red duplicate @PreAuthorize regression + complete handle/epic/…
milanmajchrak Jun 23, 2026
02f123f
Port CLARIN config-file/authorization/registration/import/submission-…
milanmajchrak Jun 23, 2026
33172a3
Port CLARIN vanilla-file methods: Item.isHidden + WorkspaceItem.findB…
milanmajchrak Jun 23, 2026
3b1ccc0
Document tranches 8 (26 REST + 14 deferred) and 9 (vanilla-file metho…
milanmajchrak Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
634 changes: 634 additions & 0 deletions CLARIN_DSPACE_V9_PROGRESS.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions dspace-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,40 @@
</profiles>

<dependencies>
<!-- CLARIN/LINDAT (UFAL) feature dependencies (ported from dtq-dev for DSpace 9 upgrade) -->
<dependency>
<!-- Matomo analytics tracking (ClarinMatomo* trackers, report subscriptions) -->
<groupId>org.piwik.java.tracking</groupId>
<artifactId>matomo-java-tracker-java11</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<!-- PDF generation for Matomo report exporter -->
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.4</version>
</dependency>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jcommon</artifactId>
<version>1.0.24</version>
</dependency>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>com.flipkart.zjsonpatch</groupId>
<artifactId>zjsonpatch</artifactId>
<version>0.4.16</version>
</dependency>
<dependency>
<!-- JWT for Clarin tokens / Personal Access Tokens (PAT) -->
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose-jwt.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.administer;

import static org.dspace.administer.ClarinTokenCreator.getExpirationDate;
import static org.dspace.administer.ClarinTokenCreator.getMaskedToken;

import java.security.NoSuchAlgorithmException;
import java.sql.SQLException;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import com.nimbusds.jose.EncryptionMethod;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.factory.ClarinServiceFactory;
import org.dspace.content.service.clarin.ClarinTokenService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;

/**
* This admin script is used to manage(create or invalidate) clarin tokens, for the user specified by ID or e-mail.
* <p>
* For the token creation, the user and the expiration time must be set in script options.
* Created token string is then printed on the console, and admin can send it to the user (e.g. via the e-mail).
* <p>
* For the token invalidation, either the token string or the user need to be set in script options.
* When neither token nor user is set, all tokens are deleted.
* <p>
* Admin user can also use this script to generate encryption/decryption secret key and store it into
* "clarin.token.encryption.secret" config property.
*
* @author Milan Kuchtiak
*/
public class ClarinTokenAdministrator {

private static final Logger log = LogManager.getLogger(ClarinTokenAdministrator.class);

private ClarinTokenAdministrator() {
}

public static void main(String args[]) throws Exception {
log.info("Clarin Token administrator started ....");

Options options = new Options();
options.addOption("c", "create", false, "create token for ePerson specified by ID or email");
options.addOption("d", "delete", false,
"delete specified token, or delete all tokens for given ePerson, " +
"or delete all tokens when -t, -u, and -e options are missing)");
options.addOption("g", "generateEncryptionKey", false,
"generate encryption/decryption secret key for clarin.token.encryption.secret property");
options.addOption("u", "ePerson_ID", true, "ePerson UUID");
options.addOption("e", "email", true, "ePerson email");
options.addOption("x", "expiration", true,
"token expiration time in days or hours, (e.g. 3d or 48h), for -c option only [required for create]");
options.addOption("t", "token", true,
"token string [optional for delete]");
options.addOption("h", "help", false, "help");

CommandLineParser parser = new DefaultParser();
try {
CommandLine line = parser.parse(options, args);
if (line.hasOption('h') || (!line.hasOption('c') && !line.hasOption('d') && !line.hasOption('g')) ) {
printHelpAndExit(options);
}
boolean isCreate = line.hasOption('c');
boolean isDelete = line.hasOption('d');
boolean generateEncryptionKey = line.hasOption('g');

if (isCreate && isDelete || isCreate && generateEncryptionKey || isDelete && generateEncryptionKey) {
throw new ParseException("Create, delete and generate options are mutually exclusive");
}

if (isCreate && !line.hasOption("u") && !line.hasOption("e")) {
throw new ParseException("either ePerson UUID or ePerson e-mail option is needed to create token");
}

if (isCreate && !line.hasOption("x")) {
throw new ParseException("Token expiration time option is missing");
}

UUID ePersonUUID = null;
if (line.hasOption("u")) {
ePersonUUID = UUID.fromString(line.getOptionValue("u"));
}

String email = null;
if (line.hasOption("e")) {
email = line.getOptionValue("e");
}

String token = null;
if (line.hasOption("t")) {
token = line.getOptionValue("t");
}

ClarinTokenService clarinTokenService =
ClarinServiceFactory.getInstance().getClarinTokenService();
EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();

try (Context context = new Context()) {
try {
context.turnOffAuthorisationSystem();
EPerson ePerson = getEPerson(context, ePersonService, ePersonUUID, email);
if (isCreate) {
if (ePerson == null) {
throw new IllegalArgumentException("Invalid ePerson UUID or email");
}
Date expirationDate = getExpirationDate(line.getOptionValue("x").toLowerCase());
createToken(context, clarinTokenService, ePerson, expirationDate);
} else if (isDelete) {
deleteToken(context, clarinTokenService, token, ePerson);
} else {
generateEncryptionKey();
}
} finally {
context.restoreAuthSystemState();
context.complete();
}
}

} catch (ParseException e) {
System.out.printf("Invalid command options: %s\n", e.getMessage());
printHelpAndExit(options);
}

log.info("Clarin Token administrator finished.");
}

private static void createToken(Context context,
ClarinTokenService clarinTokenService,
EPerson ePerson,
Date expirationDate) throws SQLException, AuthorizeException {
String token = clarinTokenService.createToken(context, ePerson, expirationDate);
log.debug("Clarin Token created: {}", getMaskedToken(token));
System.out.printf("Clarin Token created: %s\n", token);
System.out.printf("For user: %s, with ID: %s\n", ePerson.getEmail(), ePerson.getID());
}

private static void deleteToken(Context context,
ClarinTokenService clarinTokenService,
String token,
EPerson ePerson) throws SQLException, AuthorizeException {
if (token != null) {
clarinTokenService.delete(context, token);
System.out.println("Clarin Token removed.");
} else if (ePerson != null) {
clarinTokenService.delete(context, ePerson);
System.out.println("Clarin Tokens removed.");
System.out.printf("For user: %s, with ID: %s\n", ePerson.getEmail(), ePerson.getID());
} else {
clarinTokenService.deleteAll(context);
System.out.println("All Clarin Tokens removed");
}
}

private static void generateEncryptionKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(EncryptionMethod.A256GCM.cekBitLength());
SecretKey aesKey = keyGen.generateKey();

String encodedAesKey = Base64.getEncoder().encodeToString(aesKey.getEncoded());
log.debug("Encryption Key generated: {}", getMaskedToken(encodedAesKey));
System.out.printf("Encryption Key: %s\n", encodedAesKey);
}


private static void printHelpAndExit(Options options) {
// print the help message
HelpFormatter myHelp = new HelpFormatter();
myHelp.printHelp("clarin-token\n", options);
System.exit(0);
}

private static EPerson getEPerson(Context context, EPersonService ePersonService, UUID ePersonID, String email)
throws SQLException {
return ePersonID == null ? ePersonService.findByEmail(context, email) : ePersonService.find(context, ePersonID);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.administer;

import java.util.List;

import org.apache.commons.cli.Options;
import org.dspace.core.Context;
import org.dspace.scripts.DSpaceCommandLineParameter;
import org.dspace.scripts.configuration.ScriptConfiguration;

/**
* This is a ScriptConfiguration for {@link ClarinTokenCreator}.
*
* @author Milan Kuchtiak
*/
public class ClarinTokenConfiguration extends ScriptConfiguration<ClarinTokenCreator> {

private Class<ClarinTokenCreator> dspaceRunnableClass;

/**
* Generic getter for the dspaceRunnableClass
*
* @return the dspaceRunnableClass value of this ScriptConfiguration
*/
@Override
public Class<ClarinTokenCreator> getDspaceRunnableClass() {
return dspaceRunnableClass;
}

/**
* Generic setter for the dspaceRunnableClass
*
* @param dspaceRunnableClass The dspaceRunnableClass to be set on this IndexDiscoveryScriptConfiguration
*/
@Override
public void setDspaceRunnableClass(Class<ClarinTokenCreator> dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}

/**
* This script is allowed to execute to any authorized user. Further access control mechanism then checks,
* if the current user is authorized to download a file to the item specified in command line parameters.
*
* @param context The relevant DSpace context
* @param commandLineParameters the parameters that will be used to start the process if known,
* <code>null</code> otherwise
* @return A boolean indicating whether the script is allowed to execute or not
*/
@Override
public boolean isAllowedToExecute(Context context, List<DSpaceCommandLineParameter> commandLineParameters) {
return context.getCurrentUser() != null;
}

/**
* The getter for the options of the Script
*
* @return the options value of this ScriptConfiguration
*/
@Override
public Options getOptions() {
if (options == null) {

Options options = new Options();

options.addOption("h", "help", false, "help");

options.addOption("c", "create", false, "create new token");
options.addOption("d", "delete", false, "delete/deactivate token");

options.addOption("x", "expiration", true,
"token expiration in days or hours, e.g. 3d or 48h [required for token create]");
options.addOption("e", "email", true,
"e-mail to send newly created access token [optional for token create]");
options.addOption("t", "token", true, "token to delete/deactivate [required for token delete]");

super.options = options;
}
return options;
}
}
Loading
Loading