From 2f7e1b3a0a51522e9cdbf6bfca8134e0e538aa32 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 27 Jan 2025 14:29:39 +0000 Subject: [PATCH 01/70] Stash: building builtin users Keycloak SPI boilerplate (WIP) --- .../builtin-users-spi/conf/quarkus.properties | 17 +++ conf/keycloak/builtin-users-spi/pom.xml | 55 ++++++++ .../iq/keycloak/auth/spi/DataverseUser.java | 36 ++++++ .../auth/spi/DataverseUserAdapter.java | 55 ++++++++ .../spi/DataverseUserStorageProvider.java | 119 ++++++++++++++++++ .../DataverseUserStorageProviderFactory.java | 33 +++++ .../src/main/resources/META-INF/beans.xml | 0 .../main/resources/META-INF/persistence.xml | 31 +++++ ...eycloak.storage.UserStorageProviderFactory | 1 + docker-compose-dev.yml | 5 +- 10 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 conf/keycloak/builtin-users-spi/conf/quarkus.properties create mode 100644 conf/keycloak/builtin-users-spi/pom.xml create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/resources/META-INF/beans.xml create mode 100644 conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml create mode 100644 conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory diff --git a/conf/keycloak/builtin-users-spi/conf/quarkus.properties b/conf/keycloak/builtin-users-spi/conf/quarkus.properties new file mode 100644 index 00000000000..ab65c8a3cc0 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/conf/quarkus.properties @@ -0,0 +1,17 @@ +quarkus.datasource.user-store.db-kind=postgresql +quarkus.datasource.user-store.jdbc.url=jdbc:postgresql://postgres:5432/dataverse +quarkus.datasource.user-store.username=${DATAVERSE_DB_USER} +quarkus.datasource.user-store.password=secret + +# Use the XA data source for transaction support +quarkus.datasource.user-store.jdbc.driver=org.postgresql.xa.PGXADataSource +quarkus.datasource.user-store.jdbc.transactions=xa + +# XA connection recovery configuration +quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER} +quarkus.datasource.user-store.jdbc.recovery.password=secret + +# Correct format for XA properties +quarkus.datasource.user-store.jdbc.xa-properties.serverName=postgres +quarkus.datasource.user-store.jdbc.xa-properties.portNumber=5432 +quarkus.datasource.user-store.jdbc.xa-properties.databaseName=dataverse diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml new file mode 100644 index 00000000000..e77973d206b --- /dev/null +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + edu.harvard.iq.keycloak + keycloak-dv-builtin-users-authenticator + 1.0-SNAPSHOT + jar + + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + + + org.keycloak + keycloak-services + ${keycloak.version} + provided + + + + + org.keycloak + keycloak-model-jpa + ${keycloak.version} + provided + + + + + jakarta.persistence + jakarta.persistence-api + ${jakarta.persistence.version} + + + + + 22.0.0 + 17 + 3.2.0 + + diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java new file mode 100644 index 00000000000..f0a10ff4a53 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java @@ -0,0 +1,36 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import jakarta.persistence.*; + + +@NamedQueries({ + @NamedQuery(name = "DataverseUser.findAll", + query = "SELECT u FROM DataverseUser u"), + @NamedQuery(name = "DataverseUser.findByUsername", + query = "SELECT u FROM DataverseUser u WHERE LOWER(u.username)=LOWER(:username)") +}) +@Entity +@Table(name = "builtinuser") +public class DataverseUser { + @Id + private String id; + private String username; + private int passwordEncryptionVersion; + private String encryptedPassword; + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getEncryptedPassword() { + return encryptedPassword; + } + + public int getPasswordEncryptionVersion() { + return passwordEncryptionVersion; + } +} \ No newline at end of file diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java new file mode 100644 index 00000000000..d244874193d --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java @@ -0,0 +1,55 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; + +import java.util.stream.Stream; + +public class DataverseUserAdapter extends AbstractUserAdapterFederatedStorage { + + protected DataverseUser user; + protected String keycloakId; + + public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseUser user) { + super(session, realm, model); + this.user = user; + keycloakId = StorageId.keycloakId(model, user.getId()); + } + + public String getEncryptedPassword() { + return user.getEncryptedPassword(); + } + + public String getUsername() { + return user.getUsername(); + } + + @Override + public void setUsername(String s) { + + } + + @Override + public Stream getGroupsStream(String search, Integer first, Integer max) { + return super.getGroupsStream(search, first, max); + } + + @Override + public long getGroupsCount() { + return super.getGroupsCount(); + } + + @Override + public long getGroupsCountByNameContaining(String search) { + return super.getGroupsCountByNameContaining(search); + } + + @Override + public String getId() { + return keycloakId; + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java new file mode 100644 index 00000000000..13358c40796 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -0,0 +1,119 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; + +import org.jboss.logging.Logger; + +import org.keycloak.component.ComponentModel; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; +import org.keycloak.storage.user.UserRegistrationProvider; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public class DataverseUserStorageProvider implements + UserStorageProvider, + UserLookupProvider, + UserRegistrationProvider, + UserQueryProvider, + CredentialInputValidator, + OnUserCache { + + private static final Logger logger = Logger.getLogger(DataverseUserStorageProvider.class); + + protected ComponentModel model; + protected KeycloakSession session; + protected EntityManager em; + + DataverseUserStorageProvider(KeycloakSession session, ComponentModel model) { + this.session = session; + this.model = model; + em = session.getProvider(JpaConnectionProvider.class, "user-store").getEntityManager(); + } + + @Override + public UserModel getUserById(RealmModel realmModel, String id) { + return null; + } + + @Override + public UserModel getUserByUsername(RealmModel realmModel, String username) { + logger.info("getUserByUsername: " + username); + TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseUser.class); + query.setParameter("username", username); + List result = query.getResultList(); + if (result.isEmpty()) { + logger.info("could not find username: " + username); + return null; + } + + return new DataverseUserAdapter(session, realmModel, model, result.get(0)); + } + + @Override + public UserModel getUserByEmail(RealmModel realmModel, String email) { + return null; + } + + @Override + public boolean supportsCredentialType(String s) { + return false; + } + + @Override + public boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String s) { + return false; + } + + @Override + public boolean isValid(RealmModel realmModel, UserModel userModel, CredentialInput credentialInput) { + return false; + } + + @Override + public Stream searchForUserStream(RealmModel realmModel, Map map, Integer integer, Integer integer1) { + return Stream.empty(); + } + + @Override + public Stream getGroupMembersStream(RealmModel realmModel, GroupModel groupModel, Integer integer, Integer integer1) { + return Stream.empty(); + } + + @Override + public Stream searchForUserByUserAttributeStream(RealmModel realmModel, String s, String s1) { + return Stream.empty(); + } + + @Override + public UserModel addUser(RealmModel realmModel, String s) { + return null; + } + + @Override + public boolean removeUser(RealmModel realmModel, UserModel userModel) { + return false; + } + + @Override + public void close() { + } + + @Override + public void onCache(RealmModel realmModel, CachedUserModel cachedUserModel, UserModel userModel) { + + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java new file mode 100644 index 00000000000..30da64e565d --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java @@ -0,0 +1,33 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.UserStorageProviderFactory; + +public class DataverseUserStorageProviderFactory implements UserStorageProviderFactory { + + public static final String PROVIDER_ID = "dv-builtin-users-authenticator"; + + private static final Logger logger = Logger.getLogger(DataverseUserStorageProviderFactory.class); + + @Override + public DataverseUserStorageProvider create(KeycloakSession session, ComponentModel model) { + return new DataverseUserStorageProvider(session, model); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "A Keycloak Storage Provider to authenticate Dataverse Builtin Users"; + } + + @Override + public void close() { + logger.info("<<<<<< Closing factory"); + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/beans.xml b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000000..09fe6f19b51 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,31 @@ + + + + edu.harvard.iq.keycloak.auth.spi.DataverseUser + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 00000000000..504e684acd5 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1 @@ +edu.harvard.iq.keycloak.auth.spi.DataverseUserStorageProviderFactory \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index ce181d27887..403f90e8a5e 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -168,13 +168,14 @@ services: dev_keycloak: container_name: "dev_keycloak" - image: 'quay.io/keycloak/keycloak:21.0' + image: 'quay.io/keycloak/keycloak:22.0' hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin - KEYCLOAK_ADMIN_PASSWORD=kcpassword - KEYCLOAK_LOGLEVEL=DEBUG - KC_HOSTNAME_STRICT=false + - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} networks: dataverse: aliases: @@ -184,6 +185,8 @@ services: - "8090:8090" volumes: - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' + - './conf/keycloak/builtin-users-spi/conf/quarkus.properties:/opt/keycloak/conf/quarkus.properties' + - './conf/keycloak/builtin-users-spi/target/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar:/opt/keycloak/providers/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar' # This proxy configuration is only intended to be used for development purposes! # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! From fa72703b13ce9f4e81fefe299035c07ce1b9961b Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 28 Jan 2025 15:12:30 +0000 Subject: [PATCH 02/70] Added: Keycloak SPI base logic to support searching dv builtin users through kc admin console --- .../builtin-users-spi/conf/quarkus.properties | 4 +- .../spi/DataverseUserStorageProvider.java | 69 +++++++++---------- .../main/resources/META-INF/persistence.xml | 2 +- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/conf/quarkus.properties b/conf/keycloak/builtin-users-spi/conf/quarkus.properties index ab65c8a3cc0..9c276f16d4e 100644 --- a/conf/keycloak/builtin-users-spi/conf/quarkus.properties +++ b/conf/keycloak/builtin-users-spi/conf/quarkus.properties @@ -4,8 +4,8 @@ quarkus.datasource.user-store.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.password=secret # Use the XA data source for transaction support -quarkus.datasource.user-store.jdbc.driver=org.postgresql.xa.PGXADataSource -quarkus.datasource.user-store.jdbc.transactions=xa +quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver +quarkus.datasource.user-store.jdbc.transactions=disabled # XA connection recovery configuration quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index 13358c40796..35ad000716a 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -4,7 +4,6 @@ import jakarta.persistence.TypedQuery; import org.jboss.logging.Logger; - import org.keycloak.component.ComponentModel; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.credential.CredentialInput; @@ -13,12 +12,10 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.cache.CachedUserModel; -import org.keycloak.models.cache.OnUserCache; +import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.user.UserLookupProvider; -import org.keycloak.storage.user.UserQueryProvider; -import org.keycloak.storage.user.UserRegistrationProvider; +import org.keycloak.storage.user.UserQueryMethodsProvider; import java.util.List; import java.util.Map; @@ -27,10 +24,8 @@ public class DataverseUserStorageProvider implements UserStorageProvider, UserLookupProvider, - UserRegistrationProvider, - UserQueryProvider, CredentialInputValidator, - OnUserCache { + UserQueryMethodsProvider { private static final Logger logger = Logger.getLogger(DataverseUserStorageProvider.class); @@ -46,7 +41,13 @@ public class DataverseUserStorageProvider implements @Override public UserModel getUserById(RealmModel realmModel, String id) { - return null; + logger.info("getUserById: " + id); + DataverseUser user = em.find(DataverseUser.class, id); + if (user == null) { + logger.info("could not find user by id: " + id); + return null; + } + return new DataverseUserAdapter(session, realmModel, model, user); } @Override @@ -56,64 +57,62 @@ public UserModel getUserByUsername(RealmModel realmModel, String username) { query.setParameter("username", username); List result = query.getResultList(); if (result.isEmpty()) { - logger.info("could not find username: " + username); + logger.info("User not found: " + username); return null; } - + logger.info("User found: " + result.get(0).getUsername()); return new DataverseUserAdapter(session, realmModel, model, result.get(0)); } @Override public UserModel getUserByEmail(RealmModel realmModel, String email) { + logger.info("getUserByEmail is not supported in DataverseUserStorageProvider"); return null; } @Override - public boolean supportsCredentialType(String s) { - return false; + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(PasswordCredentialModel.TYPE); } @Override - public boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String s) { + public boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String credentialType) { + logger.info("isConfiguredFor called for user: " + userModel.getUsername() + " and credentialType: " + credentialType); return false; } @Override public boolean isValid(RealmModel realmModel, UserModel userModel, CredentialInput credentialInput) { + logger.info("isValid is not supported in DataverseUserStorageProvider"); return false; } @Override - public Stream searchForUserStream(RealmModel realmModel, Map map, Integer integer, Integer integer1) { - return Stream.empty(); + public void close() { + logger.info("Closing DataverseUserStorageProvider"); + if (em != null) { + em.close(); + } } @Override - public Stream getGroupMembersStream(RealmModel realmModel, GroupModel groupModel, Integer integer, Integer integer1) { - return Stream.empty(); + public Stream searchForUserStream(RealmModel realm, Map params, Integer firstResult, Integer maxResults) { + // TODO search by email too + String search = params.get(UserModel.SEARCH); + logger.info("searchForUserStream: " + search); + String lower = search != null ? search.toLowerCase() : ""; + TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseUser.class); + query.setParameter("username", lower); + return query.getResultStream().map(entity -> new DataverseUserAdapter(session, realm, model, entity)); } @Override - public Stream searchForUserByUserAttributeStream(RealmModel realmModel, String s, String s1) { + public Stream getGroupMembersStream(RealmModel realmModel, GroupModel groupModel, Integer integer, Integer integer1) { return Stream.empty(); } @Override - public UserModel addUser(RealmModel realmModel, String s) { - return null; - } - - @Override - public boolean removeUser(RealmModel realmModel, UserModel userModel) { - return false; - } - - @Override - public void close() { - } - - @Override - public void onCache(RealmModel realmModel, CachedUserModel cachedUserModel, UserModel userModel) { - + public Stream searchForUserByUserAttributeStream(RealmModel realmModel, String s, String s1) { + return Stream.empty(); } } diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml index 09fe6f19b51..b0439d526a1 100644 --- a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml @@ -22,7 +22,7 @@ - + From db34307395fb8e647b56482b4dc953184c903c04 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 29 Jan 2025 20:22:34 +0100 Subject: [PATCH 03/70] Added: DataverseAuthenticatedUser for populating extra information in DataverseUserAdapter --- .../auth/spi/DataverseAuthenticatedUser.java | 36 ++++++++++++++++++ .../auth/spi/DataverseBuiltinUser.java | 23 +++++++++++ .../iq/keycloak/auth/spi/DataverseUser.java | 36 ------------------ .../auth/spi/DataverseUserAdapter.java | 29 ++++++++++---- .../spi/DataverseUserStorageProvider.java | 38 ++++++++++++++----- .../main/resources/META-INF/persistence.xml | 3 +- 6 files changed, 110 insertions(+), 55 deletions(-) create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java delete mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java new file mode 100644 index 00000000000..1f886964cc2 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java @@ -0,0 +1,36 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import jakarta.persistence.*; + +@NamedQueries({ + @NamedQuery(name = "DataverseAuthenticatedUser.findByEmail", + query = "select au from DataverseAuthenticatedUser au WHERE LOWER(au.email)=LOWER(:email)"), + @NamedQuery(name = "DataverseAuthenticatedUser.findByIdentifier", + query = "select au from DataverseAuthenticatedUser au WHERE LOWER(au.userIdentifier)=LOWER(:identifier)"), +}) +@Entity +@Table(name = "authenticateduser") +public class DataverseAuthenticatedUser { + @Id + private String id; + private String email; + private String lastName; + private String firstName; + private String userIdentifier; + + public String getEmail() { + return email; + } + + public String getLastName() { + return lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getUserIdentifier() { + return userIdentifier; + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java new file mode 100644 index 00000000000..ea4c8496ab2 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java @@ -0,0 +1,23 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import jakarta.persistence.*; + +@NamedQueries({ + @NamedQuery(name = "DataverseUser.findByUsername", + query = "SELECT u FROM DataverseBuiltinUser u WHERE LOWER(u.username)=LOWER(:username)") +}) +@Entity +@Table(name = "builtinuser") +public class DataverseBuiltinUser { + @Id + private String id; + private String username; + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java deleted file mode 100644 index f0a10ff4a53..00000000000 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUser.java +++ /dev/null @@ -1,36 +0,0 @@ -package edu.harvard.iq.keycloak.auth.spi; - -import jakarta.persistence.*; - - -@NamedQueries({ - @NamedQuery(name = "DataverseUser.findAll", - query = "SELECT u FROM DataverseUser u"), - @NamedQuery(name = "DataverseUser.findByUsername", - query = "SELECT u FROM DataverseUser u WHERE LOWER(u.username)=LOWER(:username)") -}) -@Entity -@Table(name = "builtinuser") -public class DataverseUser { - @Id - private String id; - private String username; - private int passwordEncryptionVersion; - private String encryptedPassword; - - public String getId() { - return id; - } - - public String getUsername() { - return username; - } - - public String getEncryptedPassword() { - return encryptedPassword; - } - - public int getPasswordEncryptionVersion() { - return passwordEncryptionVersion; - } -} \ No newline at end of file diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java index d244874193d..46838e37c6b 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java @@ -11,26 +11,39 @@ public class DataverseUserAdapter extends AbstractUserAdapterFederatedStorage { - protected DataverseUser user; + protected DataverseBuiltinUser builtinUser; + protected DataverseAuthenticatedUser authenticatedUser; protected String keycloakId; - public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseUser user) { + public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseBuiltinUser builtinUser, DataverseAuthenticatedUser authenticatedUser) { super(session, realm, model); - this.user = user; - keycloakId = StorageId.keycloakId(model, user.getId()); + this.builtinUser = builtinUser; + this.authenticatedUser = authenticatedUser; + keycloakId = StorageId.keycloakId(model, builtinUser.getId()); } - public String getEncryptedPassword() { - return user.getEncryptedPassword(); + @Override + public void setUsername(String s) { } + @Override public String getUsername() { - return user.getUsername(); + return builtinUser.getUsername(); } @Override - public void setUsername(String s) { + public String getEmail() { + return authenticatedUser.getEmail(); + } + @Override + public String getFirstName() { + return authenticatedUser.getFirstName(); + } + + @Override + public String getLastName() { + return authenticatedUser.getLastName(); } @Override diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index 35ad000716a..0096687c762 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -42,26 +42,34 @@ public class DataverseUserStorageProvider implements @Override public UserModel getUserById(RealmModel realmModel, String id) { logger.info("getUserById: " + id); - DataverseUser user = em.find(DataverseUser.class, id); + DataverseBuiltinUser user = em.find(DataverseBuiltinUser.class, id); if (user == null) { - logger.info("could not find user by id: " + id); + logger.info("could not find builtin user by id: " + id); return null; } - return new DataverseUserAdapter(session, realmModel, model, user); + String username = user.getUsername(); + DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); + if (authenticatedUser == null) { + return null; + } + return new DataverseUserAdapter(session, realmModel, model, user, authenticatedUser); } @Override public UserModel getUserByUsername(RealmModel realmModel, String username) { logger.info("getUserByUsername: " + username); - TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseUser.class); + TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseBuiltinUser.class); query.setParameter("username", username); - List result = query.getResultList(); - if (result.isEmpty()) { + List builtinUsersResult = query.getResultList(); + if (builtinUsersResult.isEmpty()) { logger.info("User not found: " + username); return null; } - logger.info("User found: " + result.get(0).getUsername()); - return new DataverseUserAdapter(session, realmModel, model, result.get(0)); + DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); + if (authenticatedUser == null) { + return null; + } + return new DataverseUserAdapter(session, realmModel, model, builtinUsersResult.get(0), authenticatedUser); } @Override @@ -101,9 +109,9 @@ public Stream searchForUserStream(RealmModel realm, Map query = em.createNamedQuery("DataverseUser.findByUsername", DataverseUser.class); + TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseBuiltinUser.class); query.setParameter("username", lower); - return query.getResultStream().map(entity -> new DataverseUserAdapter(session, realm, model, entity)); + return query.getResultStream().map(entity -> new DataverseUserAdapter(session, realm, model, entity, getAuthenticatedUserByUsername(entity.getUsername()))); } @Override @@ -115,4 +123,14 @@ public Stream getGroupMembersStream(RealmModel realmModel, GroupModel public Stream searchForUserByUserAttributeStream(RealmModel realmModel, String s, String s1) { return Stream.empty(); } + + private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String username) { + TypedQuery query = em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class); + query.setParameter("identifier", username); + DataverseAuthenticatedUser singleResult = query.getSingleResult(); + if (singleResult == null) { + logger.info("Could not find authenticated user by username: " + username); + } + return singleResult; + } } diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml index b0439d526a1..9e579281ba6 100644 --- a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml @@ -4,7 +4,8 @@ xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> - edu.harvard.iq.keycloak.auth.spi.DataverseUser + edu.harvard.iq.keycloak.auth.spi.DataverseBuiltinUser + edu.harvard.iq.keycloak.auth.spi.DataverseAuthenticatedUser From 72acd635bb4eefae61a76c3cda380146bb31f183 Mon Sep 17 00:00:00 2001 From: GPortas Date: Sun, 2 Feb 2025 10:40:54 +0100 Subject: [PATCH 04/70] Added: achieving authentication from Keycloak builtin users SPI through Dataverse API call --- conf/keycloak/builtin-users-spi/pom.xml | 12 ++++++++ .../auth/spi/DataverseAPIService.java | 30 +++++++++++++++++++ .../spi/DataverseUserStorageProvider.java | 30 +++++++++++-------- .../iq/dataverse/api/BuiltinUsers.java | 15 ++++++++++ 4 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index e77973d206b..4b442cf8995 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -46,6 +46,18 @@ ${jakarta.persistence.version} + + + + org.apache.maven.plugins + maven-compiler-plugin + + 10 + 10 + + + + 22.0.0 diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java new file mode 100644 index 00000000000..d9bacc662d2 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java @@ -0,0 +1,30 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class DataverseAPIService { + + public static boolean canLogInAsBuiltinUser(String username, String password) { + try { + String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); + String encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8); + String urlString = "http://dataverse:8080/api/builtin-users/" + encodedUsername + "/canLoginWithGivenCredentials?password=" + encodedPassword; + URL url = new URL(urlString); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + + int responseCode = conn.getResponseCode(); + if (responseCode == 200) { + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index 0096687c762..c6f71a803dd 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -8,14 +8,12 @@ import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; +import org.keycloak.models.*; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryMethodsProvider; +import org.keycloak.storage.StorageId; import java.util.List; import java.util.Map; @@ -41,8 +39,10 @@ public class DataverseUserStorageProvider implements @Override public UserModel getUserById(RealmModel realmModel, String id) { - logger.info("getUserById: " + id); - DataverseBuiltinUser user = em.find(DataverseBuiltinUser.class, id); + logger.info("getUserById - id: " + id); + String persistenceId = StorageId.externalId(id); + logger.info("getUserById - persistenceId: " + persistenceId); + DataverseBuiltinUser user = em.find(DataverseBuiltinUser.class, persistenceId); if (user == null) { logger.info("could not find builtin user by id: " + id); return null; @@ -89,12 +89,6 @@ public boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, Strin return false; } - @Override - public boolean isValid(RealmModel realmModel, UserModel userModel, CredentialInput credentialInput) { - logger.info("isValid is not supported in DataverseUserStorageProvider"); - return false; - } - @Override public void close() { logger.info("Closing DataverseUserStorageProvider"); @@ -105,7 +99,7 @@ public void close() { @Override public Stream searchForUserStream(RealmModel realm, Map params, Integer firstResult, Integer maxResults) { - // TODO search by email too + // TODO search by email or other properties too String search = params.get(UserModel.SEARCH); logger.info("searchForUserStream: " + search); String lower = search != null ? search.toLowerCase() : ""; @@ -133,4 +127,14 @@ private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String usernam } return singleResult; } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + logger.info("isValid called for user: " + user.getUsername()); + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + UserCredentialModel userCredential = (UserCredentialModel) input; + String username = user.getUsername(); + String password = userCredential.getValue(); + return DataverseAPIService.canLogInAsBuiltinUser(username, password); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java index ba99cf33c5b..d820766b700 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; import edu.harvard.iq.dataverse.api.auth.ApiKeyAuthMechanism; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; @@ -17,6 +18,7 @@ import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; +import jakarta.inject.Inject; import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; import jakarta.ws.rs.GET; @@ -45,6 +47,9 @@ public class BuiltinUsers extends AbstractApiBean { @EJB protected BuiltinUserServiceBean builtinUserSvc; + @Inject + private AuthenticationServiceBean authenticationService; + @GET @Path("{username}/api-token") public Response getApiToken( @PathParam("username") String username, @QueryParam("password") String password ) { @@ -122,6 +127,16 @@ public Response create(BuiltinUser user, @PathParam("password") String password, public Response createWithNotification(BuiltinUser user, @PathParam("password") String password, @PathParam("key") String key, @PathParam("sendEmailNotification") Boolean sendEmailNotification) { return internalSave(user, password, key, sendEmailNotification); } + + @GET + @Path("{username}/canLoginWithGivenCredentials") + public Response canLogInAsBuiltinUser(@PathParam("username") String username, @QueryParam("password") String password) { + AuthenticatedUser u = authenticationService.canLogInAsBuiltinUser(username, password); + + if (u == null) return badRequest("Bad username or password"); + + return ok("User can log in with the given credentials."); + } // internalSave without providing an explicit "sendEmailNotification" private Response internalSave(BuiltinUser user, String password, String key) { From d05ea893a2f2d4f0192ee13b8e4b716bfd70f2a0 Mon Sep 17 00:00:00 2001 From: GPortas Date: Sun, 2 Feb 2025 17:11:04 +0100 Subject: [PATCH 05/70] Changed: removed UserQueryMethodsProvider impl and minor tweaks and fixes --- .../auth/spi/DataverseAuthenticatedUser.java | 6 +---- .../auth/spi/DataverseBuiltinUser.java | 5 ++-- .../auth/spi/DataverseUserAdapter.java | 2 +- .../spi/DataverseUserStorageProvider.java | 27 +------------------ 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java index 1f886964cc2..fe6925c599d 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java @@ -12,7 +12,7 @@ @Table(name = "authenticateduser") public class DataverseAuthenticatedUser { @Id - private String id; + private Integer id; private String email; private String lastName; private String firstName; @@ -29,8 +29,4 @@ public String getLastName() { public String getFirstName() { return firstName; } - - public String getUserIdentifier() { - return userIdentifier; - } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java index ea4c8496ab2..419789b4e7a 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java @@ -10,10 +10,11 @@ @Table(name = "builtinuser") public class DataverseBuiltinUser { @Id - private String id; + private Integer id; + private String username; - public String getId() { + public Integer getId() { return id; } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java index 46838e37c6b..15835b92569 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java @@ -19,7 +19,7 @@ public DataverseUserAdapter(KeycloakSession session, RealmModel realm, Component super(session, realm, model); this.builtinUser = builtinUser; this.authenticatedUser = authenticatedUser; - keycloakId = StorageId.keycloakId(model, builtinUser.getId()); + keycloakId = StorageId.keycloakId(model, builtinUser.getId().toString()); } @Override diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index c6f71a803dd..0bef00da7b7 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -12,18 +12,14 @@ import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.user.UserLookupProvider; -import org.keycloak.storage.user.UserQueryMethodsProvider; import org.keycloak.storage.StorageId; import java.util.List; -import java.util.Map; -import java.util.stream.Stream; public class DataverseUserStorageProvider implements UserStorageProvider, UserLookupProvider, - CredentialInputValidator, - UserQueryMethodsProvider { + CredentialInputValidator { private static final Logger logger = Logger.getLogger(DataverseUserStorageProvider.class); @@ -97,27 +93,6 @@ public void close() { } } - @Override - public Stream searchForUserStream(RealmModel realm, Map params, Integer firstResult, Integer maxResults) { - // TODO search by email or other properties too - String search = params.get(UserModel.SEARCH); - logger.info("searchForUserStream: " + search); - String lower = search != null ? search.toLowerCase() : ""; - TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseBuiltinUser.class); - query.setParameter("username", lower); - return query.getResultStream().map(entity -> new DataverseUserAdapter(session, realm, model, entity, getAuthenticatedUserByUsername(entity.getUsername()))); - } - - @Override - public Stream getGroupMembersStream(RealmModel realmModel, GroupModel groupModel, Integer integer, Integer integer1) { - return Stream.empty(); - } - - @Override - public Stream searchForUserByUserAttributeStream(RealmModel realmModel, String s, String s1) { - return Stream.empty(); - } - private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String username) { TypedQuery query = em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class); query.setParameter("identifier", username); From 59f19c281c1db0a0d2108c5c92a9cb6fe9787774 Mon Sep 17 00:00:00 2001 From: GPortas Date: Sun, 2 Feb 2025 17:42:24 +0100 Subject: [PATCH 06/70] Added: email/password builtin users auth --- .../auth/spi/DataverseAuthenticatedUser.java | 4 ++++ .../auth/spi/DataverseBuiltinUser.java | 2 +- .../spi/DataverseUserStorageProvider.java | 23 +++++++++++++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java index fe6925c599d..cb1cc76eef4 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java @@ -29,4 +29,8 @@ public String getLastName() { public String getFirstName() { return firstName; } + + public String getUserIdentifier() { + return userIdentifier; + } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java index 419789b4e7a..4af9e78aa5f 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java @@ -3,7 +3,7 @@ import jakarta.persistence.*; @NamedQueries({ - @NamedQuery(name = "DataverseUser.findByUsername", + @NamedQuery(name = "DataverseBuiltinUser.findByUsername", query = "SELECT u FROM DataverseBuiltinUser u WHERE LOWER(u.username)=LOWER(:username)") }) @Entity diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index 0bef00da7b7..eec22cc9d57 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -54,11 +54,11 @@ public UserModel getUserById(RealmModel realmModel, String id) { @Override public UserModel getUserByUsername(RealmModel realmModel, String username) { logger.info("getUserByUsername: " + username); - TypedQuery query = em.createNamedQuery("DataverseUser.findByUsername", DataverseBuiltinUser.class); + TypedQuery query = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class); query.setParameter("username", username); List builtinUsersResult = query.getResultList(); if (builtinUsersResult.isEmpty()) { - logger.info("User not found: " + username); + logger.info("Builtin user not found: " + username); return null; } DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); @@ -70,8 +70,23 @@ public UserModel getUserByUsername(RealmModel realmModel, String username) { @Override public UserModel getUserByEmail(RealmModel realmModel, String email) { - logger.info("getUserByEmail is not supported in DataverseUserStorageProvider"); - return null; + logger.info("getUserByEmail: " + email); + TypedQuery authenticatedUserQuery = em.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class); + authenticatedUserQuery.setParameter("email", email); + List authenticatedUsersResult = authenticatedUserQuery.getResultList(); + if (authenticatedUsersResult.isEmpty()) { + logger.info("Authenticated user not found: " + email); + return null; + } + DataverseAuthenticatedUser authenticatedUser = authenticatedUsersResult.get(0); + TypedQuery builtinUserQuery = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class); + builtinUserQuery.setParameter("username", authenticatedUser.getUserIdentifier()); + List builtinUsersResult = builtinUserQuery.getResultList(); + if (builtinUsersResult.isEmpty()) { + logger.info("Builtin user not found: " + email); + return null; + } + return new DataverseUserAdapter(session, realmModel, model, builtinUsersResult.get(0), authenticatedUser); } @Override From 1f5b2b761bb2b34575bde8b0618fa09611a2d514 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 3 Feb 2025 10:30:22 +0100 Subject: [PATCH 07/70] Refactor: DataverseUserStorageProvider --- .../builtin-users-spi/conf/quarkus.properties | 3 -- conf/keycloak/builtin-users-spi/pom.xml | 4 +- .../spi/DataverseUserStorageProvider.java | 39 ++++++++++--------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/conf/quarkus.properties b/conf/keycloak/builtin-users-spi/conf/quarkus.properties index 9c276f16d4e..99af38348d3 100644 --- a/conf/keycloak/builtin-users-spi/conf/quarkus.properties +++ b/conf/keycloak/builtin-users-spi/conf/quarkus.properties @@ -3,15 +3,12 @@ quarkus.datasource.user-store.jdbc.url=jdbc:postgresql://postgres:5432/dataverse quarkus.datasource.user-store.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.password=secret -# Use the XA data source for transaction support quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver quarkus.datasource.user-store.jdbc.transactions=disabled -# XA connection recovery configuration quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.jdbc.recovery.password=secret -# Correct format for XA properties quarkus.datasource.user-store.jdbc.xa-properties.serverName=postgres quarkus.datasource.user-store.jdbc.xa-properties.portNumber=5432 quarkus.datasource.user-store.jdbc.xa-properties.databaseName=dataverse diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index 4b442cf8995..c18c4ff14df 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -52,8 +52,8 @@ org.apache.maven.plugins maven-compiler-plugin - 10 - 10 + 16 + 16 diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index eec22cc9d57..613799d7a39 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -38,17 +38,17 @@ public UserModel getUserById(RealmModel realmModel, String id) { logger.info("getUserById - id: " + id); String persistenceId = StorageId.externalId(id); logger.info("getUserById - persistenceId: " + persistenceId); - DataverseBuiltinUser user = em.find(DataverseBuiltinUser.class, persistenceId); - if (user == null) { - logger.info("could not find builtin user by id: " + id); + DataverseBuiltinUser builtinUser = em.find(DataverseBuiltinUser.class, persistenceId); + if (builtinUser == null) { + logger.info("Could not find builtin user by id: " + persistenceId); return null; } - String username = user.getUsername(); + String username = builtinUser.getUsername(); DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); if (authenticatedUser == null) { return null; } - return new DataverseUserAdapter(session, realmModel, model, user, authenticatedUser); + return new DataverseUserAdapter(session, realmModel, model, builtinUser, authenticatedUser); } @Override @@ -58,7 +58,7 @@ public UserModel getUserByUsername(RealmModel realmModel, String username) { query.setParameter("username", username); List builtinUsersResult = query.getResultList(); if (builtinUsersResult.isEmpty()) { - logger.info("Builtin user not found: " + username); + logger.info("Could not find builtin user by username: " + username); return null; } DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); @@ -75,15 +75,16 @@ public UserModel getUserByEmail(RealmModel realmModel, String email) { authenticatedUserQuery.setParameter("email", email); List authenticatedUsersResult = authenticatedUserQuery.getResultList(); if (authenticatedUsersResult.isEmpty()) { - logger.info("Authenticated user not found: " + email); + logger.info("Could not find authenticated user by email: " + email); return null; } DataverseAuthenticatedUser authenticatedUser = authenticatedUsersResult.get(0); TypedQuery builtinUserQuery = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class); - builtinUserQuery.setParameter("username", authenticatedUser.getUserIdentifier()); + String username = authenticatedUser.getUserIdentifier(); + builtinUserQuery.setParameter("username", username); List builtinUsersResult = builtinUserQuery.getResultList(); if (builtinUsersResult.isEmpty()) { - logger.info("Builtin user not found: " + email); + logger.info("Could not find builtin user by username: " + username); return null; } return new DataverseUserAdapter(session, realmModel, model, builtinUsersResult.get(0), authenticatedUser); @@ -108,6 +109,16 @@ public void close() { } } + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + logger.info("isValid called for user: " + user.getUsername()); + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel userCredential)) + return false; + String username = user.getUsername(); + String password = userCredential.getValue(); + return DataverseAPIService.canLogInAsBuiltinUser(username, password); + } + private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String username) { TypedQuery query = em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class); query.setParameter("identifier", username); @@ -117,14 +128,4 @@ private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String usernam } return singleResult; } - - @Override - public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { - logger.info("isValid called for user: " + user.getUsername()); - if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; - UserCredentialModel userCredential = (UserCredentialModel) input; - String username = user.getUsername(); - String password = userCredential.getValue(); - return DataverseAPIService.canLogInAsBuiltinUser(username, password); - } } From 0ed0b795a65396040964cabbfd0d9ed957f9836b Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 3 Feb 2025 17:35:39 +0100 Subject: [PATCH 08/70] Refactor: DataverseAPIService --- .../auth/spi/DataverseAPIService.java | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java index d9bacc662d2..60b0288768d 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java @@ -1,30 +1,52 @@ package edu.harvard.iq.keycloak.auth.spi; +import org.jboss.logging.Logger; + +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +/** + * Provides API interaction methods for Dataverse authentication. + */ public class DataverseAPIService { + private static final Logger logger = Logger.getLogger(DataverseAPIService.class); + private static final String DATAVERSE_API_URL = "http://dataverse:8080/api/builtin-users/%s/canLoginWithGivenCredentials?password=%s"; + + /** + * Validates if a Dataverse built-in user can log in with the given credentials. + * + * @param username The username of the Dataverse built-in user. + * @param password The password to be validated. + * @return {@code true} if the user can log in, {@code false} otherwise. + */ public static boolean canLogInAsBuiltinUser(String username, String password) { + HttpURLConnection connection = null; + try { String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); String encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8); - String urlString = "http://dataverse:8080/api/builtin-users/" + encodedUsername + "/canLoginWithGivenCredentials?password=" + encodedPassword; - URL url = new URL(urlString); + String requestUrl = String.format(DATAVERSE_API_URL, encodedUsername, encodedPassword); + + URL url = new URL(requestUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "application/json"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty("Accept", "application/json"); + int responseCode = connection.getResponseCode(); + logger.infof("Dataverse API response code for user '%s': %d", username, responseCode); - int responseCode = conn.getResponseCode(); - if (responseCode == 200) { - return true; + return responseCode == HttpURLConnection.HTTP_OK; + } catch (IOException e) { + logger.errorf(e, "Error occurred while validating login for user '%s'", username); + return false; + } finally { + if (connection != null) { + connection.disconnect(); } - } catch (Exception e) { - e.printStackTrace(); } - return false; } } From c8d8af048cb4e0e6800ddc2b558b4aab278eb1ae Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 3 Feb 2025 17:36:01 +0100 Subject: [PATCH 09/70] Refactor: DataverseUserStorageProvider --- .../spi/DataverseUserStorageProvider.java | 131 +++++++++--------- 1 file changed, 68 insertions(+), 63 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index 613799d7a39..a75d3e9ce66 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -2,7 +2,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; - import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; import org.keycloak.connections.jpa.JpaConnectionProvider; @@ -16,6 +15,10 @@ import java.util.List; +/** + * DataverseUserStorageProvider integrates Keycloak with Dataverse user storage. + * It enables authentication and retrieval of users from a Dataverse-based user store. + */ public class DataverseUserStorageProvider implements UserStorageProvider, UserLookupProvider, @@ -23,84 +26,89 @@ public class DataverseUserStorageProvider implements private static final Logger logger = Logger.getLogger(DataverseUserStorageProvider.class); - protected ComponentModel model; - protected KeycloakSession session; - protected EntityManager em; + private final ComponentModel model; + private final KeycloakSession session; + private final EntityManager em; - DataverseUserStorageProvider(KeycloakSession session, ComponentModel model) { + public DataverseUserStorageProvider(KeycloakSession session, ComponentModel model) { this.session = session; this.model = model; - em = session.getProvider(JpaConnectionProvider.class, "user-store").getEntityManager(); + this.em = session.getProvider(JpaConnectionProvider.class, "user-store").getEntityManager(); } @Override - public UserModel getUserById(RealmModel realmModel, String id) { - logger.info("getUserById - id: " + id); + public UserModel getUserById(RealmModel realm, String id) { + logger.infof("Fetching user by ID: %s", id); String persistenceId = StorageId.externalId(id); - logger.info("getUserById - persistenceId: " + persistenceId); + DataverseBuiltinUser builtinUser = em.find(DataverseBuiltinUser.class, persistenceId); if (builtinUser == null) { - logger.info("Could not find builtin user by id: " + persistenceId); - return null; - } - String username = builtinUser.getUsername(); - DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); - if (authenticatedUser == null) { + logger.infof("User not found for external ID: %s", persistenceId); return null; } - return new DataverseUserAdapter(session, realmModel, model, builtinUser, authenticatedUser); + + DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(builtinUser.getUsername()); + return (authenticatedUser != null) ? new DataverseUserAdapter(session, realm, model, builtinUser, authenticatedUser) : null; } @Override - public UserModel getUserByUsername(RealmModel realmModel, String username) { - logger.info("getUserByUsername: " + username); - TypedQuery query = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class); - query.setParameter("username", username); - List builtinUsersResult = query.getResultList(); - if (builtinUsersResult.isEmpty()) { - logger.info("Could not find builtin user by username: " + username); + public UserModel getUserByUsername(RealmModel realm, String username) { + logger.infof("Fetching user by username: %s", username); + List users = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) + .setParameter("username", username) + .getResultList(); + + if (users.isEmpty()) { + logger.infof("User not found by username: %s", username); return null; } + DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); - if (authenticatedUser == null) { - return null; - } - return new DataverseUserAdapter(session, realmModel, model, builtinUsersResult.get(0), authenticatedUser); + return (authenticatedUser != null) ? new DataverseUserAdapter(session, realm, model, users.get(0), authenticatedUser) : null; } @Override - public UserModel getUserByEmail(RealmModel realmModel, String email) { - logger.info("getUserByEmail: " + email); - TypedQuery authenticatedUserQuery = em.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class); - authenticatedUserQuery.setParameter("email", email); - List authenticatedUsersResult = authenticatedUserQuery.getResultList(); - if (authenticatedUsersResult.isEmpty()) { - logger.info("Could not find authenticated user by email: " + email); - return null; - } - DataverseAuthenticatedUser authenticatedUser = authenticatedUsersResult.get(0); - TypedQuery builtinUserQuery = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class); - String username = authenticatedUser.getUserIdentifier(); - builtinUserQuery.setParameter("username", username); - List builtinUsersResult = builtinUserQuery.getResultList(); - if (builtinUsersResult.isEmpty()) { - logger.info("Could not find builtin user by username: " + username); + public UserModel getUserByEmail(RealmModel realm, String email) { + logger.infof("Fetching user by email: %s", email); + List authUsers = em.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class) + .setParameter("email", email) + .getResultList(); + + if (authUsers.isEmpty()) { + logger.infof("User not found by email: %s", email); return null; } - return new DataverseUserAdapter(session, realmModel, model, builtinUsersResult.get(0), authenticatedUser); + + String username = authUsers.get(0).getUserIdentifier(); + List builtinUsers = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) + .setParameter("username", username) + .getResultList(); + + return (builtinUsers.isEmpty()) ? null : new DataverseUserAdapter(session, realm, model, builtinUsers.get(0), authUsers.get(0)); } @Override public boolean supportsCredentialType(String credentialType) { - return credentialType.equals(PasswordCredentialModel.TYPE); + return PasswordCredentialModel.TYPE.equals(credentialType); } @Override - public boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String credentialType) { - logger.info("isConfiguredFor called for user: " + userModel.getUsername() + " and credentialType: " + credentialType); + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + logger.infof("Checking credential configuration for user: %s, credentialType: %s", user.getUsername(), credentialType); return false; } + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + logger.infof("Validating credentials for user: %s", user.getUsername()); + + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel userCredential)) { + return false; + } + + return DataverseAPIService.canLogInAsBuiltinUser(user.getUsername(), userCredential.getValue()); + } + @Override public void close() { logger.info("Closing DataverseUserStorageProvider"); @@ -109,23 +117,20 @@ public void close() { } } - @Override - public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { - logger.info("isValid called for user: " + user.getUsername()); - if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel userCredential)) - return false; - String username = user.getUsername(); - String password = userCredential.getValue(); - return DataverseAPIService.canLogInAsBuiltinUser(username, password); - } - + /** + * Retrieves an authenticated user from Dataverse by username. + * + * @param username The username to look up. + * @return The authenticated user or null if not found. + */ private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String username) { - TypedQuery query = em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class); - query.setParameter("identifier", username); - DataverseAuthenticatedUser singleResult = query.getSingleResult(); - if (singleResult == null) { - logger.info("Could not find authenticated user by username: " + username); + try { + return em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class) + .setParameter("identifier", username) + .getSingleResult(); + } catch (Exception e) { + logger.infof("Could not find authenticated user by username: %s", username); + return null; } - return singleResult; } } From b54aeb84ffb01a36ca0d935c7217cb730c7468dd Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Feb 2025 08:41:16 +0000 Subject: [PATCH 10/70] Changed: temporal warning log levels --- .../authorization/AuthenticationServiceBean.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 032c1dd5164..ec42771c7d0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -1016,20 +1016,20 @@ public OAuth2UserRecord verifyOIDCBearerTokenAndGetOAuth2UserRecord(String beare // Retrieve OAuth2UserRecord if UserInfo is present Optional userInfo = provider.getUserInfo(accessToken); if (userInfo.isPresent()) { - logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided user info", provider.getId()); + logger.log(Level.WARNING, "Bearer token detected, provider {0} confirmed validity and provided user info", provider.getId()); return provider.getUserRecord(userInfo.get()); } } catch (IOException | OAuth2Exception e) { - logger.log(Level.FINE, "Bearer token detected, provider " + provider.getId() + " indicates an invalid Token, skipping", e); + logger.log(Level.WARNING, "Bearer token detected, provider " + provider.getId() + " indicates an invalid Token, skipping", e); } } } catch (ParseException e) { - logger.log(Level.FINE, "Bearer token detected, unable to parse bearer token (invalid Token)", e); + logger.log(Level.WARNING, "Bearer token detected, unable to parse bearer token (invalid Token)", e); throw new AuthorizationException(BundleUtil.getStringFromBundle("authenticationServiceBean.errors.invalidBearerToken")); } // If no provider validated the token, throw an authorization exception. - logger.log(Level.FINE, "Bearer token detected, yet no configured OIDC provider validated it."); + logger.log(Level.WARNING, "Bearer token detected, yet no configured OIDC provider validated it."); throw new AuthorizationException(BundleUtil.getStringFromBundle("authenticationServiceBean.errors.unauthorizedBearerToken")); } From 2206e2a7cbdb17e2c6390766b72dde14a0a4dcc4 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Feb 2025 09:02:25 +0000 Subject: [PATCH 11/70] Added: temporal warning log in AuthenticationServiceBean --- .../iq/dataverse/authorization/AuthenticationServiceBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index ec42771c7d0..0b9af8daeda 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -332,6 +332,7 @@ public boolean isEmailAddressAvailable(String email) { } public AuthenticatedUser lookupUser(UserRecordIdentifier id) { + logger.warning("lookupUser() called for repo id " + id + " and user id in repo " + id.userIdInRepo); return lookupUser(id.repoId, id.userIdInRepo); } From ec76ce3b197c693c5adb7d8064aa67790f926424 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Feb 2025 09:40:30 +0000 Subject: [PATCH 12/70] Changed: querying authenticated users before lookup in OIDC flow --- .../iq/dataverse/authorization/AuthenticationServiceBean.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 0b9af8daeda..efabb37a558 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -990,7 +990,9 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws // TODO: Get the identifier from an invalidating cache to avoid lookup bursts of the same token. // Tokens in the cache should be removed after some (configurable) time. OAuth2UserRecord oAuth2UserRecord = verifyOIDCBearerTokenAndGetOAuth2UserRecord(bearerToken); - return lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); + logger.log(Level.WARNING, "Received oAuth2UserRecord for username: " + oAuth2UserRecord.getUsername()); + AuthenticatedUser builtinAuthenticatedUser = getAuthenticatedUser(oAuth2UserRecord.getUsername()); + return builtinAuthenticatedUser != null ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); } /** From a1ea5e0391df8ad667bba799ea22e7aee083c2db Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Feb 2025 09:45:09 +0000 Subject: [PATCH 13/70] Changed: temporal disable unit test for PoC --- .../dataverse/authorization/AuthenticationServiceBeanTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index 56ac4eefb3d..f6e0e24db3b 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -14,6 +14,7 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.TypedQuery; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -23,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.*; +@Disabled public class AuthenticationServiceBeanTest { private AuthenticationServiceBean sut; From 87ab98904467e48e94c7ebc4beff9c1273e20e31 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Feb 2025 11:57:35 +0000 Subject: [PATCH 14/70] Changed: reverted log level back to warning in AuthenticationServiceBean --- .../authorization/AuthenticationServiceBean.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index efabb37a558..4ad7dcffd14 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -990,7 +990,7 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws // TODO: Get the identifier from an invalidating cache to avoid lookup bursts of the same token. // Tokens in the cache should be removed after some (configurable) time. OAuth2UserRecord oAuth2UserRecord = verifyOIDCBearerTokenAndGetOAuth2UserRecord(bearerToken); - logger.log(Level.WARNING, "Received oAuth2UserRecord for username: " + oAuth2UserRecord.getUsername()); + logger.log(Level.FINE, "Received oAuth2UserRecord for username: " + oAuth2UserRecord.getUsername()); AuthenticatedUser builtinAuthenticatedUser = getAuthenticatedUser(oAuth2UserRecord.getUsername()); return builtinAuthenticatedUser != null ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); } @@ -1009,7 +1009,7 @@ public OAuth2UserRecord verifyOIDCBearerTokenAndGetOAuth2UserRecord(String beare // Ensure at least one OIDC provider is configured to validate the token. if (providers.isEmpty()) { - logger.log(Level.WARNING, "Bearer token detected, no OIDC provider configured"); + logger.log(Level.FINE, "Bearer token detected, no OIDC provider configured"); throw new AuthorizationException(BundleUtil.getStringFromBundle("authenticationServiceBean.errors.bearerTokenDetectedNoOIDCProviderConfigured")); } @@ -1019,20 +1019,20 @@ public OAuth2UserRecord verifyOIDCBearerTokenAndGetOAuth2UserRecord(String beare // Retrieve OAuth2UserRecord if UserInfo is present Optional userInfo = provider.getUserInfo(accessToken); if (userInfo.isPresent()) { - logger.log(Level.WARNING, "Bearer token detected, provider {0} confirmed validity and provided user info", provider.getId()); + logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided user info", provider.getId()); return provider.getUserRecord(userInfo.get()); } } catch (IOException | OAuth2Exception e) { - logger.log(Level.WARNING, "Bearer token detected, provider " + provider.getId() + " indicates an invalid Token, skipping", e); + logger.log(Level.FINE, "Bearer token detected, provider " + provider.getId() + " indicates an invalid Token, skipping", e); } } } catch (ParseException e) { - logger.log(Level.WARNING, "Bearer token detected, unable to parse bearer token (invalid Token)", e); + logger.log(Level.FINE, "Bearer token detected, unable to parse bearer token (invalid Token)", e); throw new AuthorizationException(BundleUtil.getStringFromBundle("authenticationServiceBean.errors.invalidBearerToken")); } // If no provider validated the token, throw an authorization exception. - logger.log(Level.WARNING, "Bearer token detected, yet no configured OIDC provider validated it."); + logger.log(Level.FINE, "Bearer token detected, yet no configured OIDC provider validated it."); throw new AuthorizationException(BundleUtil.getStringFromBundle("authenticationServiceBean.errors.unauthorizedBearerToken")); } From 6b49842284ecc703ced5c9b05e591effcb33dd9a Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Feb 2025 11:58:23 +0000 Subject: [PATCH 15/70] Changed: log level in AuthenticationServiceBean --- .../iq/dataverse/authorization/AuthenticationServiceBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 4ad7dcffd14..1337cfe7d43 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -1009,7 +1009,7 @@ public OAuth2UserRecord verifyOIDCBearerTokenAndGetOAuth2UserRecord(String beare // Ensure at least one OIDC provider is configured to validate the token. if (providers.isEmpty()) { - logger.log(Level.FINE, "Bearer token detected, no OIDC provider configured"); + logger.log(Level.WARNING, "Bearer token detected, no OIDC provider configured"); throw new AuthorizationException(BundleUtil.getStringFromBundle("authenticationServiceBean.errors.bearerTokenDetectedNoOIDCProviderConfigured")); } From 04f7b39017ca1674bdc1f254f8b238e1dad3b239 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 10 Feb 2025 15:46:11 +0000 Subject: [PATCH 16/70] Added: UserEventListenerProvider logging users login --- .../spi/DataverseUserStorageProvider.java | 1 - .../auth/spi/UserEventListenerProvider.java | 32 +++++++++++++ .../spi/UserEventListenerProviderFactory.java | 48 +++++++++++++++++++ ...ycloak.events.EventListenerProviderFactory | 1 + 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java index a75d3e9ce66..aa39c2b4ef6 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java @@ -1,7 +1,6 @@ package edu.harvard.iq.keycloak.auth.spi; import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; import org.keycloak.connections.jpa.JpaConnectionProvider; diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java new file mode 100644 index 00000000000..1b058c16f12 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java @@ -0,0 +1,32 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import org.keycloak.events.Event; +import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.KeycloakSession; +import org.keycloak.events.EventType; + +import org.jboss.logging.Logger; + +public class UserEventListenerProvider implements EventListenerProvider { + private static final Logger logger = Logger.getLogger(UserEventListenerProvider.class); + private final KeycloakSession session; + + public UserEventListenerProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public void onEvent(Event event) { + if (event.getType() == EventType.LOGIN || event.getType() == EventType.REGISTER) { + logger.infof("Event captured: %s for user: %s", event.getType(), event.getUserId()); + } + } + + @Override + public void onEvent(AdminEvent event, boolean b) { + } + + @Override + public void close() {} +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java new file mode 100644 index 00000000000..d4745371d4f --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java @@ -0,0 +1,48 @@ +package edu.harvard.iq.keycloak.auth.spi; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.events.EventListenerProviderFactory; +import org.keycloak.events.EventListenerProvider; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.List; + +public class UserEventListenerProviderFactory implements EventListenerProviderFactory { + + @Override + public String getId() { + return "user-event-listener"; + } + + @Override + public int order() { + return EventListenerProviderFactory.super.order(); + } + + @Override + public List getConfigMetadata() { + return EventListenerProviderFactory.super.getConfigMetadata(); + } + + @Override + public EventListenerProvider create(KeycloakSession session) { + return new UserEventListenerProvider(session); + } + + @Override + public void init(Config.Scope scope) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // Post-init if needed + } + + @Override + public void close() { + // Close if needed + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory new file mode 100644 index 00000000000..1fdc67176b6 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory @@ -0,0 +1 @@ +edu.harvard.iq.keycloak.auth.spi.UserEventListenerProviderFactory \ No newline at end of file From 267c6e7aa62a60a97cad544ce4271e9655c2d515 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 16:14:31 +0000 Subject: [PATCH 17/70] Removed: event listeners for a simpler first iteration --- .../auth/spi/UserEventListenerProvider.java | 32 ------------- .../spi/UserEventListenerProviderFactory.java | 48 ------------------- ...ycloak.events.EventListenerProviderFactory | 1 - 3 files changed, 81 deletions(-) delete mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java delete mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java delete mode 100644 conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java deleted file mode 100644 index 1b058c16f12..00000000000 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -package edu.harvard.iq.keycloak.auth.spi; - -import org.keycloak.events.Event; -import org.keycloak.events.EventListenerProvider; -import org.keycloak.events.admin.AdminEvent; -import org.keycloak.models.KeycloakSession; -import org.keycloak.events.EventType; - -import org.jboss.logging.Logger; - -public class UserEventListenerProvider implements EventListenerProvider { - private static final Logger logger = Logger.getLogger(UserEventListenerProvider.class); - private final KeycloakSession session; - - public UserEventListenerProvider(KeycloakSession session) { - this.session = session; - } - - @Override - public void onEvent(Event event) { - if (event.getType() == EventType.LOGIN || event.getType() == EventType.REGISTER) { - logger.infof("Event captured: %s for user: %s", event.getType(), event.getUserId()); - } - } - - @Override - public void onEvent(AdminEvent event, boolean b) { - } - - @Override - public void close() {} -} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java deleted file mode 100644 index d4745371d4f..00000000000 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/UserEventListenerProviderFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -package edu.harvard.iq.keycloak.auth.spi; - -import org.keycloak.Config; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.events.EventListenerProviderFactory; -import org.keycloak.events.EventListenerProvider; -import org.keycloak.provider.ProviderConfigProperty; - -import java.util.List; - -public class UserEventListenerProviderFactory implements EventListenerProviderFactory { - - @Override - public String getId() { - return "user-event-listener"; - } - - @Override - public int order() { - return EventListenerProviderFactory.super.order(); - } - - @Override - public List getConfigMetadata() { - return EventListenerProviderFactory.super.getConfigMetadata(); - } - - @Override - public EventListenerProvider create(KeycloakSession session) { - return new UserEventListenerProvider(session); - } - - @Override - public void init(Config.Scope scope) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - // Post-init if needed - } - - @Override - public void close() { - // Close if needed - } -} diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory deleted file mode 100644 index 1fdc67176b6..00000000000 --- a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory +++ /dev/null @@ -1 +0,0 @@ -edu.harvard.iq.keycloak.auth.spi.UserEventListenerProviderFactory \ No newline at end of file From c49b35d7da45d6bf16555112acb2b4cc5d5454b3 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 21:19:23 +0000 Subject: [PATCH 18/70] Changed: parametrized quarkus properties in builtin-users-spi --- .../keycloak/builtin-users-spi/conf/quarkus.properties | 10 +++++----- docker-compose-dev.yml | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/conf/quarkus.properties b/conf/keycloak/builtin-users-spi/conf/quarkus.properties index 99af38348d3..26aa7616235 100644 --- a/conf/keycloak/builtin-users-spi/conf/quarkus.properties +++ b/conf/keycloak/builtin-users-spi/conf/quarkus.properties @@ -1,14 +1,14 @@ quarkus.datasource.user-store.db-kind=postgresql -quarkus.datasource.user-store.jdbc.url=jdbc:postgresql://postgres:5432/dataverse +quarkus.datasource.user-store.jdbc.url=jdbc:postgresql://${DATAVERSE_DB_HOST}:${DATAVERSE_DB_PORT}/dataverse quarkus.datasource.user-store.username=${DATAVERSE_DB_USER} -quarkus.datasource.user-store.password=secret +quarkus.datasource.user-store.password=${DATAVERSE_DB_PASSWORD} quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver quarkus.datasource.user-store.jdbc.transactions=disabled quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER} -quarkus.datasource.user-store.jdbc.recovery.password=secret +quarkus.datasource.user-store.jdbc.recovery.password=${DATAVERSE_DB_PASSWORD} -quarkus.datasource.user-store.jdbc.xa-properties.serverName=postgres -quarkus.datasource.user-store.jdbc.xa-properties.portNumber=5432 +quarkus.datasource.user-store.jdbc.xa-properties.serverName=${DATAVERSE_DB_HOST} +quarkus.datasource.user-store.jdbc.xa-properties.portNumber=${DATAVERSE_DB_PORT} quarkus.datasource.user-store.jdbc.xa-properties.databaseName=dataverse diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 3c425f811cb..d21a32efecc 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -177,7 +177,10 @@ services: - KEYCLOAK_ADMIN_PASSWORD=kcpassword - KEYCLOAK_LOGLEVEL=DEBUG - KC_HOSTNAME_STRICT=false + - DATAVERSE_DB_HOST=postgres + - DATAVERSE_DB_PORT=5432 - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} + - DATAVERSE_DB_PASSWORD=secret networks: dataverse: aliases: From fbfa9c45af970646269bae362cb994da0a1eecb2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 21:36:49 +0000 Subject: [PATCH 19/70] Changed: spi folders structure and parametrized DATAVERSE_BASE_URL --- .../auth/spi/{ => adapters}/DataverseUserAdapter.java | 4 +++- .../auth/spi/{ => models}/DataverseAuthenticatedUser.java | 2 +- .../auth/spi/{ => models}/DataverseBuiltinUser.java | 2 +- .../spi/{ => providers}/DataverseUserStorageProvider.java | 6 +++++- .../DataverseUserStorageProviderFactory.java | 2 +- .../auth/spi/{ => services}/DataverseAPIService.java | 6 ++++-- .../src/main/resources/META-INF/persistence.xml | 4 ++-- .../org.keycloak.storage.UserStorageProviderFactory | 2 +- docker-compose-dev.yml | 1 + 9 files changed, 19 insertions(+), 10 deletions(-) rename conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/{ => adapters}/DataverseUserAdapter.java (90%) rename conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/{ => models}/DataverseAuthenticatedUser.java (95%) rename conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/{ => models}/DataverseBuiltinUser.java (90%) rename conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/{ => providers}/DataverseUserStorageProvider.java (94%) rename conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/{ => providers}/DataverseUserStorageProviderFactory.java (94%) rename conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/{ => services}/DataverseAPIService.java (86%) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java similarity index 90% rename from conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java rename to conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java index 15835b92569..0dec1a6bdb0 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserAdapter.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java @@ -1,5 +1,7 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.adapters; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; import org.keycloak.component.ComponentModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java similarity index 95% rename from conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java rename to conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java index cb1cc76eef4..c64c227f7dd 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAuthenticatedUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.models; import jakarta.persistence.*; diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java similarity index 90% rename from conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java rename to conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java index 4af9e78aa5f..d16d10960d9 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseBuiltinUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.models; import jakarta.persistence.*; diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java similarity index 94% rename from conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java rename to conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java index aa39c2b4ef6..a7e236a0440 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java @@ -1,5 +1,9 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.providers; +import edu.harvard.iq.keycloak.auth.spi.adapters.DataverseUserAdapter; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import edu.harvard.iq.keycloak.auth.spi.services.DataverseAPIService; import jakarta.persistence.EntityManager; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java similarity index 94% rename from conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java rename to conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java index 30da64e565d..91f78f8efa0 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseUserStorageProviderFactory.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.providers; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java similarity index 86% rename from conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java rename to conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java index 60b0288768d..a3efc8c8e09 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/DataverseAPIService.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.services; import org.jboss.logging.Logger; @@ -14,7 +14,9 @@ public class DataverseAPIService { private static final Logger logger = Logger.getLogger(DataverseAPIService.class); - private static final String DATAVERSE_API_URL = "http://dataverse:8080/api/builtin-users/%s/canLoginWithGivenCredentials?password=%s"; + + private static final String DATAVERSE_BASE_URL = System.getenv("DATAVERSE_BASE_URL"); + private static final String DATAVERSE_API_URL = String.format("%s/api/builtin-users/%%s/canLoginWithGivenCredentials?password=%%s", DATAVERSE_BASE_URL); /** * Validates if a Dataverse built-in user can log in with the given credentials. diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml index 9e579281ba6..5c1e867dc6d 100644 --- a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/persistence.xml @@ -4,8 +4,8 @@ xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> - edu.harvard.iq.keycloak.auth.spi.DataverseBuiltinUser - edu.harvard.iq.keycloak.auth.spi.DataverseAuthenticatedUser + edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser + edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser diff --git a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory index 504e684acd5..4ec99f734db 100644 --- a/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory +++ b/conf/keycloak/builtin-users-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -1 +1 @@ -edu.harvard.iq.keycloak.auth.spi.DataverseUserStorageProviderFactory \ No newline at end of file +edu.harvard.iq.keycloak.auth.spi.providers.DataverseUserStorageProviderFactory \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index d21a32efecc..c37e9b1bc98 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -181,6 +181,7 @@ services: - DATAVERSE_DB_PORT=5432 - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} - DATAVERSE_DB_PASSWORD=secret + - DATAVERSE_BASE_URL=http://dataverse:8080 networks: dataverse: aliases: From 7abce926bcaf04dbb0e972fcb3d092f170210387 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 22:34:57 +0000 Subject: [PATCH 20/70] Added: testing dependencies --- conf/keycloak/builtin-users-spi/pom.xml | 20 +++++++++++++++++++ .../DataverseUserStorageProviderTest.java | 4 ++++ 2 files changed, 24 insertions(+) create mode 100644 conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index c18c4ff14df..13a1d4d0045 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -45,6 +45,24 @@ jakarta.persistence-api ${jakarta.persistence.version} + + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + + + org.mockito + mockito-core + ${mockito.version} + test + @@ -63,5 +81,7 @@ 22.0.0 17 3.2.0 + 5.15.2 + 5.11.4 diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java new file mode 100644 index 00000000000..db2b221faaf --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java @@ -0,0 +1,4 @@ +package edu.harvard.iq.keycloak.auth.spi; + +public class DataverseUserStorageProviderTest { +} From 26b5f57cccdb67a94eb72958a634885bb1e052da Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 22:35:53 +0000 Subject: [PATCH 21/70] Added: DataverseUserStorageProviderTest --- .../models/DataverseAuthenticatedUser.java | 12 ++ .../auth/spi/models/DataverseBuiltinUser.java | 8 + .../DataverseUserStorageProviderTest.java | 151 +++++++++++++++++- 3 files changed, 169 insertions(+), 2 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java index c64c227f7dd..d2d1e292ade 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseAuthenticatedUser.java @@ -18,6 +18,18 @@ public class DataverseAuthenticatedUser { private String firstName; private String userIdentifier; + public void setId(Integer id) { + this.id = id; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setUserIdentifier(String userIdentifier) { + this.userIdentifier = userIdentifier; + } + public String getEmail() { return email; } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java index d16d10960d9..d2568e5ed9a 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java @@ -14,6 +14,14 @@ public class DataverseBuiltinUser { private String username; + public void setId(Integer id) { + this.id = id; + } + + public void setUsername(String username) { + this.username = username; + } + public Integer getId() { return id; } diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java index db2b221faaf..40f13f34598 100644 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java @@ -1,4 +1,151 @@ -package edu.harvard.iq.keycloak.auth.spi; +package edu.harvard.iq.keycloak.auth.spi.providers; -public class DataverseUserStorageProviderTest { +import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keycloak.component.ComponentModel; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class DataverseUserStorageProviderTest { + + private EntityManager entityManagerMock; + private RealmModel realmStub; + private DataverseUserStorageProvider sut; + + @BeforeEach + void setUp() { + entityManagerMock = mock(EntityManager.class); + realmStub = mock(RealmModel.class); + KeycloakSession sessionMock = mock(KeycloakSession.class); + + JpaConnectionProvider jpaConnectionProviderMock = mock(JpaConnectionProvider.class); + when(sessionMock.getProvider(JpaConnectionProvider.class, "user-store")).thenReturn(jpaConnectionProviderMock); + when(jpaConnectionProviderMock.getEntityManager()).thenReturn(entityManagerMock); + + sut = new DataverseUserStorageProvider(sessionMock, mock(ComponentModel.class)); + } + + @Test + void getUserById_userExists() { + String testUserId = "123"; + String testUsername = "testuser"; + + DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); + builtinUser.setId(1); + builtinUser.setUsername(testUsername); + + when(entityManagerMock.find(DataverseBuiltinUser.class, "123")).thenReturn(builtinUser); + TypedQuery authUserQuery = mock(TypedQuery.class); + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class)) + .thenReturn(authUserQuery); + when(authUserQuery.setParameter("identifier", testUsername)).thenReturn(authUserQuery); + + DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); + authUser.setUserIdentifier(testUsername); + when(authUserQuery.getSingleResult()).thenReturn(authUser); + + UserModel user = sut.getUserById(realmStub, testUserId); + assertNotNull(user); + assertEquals(testUsername, user.getUsername()); + } + + @Test + void getUserById_userNotFound() { + when(entityManagerMock.find(DataverseBuiltinUser.class, "123")).thenReturn(null); + assertNull(sut.getUserById(realmStub, "123")); + } + + @Test + void getUserByUsername_userExists() { + String testUsername = "testuser"; + + DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); + builtinUser.setUsername(testUsername); + builtinUser.setId(1); + + DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); + authUser.setUserIdentifier(testUsername); + + TypedQuery builtinUserQuery = mock(TypedQuery.class); + TypedQuery authUserQuery = mock(TypedQuery.class); + + when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) + .thenReturn(builtinUserQuery); + when(builtinUserQuery.setParameter("username", testUsername)).thenReturn(builtinUserQuery); + when(builtinUserQuery.getResultList()).thenReturn(Collections.singletonList(builtinUser)); + + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class)) + .thenReturn(authUserQuery); + when(authUserQuery.setParameter("identifier", testUsername)).thenReturn(authUserQuery); + when(authUserQuery.getSingleResult()).thenReturn(authUser); + + UserModel user = sut.getUserByUsername(realmStub, testUsername); + assertNotNull(user); + assertEquals(testUsername, user.getUsername()); + } + + @Test + void getUserByUsername_userNotFound() { + TypedQuery query = mock(TypedQuery.class); + when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) + .thenReturn(query); + when(query.setParameter("username", "unknown")).thenReturn(query); + when(query.getResultList()).thenReturn(Collections.emptyList()); + + assertNull(sut.getUserByUsername(realmStub, "unknown")); + } + + @Test + void getUserByEmail_userExists() { + String testEmail = "test@dataverse.org"; + String testUsername = "testuser"; + + DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); + authUser.setEmail(testEmail); + authUser.setId(1); + authUser.setUserIdentifier(testUsername); + + DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); + builtinUser.setUsername(testUsername); + builtinUser.setId(1); + + TypedQuery authUserQuery = mock(TypedQuery.class); + TypedQuery builtinUserQuery = mock(TypedQuery.class); + + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class)) + .thenReturn(authUserQuery); + when(authUserQuery.setParameter("email", testEmail)).thenReturn(authUserQuery); + when(authUserQuery.getResultList()).thenReturn(Collections.singletonList(authUser)); + + when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) + .thenReturn(builtinUserQuery); + when(builtinUserQuery.setParameter("username", testUsername)).thenReturn(builtinUserQuery); + when(builtinUserQuery.getResultList()).thenReturn(Collections.singletonList(builtinUser)); + + UserModel user = sut.getUserByEmail(realmStub, testEmail); + assertNotNull(user); + assertEquals(testUsername, user.getUsername()); + } + + @Test + void getUserByEmail_userNotFound() { + TypedQuery query = mock(TypedQuery.class); + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class)) + .thenReturn(query); + when(query.setParameter("email", "unknown@dataverse.org")).thenReturn(query); + when(query.getResultList()).thenReturn(Collections.emptyList()); + + assertNull(sut.getUserByEmail(realmStub, "unknown@dataverse.org")); + } } From 9c25eeb29c8b132ec773206913c29293ca190bb8 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 22:51:44 +0000 Subject: [PATCH 22/70] Added: DataverseAPIServiceTest --- .../DataverseUserStorageProvider.java | 3 +- .../spi/services/DataverseAPIService.java | 25 +++++++--- .../spi/services/DataverseAPIServiceTest.java | 48 +++++++++++++++++++ 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java index a7e236a0440..7159f5ebe78 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java @@ -109,7 +109,8 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) return false; } - return DataverseAPIService.canLogInAsBuiltinUser(user.getUsername(), userCredential.getValue()); + DataverseAPIService dataverseAPIService = new DataverseAPIService(); + return dataverseAPIService.canLogInAsBuiltinUser(user.getUsername(), userCredential.getValue()); } @Override diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java index a3efc8c8e09..a8969e4efde 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java @@ -18,6 +18,16 @@ public class DataverseAPIService { private static final String DATAVERSE_BASE_URL = System.getenv("DATAVERSE_BASE_URL"); private static final String DATAVERSE_API_URL = String.format("%s/api/builtin-users/%%s/canLoginWithGivenCredentials?password=%%s", DATAVERSE_BASE_URL); + private URL requestUrl; + + public DataverseAPIService() { + } + + // Constructor for testing purposes + public DataverseAPIService(URL requestUrl) { + this.requestUrl = requestUrl; + } + /** * Validates if a Dataverse built-in user can log in with the given credentials. * @@ -25,16 +35,17 @@ public class DataverseAPIService { * @param password The password to be validated. * @return {@code true} if the user can log in, {@code false} otherwise. */ - public static boolean canLogInAsBuiltinUser(String username, String password) { + public boolean canLogInAsBuiltinUser(String username, String password) { HttpURLConnection connection = null; try { - String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); - String encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8); - String requestUrl = String.format(DATAVERSE_API_URL, encodedUsername, encodedPassword); - - URL url = new URL(requestUrl); - connection = (HttpURLConnection) url.openConnection(); + if (requestUrl == null) { + String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); + String encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8); + String requestUrlString = String.format(DATAVERSE_API_URL, encodedUsername, encodedPassword); + requestUrl = new URL(requestUrlString); + } + connection = (HttpURLConnection) requestUrl.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Accept", "application/json"); diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java new file mode 100644 index 00000000000..b05abbdfbe7 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java @@ -0,0 +1,48 @@ +package edu.harvard.iq.keycloak.auth.spi.services; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class DataverseAPIServiceTest { + + private HttpURLConnection connectionMock; + private URL urlMock; + private DataverseAPIService sut; + + @BeforeEach + void setUp() throws Exception { + connectionMock = mock(HttpURLConnection.class); + urlMock = mock(URL.class); + when(urlMock.openConnection()).thenReturn(connectionMock); + + sut = new DataverseAPIService(urlMock); + } + + @Test + void canLogInAsBuiltinUser_validCredentials() throws Exception { + when(connectionMock.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + boolean result = sut.canLogInAsBuiltinUser("validUser", "validPass"); + assertTrue(result); + } + + @Test + void canLogInAsBuiltinUser_invalidCredentials() throws Exception { + when(connectionMock.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + boolean result = sut.canLogInAsBuiltinUser("invalidUser", "invalidPass"); + assertFalse(result); + } + + @Test + void canLogInAsBuiltinUser_apiError() throws Exception { + when(urlMock.openConnection()).thenThrow(new IOException("Connection error")); + boolean result = sut.canLogInAsBuiltinUser("errorUser", "errorPass"); + assertFalse(result); + } +} From ee6e9f098f6c180c953db104eafde45191ea0ed0 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 22:59:11 +0000 Subject: [PATCH 23/70] Removed: logs from AuthenticationServiceBean --- .../iq/dataverse/authorization/AuthenticationServiceBean.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 1337cfe7d43..238ef13a1fd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -332,7 +332,6 @@ public boolean isEmailAddressAvailable(String email) { } public AuthenticatedUser lookupUser(UserRecordIdentifier id) { - logger.warning("lookupUser() called for repo id " + id + " and user id in repo " + id.userIdInRepo); return lookupUser(id.repoId, id.userIdInRepo); } @@ -990,7 +989,6 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws // TODO: Get the identifier from an invalidating cache to avoid lookup bursts of the same token. // Tokens in the cache should be removed after some (configurable) time. OAuth2UserRecord oAuth2UserRecord = verifyOIDCBearerTokenAndGetOAuth2UserRecord(bearerToken); - logger.log(Level.FINE, "Received oAuth2UserRecord for username: " + oAuth2UserRecord.getUsername()); AuthenticatedUser builtinAuthenticatedUser = getAuthenticatedUser(oAuth2UserRecord.getUsername()); return builtinAuthenticatedUser != null ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); } From 6695b7995f41085aa9c8de1800c4d7e147ef88c0 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 23:18:44 +0000 Subject: [PATCH 24/70] Changed: AuthenticationServiceBeanTest re-enabled with updates --- .../AuthenticationServiceBeanTest.java | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index f6e0e24db3b..89d610a7151 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -14,7 +14,6 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.TypedQuery; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -24,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.*; -@Disabled public class AuthenticationServiceBeanTest { private AuthenticationServiceBean sut; @@ -85,8 +83,11 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken() throws ParseExcept // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); + // Setting up not authenticated builtin user is found + setupAuthenticatedUserByIdentifierQueryWithResult(null); + // Setting up an authenticated user is found - AuthenticatedUser authenticatedUser = setupAuthenticatedUserQueryWithResult(new AuthenticatedUser()); + AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser()); // When invoking lookupUserByOIDCBearerToken User actualUser = sut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); @@ -100,6 +101,9 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_noAccount() throws P // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); + // Setting up not authenticated builtin user is found + setupAuthenticatedUserByIdentifierQueryWithResult(null); + // Setting up an authenticated user is not found setupAuthenticatedUserQueryWithNoResult(); @@ -110,13 +114,20 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_noAccount() throws P assertNull(actualUser); } - private AuthenticatedUser setupAuthenticatedUserQueryWithResult(AuthenticatedUser authenticatedUser) { - TypedQuery queryStub = Mockito.mock(TypedQuery.class); - AuthenticatedUserLookup lookupStub = Mockito.mock(AuthenticatedUserLookup.class); - Mockito.when(lookupStub.getAuthenticatedUser()).thenReturn(authenticatedUser); - Mockito.when(queryStub.getSingleResult()).thenReturn(lookupStub); - Mockito.when(sut.em.createNamedQuery("AuthenticatedUserLookup.findByAuthPrvID_PersUserId", AuthenticatedUserLookup.class)).thenReturn(queryStub); - return authenticatedUser; + @Test + void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsBuiltin() throws ParseException, IOException, AuthorizationException, OAuth2Exception { + // Given a single OIDC provider that returns a valid user identifier + setUpOIDCProviderWhichValidatesToken(); + + // Setting up authenticated builtin user is found + AuthenticatedUser authenticatedUser = new AuthenticatedUser(); + setupAuthenticatedUserByIdentifierQueryWithResult(authenticatedUser); + + // When invoking lookupUserByOIDCBearerToken + User actualUser = sut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); + + // Then the actual user should match the expected authenticated user + assertEquals(authenticatedUser, actualUser); } private void setupAuthenticatedUserQueryWithNoResult() { @@ -140,6 +151,7 @@ private void setUpOIDCProviderWhichValidatesToken() throws ParseException, IOExc Mockito.when(userRecordIdentifierStub.getUserIdInRepo()).thenReturn("testUserId"); Mockito.when(userRecordIdentifierStub.getUserRepoId()).thenReturn("testRepoId"); Mockito.when(oAuth2UserRecordStub.getUserRecordIdentifier()).thenReturn(userRecordIdentifierStub); + Mockito.when(oAuth2UserRecordStub.getUsername()).thenReturn("testUsername"); // Stub the OIDCAuthProvider to return OAuth2UserRecord Mockito.when(oidcAuthProviderStub.getUserRecord(userInfoStub)).thenReturn(oAuth2UserRecordStub); @@ -151,4 +163,21 @@ private OIDCAuthProvider stubOIDCAuthProvider(String providerID) { Mockito.when(sut.authProvidersRegistrationService.getAuthenticationProvidersMap()).thenReturn(Map.of(providerID, oidcAuthProviderStub)); return oidcAuthProviderStub; } + + private AuthenticatedUser setupAuthenticatedUserByAuthPrvIDQueryWithResult(AuthenticatedUser authenticatedUser) { + TypedQuery queryStub = Mockito.mock(TypedQuery.class); + AuthenticatedUserLookup lookupStub = Mockito.mock(AuthenticatedUserLookup.class); + Mockito.when(lookupStub.getAuthenticatedUser()).thenReturn(authenticatedUser); + Mockito.when(queryStub.getSingleResult()).thenReturn(lookupStub); + Mockito.when(sut.em.createNamedQuery("AuthenticatedUserLookup.findByAuthPrvID_PersUserId", AuthenticatedUserLookup.class)).thenReturn(queryStub); + return authenticatedUser; + } + + private AuthenticatedUser setupAuthenticatedUserByIdentifierQueryWithResult(AuthenticatedUser authenticatedUser) { + TypedQuery queryStub = Mockito.mock(TypedQuery.class); + Mockito.when(queryStub.getSingleResult()).thenReturn(authenticatedUser); + Mockito.when(queryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(queryStub); + Mockito.when(sut.em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class)).thenReturn(queryStub); + return authenticatedUser; + } } From 92aaa91a28cbc338eb04f444f4bc29bde097f098 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Feb 2025 23:19:57 +0000 Subject: [PATCH 25/70] Refactor: AuthenticationServiceBeanTest.setupAuthenticatedUserByIdentifierQueryWithResult method --- .../dataverse/authorization/AuthenticationServiceBeanTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index 89d610a7151..b1e5cb3926a 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -173,11 +173,10 @@ private AuthenticatedUser setupAuthenticatedUserByAuthPrvIDQueryWithResult(Authe return authenticatedUser; } - private AuthenticatedUser setupAuthenticatedUserByIdentifierQueryWithResult(AuthenticatedUser authenticatedUser) { + private void setupAuthenticatedUserByIdentifierQueryWithResult(AuthenticatedUser authenticatedUser) { TypedQuery queryStub = Mockito.mock(TypedQuery.class); Mockito.when(queryStub.getSingleResult()).thenReturn(authenticatedUser); Mockito.when(queryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(queryStub); Mockito.when(sut.em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class)).thenReturn(queryStub); - return authenticatedUser; } } From 169aa75990afa501f854f211db7cf023e963e854 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 12 Feb 2025 14:51:46 +0000 Subject: [PATCH 26/70] Changed: updated oauth2-oidc-sdk and excluded builtin-users-spi from tests --- pom.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cb16f16c229..e81ea5c197d 100644 --- a/pom.xml +++ b/pom.xml @@ -494,7 +494,7 @@ com.nimbusds oauth2-oidc-sdk - 10.13.2 + 11.22.1 @@ -974,6 +974,9 @@ ${testsToExclude} ${skipUnitTests} ${surefire.jacoco.args} ${argLine} + + **/builtin-users-spi/** + From 7fd32c9eac926d2c5d0fd3097b5ae5dca77f6a65 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 14 Feb 2025 18:37:06 +0000 Subject: [PATCH 27/70] Added: API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH feature flag --- docker-compose-dev.yml | 1 + .../authorization/AuthenticationServiceBean.java | 16 ++++++++++++++-- .../iq/dataverse/settings/FeatureFlags.java | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index c37e9b1bc98..501c76c217b 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -18,6 +18,7 @@ services: DATAVERSE_JSF_REFRESH_PERIOD: "1" DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_FEATURE_API_BEARER_AUTH_PROVIDE_MISSING_CLAIMS: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 238ef13a1fd..e8a93c0befa 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -37,6 +37,7 @@ import edu.harvard.iq.dataverse.privateurl.PrivateUrl; import edu.harvard.iq.dataverse.privateurl.PrivateUrlServiceBean; import edu.harvard.iq.dataverse.search.savedsearch.SavedSearchServiceBean; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; import edu.harvard.iq.dataverse.workflow.PendingWorkflowInvocation; @@ -989,8 +990,11 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws // TODO: Get the identifier from an invalidating cache to avoid lookup bursts of the same token. // Tokens in the cache should be removed after some (configurable) time. OAuth2UserRecord oAuth2UserRecord = verifyOIDCBearerTokenAndGetOAuth2UserRecord(bearerToken); - AuthenticatedUser builtinAuthenticatedUser = getAuthenticatedUser(oAuth2UserRecord.getUsername()); - return builtinAuthenticatedUser != null ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); + if (FeatureFlags.API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH.enabled()) { + AuthenticatedUser builtinAuthenticatedUser = getBuiltinAuthenticatedUserByIdentifier(oAuth2UserRecord.getUsername()); + return (builtinAuthenticatedUser != null) ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); + } + return lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); } /** @@ -1044,4 +1048,12 @@ private List getAvailableOidcProviders() { .map(providerId -> (OIDCAuthProvider) getAuthenticationProvider(providerId)) .toList(); } + + private AuthenticatedUser getBuiltinAuthenticatedUserByIdentifier(String identifier) { + AuthenticatedUser builtinAuthenticatedUser = getAuthenticatedUserWithProvider(identifier); + if (builtinAuthenticatedUser != null && builtinAuthenticatedUser.getAuthProviderId().equals("builtin")) { + return builtinAuthenticatedUser; + } + return null; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 04ae0018323..29e34604724 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -45,7 +45,7 @@ public enum FeatureFlags { * {@link #API_BEARER_AUTH} is enabled.

* * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth-provide-missing-claims" - * @since Dataverse @TODO: + * @since Dataverse 6.6: */ API_BEARER_AUTH_PROVIDE_MISSING_CLAIMS("api-bearer-auth-provide-missing-claims"), /** @@ -56,9 +56,21 @@ public enum FeatureFlags { * {@link #API_BEARER_AUTH} is enabled.

* * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth-handle-tos-acceptance-in-idp" - * @since Dataverse @TODO: + * @since Dataverse 6.6: */ API_BEARER_AUTH_HANDLE_TOS_ACCEPTANCE_IN_IDP("api-bearer-auth-handle-tos-acceptance-in-idp"), + /** + * Allows the use of a built-in user account when an identity match is found during API bearer authentication. + * This feature enables automatic association of an incoming IdP identity with an existing built-in user account, + * bypassing the need for additional user registration steps. + * + *

The value of this feature flag is only considered when the feature flag + * {@link #API_BEARER_AUTH} is enabled.

+ * + * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth-use-builtin-user-on-id-match" + * @since Dataverse @TODO: + */ + API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH("api-bearer-auth-use-builtin-user-on-id-match"), /** * For published (public) objects, don't use a join when searching Solr. * Experimental! Requires a reindex with the following feature flag enabled, From 67552e14463cc3d8765bd8f54fd813547a18e3ac Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 14 Feb 2025 18:40:30 +0000 Subject: [PATCH 28/70] Added: docs for api-bearer-auth-use-builtin-user-on-id-match --- doc/sphinx-guides/source/installation/config.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 05a17992acf..04d743885d5 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3470,6 +3470,9 @@ please find all known feature flags below. Any of these flags can be activated u * - api-bearer-auth-handle-tos-acceptance-in-idp - Specifies that Terms of Service acceptance is handled by the IdP, eliminating the need to include ToS acceptance boolean parameter (termsAccepted) in the OIDC user registration request body. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. - ``Off`` + * - api-bearer-auth-use-builtin-user-on-id-match + - Allows the use of a built-in user account when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this feature flag exposes the installation to potential user impersonation issues.** + - ``Off`` * - avoid-expensive-solr-join - Changes the way Solr queries are constructed for public content (published Collections, Datasets and Files). It removes a very expensive Solr join on all such documents, improving overall performance, especially for large instances under heavy load. Before this feature flag is enabled, the corresponding indexing feature (see next feature flag) must be turned on and a full reindex performed (otherwise public objects are not going to be shown in search results). See :doc:`/admin/solr-search-index`. - ``Off`` From 22fc4da0ca3c7cb491c01648d4222ee293f15d8a Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 14 Feb 2025 19:00:05 +0000 Subject: [PATCH 29/70] Changed: null check added to AuthenticationServiceBean and updated tests in AuthenticationServiceBeanTest --- .../AuthenticationServiceBean.java | 8 ++- .../AuthenticationServiceBeanTest.java | 52 ++++++++++++++----- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index e8a93c0befa..4f34b7083c0 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -245,11 +245,15 @@ public AuthenticatedUser getAuthenticatedUserWithProvider( String identifier ) { AuthenticatedUser authenticatedUser = em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class) .setParameter("identifier", identifier) .getSingleResult(); + AuthenticatedUserLookup aul = em.createNamedQuery("AuthenticatedUserLookup.findByAuthUser", AuthenticatedUserLookup.class) .setParameter("authUser", authenticatedUser) .getSingleResult(); - authenticatedUser.setAuthProviderId(aul.getAuthenticationProviderId()); - + + if (authenticatedUser != null) { + authenticatedUser.setAuthProviderId(aul.getAuthenticationProviderId()); + } + return authenticatedUser; } catch ( NoResultException nre ) { return null; diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index b1e5cb3926a..b01276e82ba 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -9,7 +9,10 @@ import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.testing.JvmSetting; +import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; import jakarta.persistence.EntityManager; import jakarta.persistence.NoResultException; import jakarta.persistence.TypedQuery; @@ -23,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.*; +@LocalJvmSettings public class AuthenticationServiceBeanTest { private AuthenticationServiceBean sut; @@ -83,9 +87,6 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken() throws ParseExcept // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); - // Setting up not authenticated builtin user is found - setupAuthenticatedUserByIdentifierQueryWithResult(null); - // Setting up an authenticated user is found AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser()); @@ -101,9 +102,6 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_noAccount() throws P // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); - // Setting up not authenticated builtin user is found - setupAuthenticatedUserByIdentifierQueryWithResult(null); - // Setting up an authenticated user is not found setupAuthenticatedUserQueryWithNoResult(); @@ -115,13 +113,33 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_noAccount() throws P } @Test - void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsBuiltin() throws ParseException, IOException, AuthorizationException, OAuth2Exception { + @JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth-use-builtin-user-on-id-match") + void testLookupUserByOIDCBearerToken_oneProvider_validToken_userNotPresentAsBuiltin_useBuiltinUserOnIdMatchFeatureFlagEnabled() throws ParseException, IOException, AuthorizationException, OAuth2Exception { + // Given a single OIDC provider that returns a valid user identifier + setUpOIDCProviderWhichValidatesToken(); + + // Setting up authenticated builtin user is found + setupBuiltinUserOnIdMatchFeatureFlagQueriesWithResult(null); + + // Setting up an authenticated user is found + AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser()); + + // When invoking lookupUserByOIDCBearerToken + User actualUser = sut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); + + // Then the actual user should match the expected authenticated user + assertEquals(authenticatedUser, actualUser); + } + + @Test + @JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth-use-builtin-user-on-id-match") + void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsBuiltin_useBuiltinUserOnIdMatchFeatureFlagEnabled() throws ParseException, IOException, AuthorizationException, OAuth2Exception { // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); // Setting up authenticated builtin user is found AuthenticatedUser authenticatedUser = new AuthenticatedUser(); - setupAuthenticatedUserByIdentifierQueryWithResult(authenticatedUser); + setupBuiltinUserOnIdMatchFeatureFlagQueriesWithResult(authenticatedUser); // When invoking lookupUserByOIDCBearerToken User actualUser = sut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); @@ -173,10 +191,18 @@ private AuthenticatedUser setupAuthenticatedUserByAuthPrvIDQueryWithResult(Authe return authenticatedUser; } - private void setupAuthenticatedUserByIdentifierQueryWithResult(AuthenticatedUser authenticatedUser) { - TypedQuery queryStub = Mockito.mock(TypedQuery.class); - Mockito.when(queryStub.getSingleResult()).thenReturn(authenticatedUser); - Mockito.when(queryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(queryStub); - Mockito.when(sut.em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class)).thenReturn(queryStub); + private void setupBuiltinUserOnIdMatchFeatureFlagQueriesWithResult(AuthenticatedUser authenticatedUser) { + TypedQuery authenticatedUserQueryStub = Mockito.mock(TypedQuery.class); + Mockito.when(authenticatedUserQueryStub.getSingleResult()).thenReturn(authenticatedUser); + Mockito.when(authenticatedUserQueryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(authenticatedUserQueryStub); + + TypedQuery authenticatedUserLookupQueryStub = Mockito.mock(TypedQuery.class); + AuthenticatedUserLookup authenticatedUserLookupMock = Mockito.mock(AuthenticatedUserLookup.class); + Mockito.when(authenticatedUserLookupMock.getAuthenticationProviderId()).thenReturn("builtin"); + Mockito.when(authenticatedUserLookupQueryStub.getSingleResult()).thenReturn(authenticatedUserLookupMock); + Mockito.when(authenticatedUserLookupQueryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(authenticatedUserLookupQueryStub); + + Mockito.when(sut.em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class)).thenReturn(authenticatedUserQueryStub); + Mockito.when(sut.em.createNamedQuery("AuthenticatedUserLookup.findByAuthUser", AuthenticatedUserLookup.class)).thenReturn(authenticatedUserLookupQueryStub); } } From 779dfd654e4e123b666a5f66d1aac0c7f71797d2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 14 Feb 2025 19:02:12 +0000 Subject: [PATCH 30/70] Fixed: docs rst format issue --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 04d743885d5..4258dc3e9e0 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3470,7 +3470,7 @@ please find all known feature flags below. Any of these flags can be activated u * - api-bearer-auth-handle-tos-acceptance-in-idp - Specifies that Terms of Service acceptance is handled by the IdP, eliminating the need to include ToS acceptance boolean parameter (termsAccepted) in the OIDC user registration request body. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. - ``Off`` - * - api-bearer-auth-use-builtin-user-on-id-match + * - api-bearer-auth-use-builtin-user-on-id-match - Allows the use of a built-in user account when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this feature flag exposes the installation to potential user impersonation issues.** - ``Off`` * - avoid-expensive-solr-join From db83ddc724c93a98af25be78107f700ba210bbad Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 14 Feb 2025 19:19:35 +0000 Subject: [PATCH 31/70] Added: refined implementation and tests for canLoginWithGivenCredentials endpoint --- .../iq/dataverse/api/BuiltinUsers.java | 9 ++++---- src/main/java/propertyFiles/Bundle.properties | 3 +++ .../iq/dataverse/api/BuiltinUsersIT.java | 23 ++++++++++++++++--- .../edu/harvard/iq/dataverse/api/UtilIT.java | 5 ++++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java index d820766b700..d5b8297681f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java @@ -16,6 +16,8 @@ import java.sql.Timestamp; import java.util.logging.Level; import java.util.logging.Logger; + +import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; import jakarta.inject.Inject; @@ -30,7 +32,6 @@ import jakarta.ws.rs.core.Response.Status; import java.util.Date; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; -import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; /** * REST API bean for managing {@link BuiltinUser}s. @@ -130,12 +131,12 @@ public Response createWithNotification(BuiltinUser user, @PathParam("password") @GET @Path("{username}/canLoginWithGivenCredentials") - public Response canLogInAsBuiltinUser(@PathParam("username") String username, @QueryParam("password") String password) { + public Response canLoginWithGivenCredentials(@PathParam("username") String username, @QueryParam("password") String password) { AuthenticatedUser u = authenticationService.canLogInAsBuiltinUser(username, password); - if (u == null) return badRequest("Bad username or password"); + if (u == null) return badRequest(BundleUtil.getStringFromBundle("builtinUsers.canLogInAsBuiltinUser.errors.invalidCredentials")); - return ok("User can log in with the given credentials."); + return ok(BundleUtil.getStringFromBundle("builtinUsers.canLogInAsBuiltinUser.success")); } // internalSave without providing an explicit "sendEmailNotification" diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 6eb7be67eed..02333c3689a 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3157,3 +3157,6 @@ sendfeedback.fromEmail.error.invalid=Invalid fromEmail: {0} dataverseFeaturedItems.errors.notFound=Could not find dataverse featured item with identifier {0} dataverseFeaturedItems.delete.successful=Successfully deleted dataverse featured item with identifier {0} +#BuiltinUsers.java +builtinUsers.canLogInAsBuiltinUser.errors.invalidCredentials=Invalid credentials. +builtinUsers.canLogInAsBuiltinUser.success=User can log in with the given credentials. diff --git a/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java b/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java index af938cbebe1..c239ecfdfce 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java @@ -17,9 +17,8 @@ import java.util.stream.Stream; import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; -import static jakarta.ws.rs.core.Response.Status.OK; -import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; -import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; + +import static jakarta.ws.rs.core.Response.Status.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.startsWith; @@ -379,6 +378,24 @@ public void testValidatePasswordsOutOfBoxSettings() { ); } + @Test + public void testCanLoginWithGivenCredentials() { + String usernameToCreate = getRandomUsername(); + Response createUserResponse = createUser(usernameToCreate, "firstName", "lastName", null); + createUserResponse.then().assertThat().statusCode(OK.getStatusCode()); + + JsonPath createdUser = JsonPath.from(createUserResponse.body().asString()); + String createdUsernameAndPassword = createdUser.getString("data.user." + usernameKey); + + // Valid credentials + Response canLoginWithGivenCredentialsResponse = UtilIT.canLoginWithGivenCredentials(createdUsernameAndPassword, createdUsernameAndPassword); + canLoginWithGivenCredentialsResponse.then().assertThat().statusCode(OK.getStatusCode()); + + // Invalid credentials + canLoginWithGivenCredentialsResponse = UtilIT.canLoginWithGivenCredentials(createdUsernameAndPassword, "wrongPassword"); + canLoginWithGivenCredentialsResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); + } + private Response createUser(String username, String firstName, String lastName, String email) { String userAsJson = getUserAsJsonString(username, firstName, lastName, email); String password = getPassword(userAsJson); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index e3719ea5955..5bf352d1ad4 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -4562,4 +4562,9 @@ static Response deleteDataverseFeaturedItems(String dataverseAlias, String apiTo .header(API_TOKEN_HTTP_HEADER, apiToken) .delete("/api/dataverses/" + dataverseAlias + "/featuredItems"); } + + static Response canLoginWithGivenCredentials(String username, String password) { + return given() + .get("/api/builtin-users/" + username + "/canLoginWithGivenCredentials" + "?password=" + password); + } } From 321b8f238ff877231e741d56d57aa6248aafaf63 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 14 Feb 2025 19:38:47 +0000 Subject: [PATCH 32/70] Added: custom Keycloak Dockerfile for spi building and realm setup --- conf/keycloak/Dockerfile | 31 +++++++++++++++++++++++++++++++ docker-compose-dev.yml | 8 +++----- 2 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 conf/keycloak/Dockerfile diff --git a/conf/keycloak/Dockerfile b/conf/keycloak/Dockerfile new file mode 100644 index 00000000000..fbb6a654312 --- /dev/null +++ b/conf/keycloak/Dockerfile @@ -0,0 +1,31 @@ +# ------------------------------------------ +# Stage 1: Build SPI with Maven +# ------------------------------------------ +FROM maven:3.9.5-eclipse-temurin-17 AS builder + +WORKDIR /app + +# Copy SPI source code +COPY ./builtin-users-spi /app + +# Build the SPI JAR +RUN mvn clean package + +# ------------------------------------------ +# Stage 2: Build Keycloak Image +# ------------------------------------------ +FROM quay.io/keycloak/keycloak:22.0 + +# Copy SPI JAR from builder stage +COPY --from=builder /app/target/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar /opt/keycloak/providers/ + +# Copy additional configurations +COPY ./builtin-users-spi/conf/quarkus.properties /opt/keycloak/conf/ +COPY ./test-realm.json /opt/keycloak/data/import/ + +# Set the Keycloak command +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] +CMD ["start-dev", "--import-realm", "--http-port=8090"] + +# Expose port 8090 +EXPOSE 8090 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 501c76c217b..5f52ac6cc63 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -171,7 +171,9 @@ services: dev_keycloak: container_name: "dev_keycloak" - image: 'quay.io/keycloak/keycloak:22.0' + build: + context: ./conf/keycloak + dockerfile: Dockerfile hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin @@ -190,10 +192,6 @@ services: command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used ports: - "8090:8090" - volumes: - - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' - - './conf/keycloak/builtin-users-spi/conf/quarkus.properties:/opt/keycloak/conf/quarkus.properties' - - './conf/keycloak/builtin-users-spi/target/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar:/opt/keycloak/providers/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar' # This proxy configuration is only intended to be used for development purposes! # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! From 57b3a3b572d2587473f1b3ba4f4a8aef4134f474 Mon Sep 17 00:00:00 2001 From: GPortas Date: Sun, 16 Feb 2025 19:56:00 +0000 Subject: [PATCH 33/70] Added: dev_keycloak_initializer for SPI setup in realm --- conf/keycloak/setup-spi.sh | 38 ++++++++++++++++++++++++++++++++++++++ docker-compose-dev.yml | 16 +++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100755 conf/keycloak/setup-spi.sh diff --git a/conf/keycloak/setup-spi.sh b/conf/keycloak/setup-spi.sh new file mode 100755 index 00000000000..92640916f56 --- /dev/null +++ b/conf/keycloak/setup-spi.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +echo "Waiting for Keycloak to be fully up..." + +# Loop until the health check returns 200 +while true; do + HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://keycloak:8090/health") + if [ "$HTTP_RESPONSE" -eq 200 ]; then + echo "Keycloak is up! (HTTP $HTTP_RESPONSE)" + break + else + echo "Health check failed. Waiting..." + sleep 5 + fi +done + +echo "Keycloak is up and running! Executing SPI setup script..." + +# Obtain admin token +ADMIN_TOKEN=$(curl -s -X POST "http://keycloak:8090/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$KEYCLOAK_ADMIN" \ + -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" | jq -r .access_token) + +# Create user storage provider using the components endpoint +curl -X POST "http://keycloak:8090/admin/realms/test/components" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Dataverse built-in users authentication", + "providerId": "dv-builtin-users-authenticator", + "providerType": "org.keycloak.storage.UserStorageProvider", + "parentId": null + }' + +echo "Keycloak SPI configured in realm." diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 5f52ac6cc63..939c63cdee5 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -189,10 +189,24 @@ services: dataverse: aliases: - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) - command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used + command: start-dev --import-realm --health-enabled=true --http-port=8090 # change port to 8090, so within the network and external the same port is used ports: - "8090:8090" + dev_keycloak_initializer: + image: alpine:latest + container_name: "dev_keycloak_initializer" + depends_on: + - dev_keycloak + environment: + - KEYCLOAK_ADMIN=kcadmin + - KEYCLOAK_ADMIN_PASSWORD=kcpassword + volumes: + - ./conf/keycloak/setup-spi.sh:/usr/local/bin/setup-spi.sh + command: [ "/bin/sh", "-c", "apk add --no-cache curl jq && /usr/local/bin/setup-spi.sh" ] + networks: + - dataverse + # This proxy configuration is only intended to be used for development purposes! # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! dev_proxy: From 9ddb86b9992bc7f4a845bc4d195c55d1548c9ae9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 17 Feb 2025 14:19:03 +0000 Subject: [PATCH 34/70] Added: release notes for #11197 --- doc/release-notes/11197-builtin-users-oidc-auth.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 doc/release-notes/11197-builtin-users-oidc-auth.md diff --git a/doc/release-notes/11197-builtin-users-oidc-auth.md b/doc/release-notes/11197-builtin-users-oidc-auth.md new file mode 100644 index 00000000000..ce9091912ad --- /dev/null +++ b/doc/release-notes/11197-builtin-users-oidc-auth.md @@ -0,0 +1,13 @@ +### API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH feature flag + +Introduced a new feature flag, ``API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH``, which allows the use of a built-in user +account when an identity match is found during OIDC API bearer token authentication. + +This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing +the need for additional user registration steps. + +### New bultin-users API endpoint + +``/builtin-users/{username}/canLoginWithGivenCredentials`` + +Validates the provided credentials to determine if the user can log in with them. From 2dc196b61f500d3397ea0bda7b0a1ca67617f4e2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 17 Feb 2025 14:19:23 +0000 Subject: [PATCH 35/70] Added: API docs for builtin-users/canLoginWithGivenCredentials --- doc/sphinx-guides/source/api/native-api.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index e29e972852c..160821ed96a 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -4830,6 +4830,25 @@ Place this ``user-add.json`` file in your current directory and run the followin Optionally, you may use a third query parameter "sendEmailNotification=false" to explicitly disable sending an email notification to the new user. +Validate Builtin User Credentials +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Validates the provided credentials to determine if the user can log in with them. + +.. code-block:: bash + + export SERVER_URL=https://demo.dataverse.org + export USERNAME_OR_EMAIL=dataverseAdmin + export PASSWORD=admin1 + + curl -X GET "$SERVER_URL/api/builtin-users/canLoginWithGivenCredentials/$USERNAME_OR_EMAIL?password=$PASSWORD" + +The fully expanded example above (without environment variables) looks like this: + +.. code-block:: bash + + curl -X GET "https://demo.dataverse.org/api/builtin-users/canLoginWithGivenCredentials/dataverseAdmin?password=admin1" + Roles ----- From 2e5dde2eb2c699b14c3fa76431593c8fe5192339 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 17 Feb 2025 16:02:45 +0000 Subject: [PATCH 36/70] Fixed: maven docker plugin build by setting an image for dev_keycloak --- docker-compose-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 939c63cdee5..f62a0f380cc 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -174,6 +174,7 @@ services: build: context: ./conf/keycloak dockerfile: Dockerfile + image: dev_keycloak:latest hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin From 3ca4940bc6486acc24d0fcd98daca23e63ef6a4b Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 17 Feb 2025 16:37:12 +0000 Subject: [PATCH 37/70] Changed: dev keycloak image name --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index f62a0f380cc..8ff8e7ec519 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -174,7 +174,7 @@ services: build: context: ./conf/keycloak dockerfile: Dockerfile - image: dev_keycloak:latest + image: gdcc/keycloak:unstable hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin From 900f075b6dc2cc7fc7ff89c8539c22a1cd388b7a Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 4 Mar 2025 13:55:24 +0000 Subject: [PATCH 38/70] Changed: builtin-users-spi maven compiler plugin version to 17 --- conf/keycloak/builtin-users-spi/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index 13a1d4d0045..57cd5354f24 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -70,8 +70,8 @@ org.apache.maven.plugins maven-compiler-plugin - 16 - 16 + 17 + 17 From 0de1d2237ec3beb59891e8f20c3577a49d0df4ab Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 5 Mar 2025 19:32:10 +0000 Subject: [PATCH 39/70] Stash: changing builtin-users-spi to use the database instead of API for authentication (WIP) --- conf/keycloak/builtin-users-spi/pom.xml | 23 +++++ .../spi/adapters/DataverseUserAdapter.java | 21 ++--- .../auth/spi/models/DataverseBuiltinUser.java | 12 +++ .../auth/spi/models/DataverseUser.java | 20 +++++ .../DataverseUserStorageProvider.java | 77 +++------------- .../DataverseAuthenticationService.java | 36 ++++++++ .../spi/services/DataverseUserService.java | 90 +++++++++++++++++++ .../auth/spi/services/PasswordEncryption.java | 82 +++++++++++++++++ .../DataverseUserStorageProviderTest.java | 2 + .../spi/services/DataverseAPIServiceTest.java | 2 + 10 files changed, 289 insertions(+), 76 deletions(-) create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseUser.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java create mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index 57cd5354f24..51397036103 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -46,6 +46,14 @@ ${jakarta.persistence.version}
+ + + org.mindrot + jbcrypt + ${mindrot.jbcrypt.version} + compile + + @@ -66,6 +74,20 @@ + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + org.apache.maven.plugins maven-compiler-plugin @@ -81,6 +103,7 @@ 22.0.0 17 3.2.0 + 0.4 5.15.2 5.11.4
diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java index 0dec1a6bdb0..d9609fe1c1f 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/adapters/DataverseUserAdapter.java @@ -1,7 +1,6 @@ package edu.harvard.iq.keycloak.auth.spi.adapters; -import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; -import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; import org.keycloak.component.ComponentModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; @@ -13,15 +12,13 @@ public class DataverseUserAdapter extends AbstractUserAdapterFederatedStorage { - protected DataverseBuiltinUser builtinUser; - protected DataverseAuthenticatedUser authenticatedUser; + protected DataverseUser dataverseUser; protected String keycloakId; - public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseBuiltinUser builtinUser, DataverseAuthenticatedUser authenticatedUser) { + public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseUser dataverseUser) { super(session, realm, model); - this.builtinUser = builtinUser; - this.authenticatedUser = authenticatedUser; - keycloakId = StorageId.keycloakId(model, builtinUser.getId().toString()); + this.dataverseUser = dataverseUser; + keycloakId = StorageId.keycloakId(model, dataverseUser.getBuiltinUser().getId().toString()); } @Override @@ -30,22 +27,22 @@ public void setUsername(String s) { @Override public String getUsername() { - return builtinUser.getUsername(); + return dataverseUser.getBuiltinUser().getUsername(); } @Override public String getEmail() { - return authenticatedUser.getEmail(); + return dataverseUser.getAuthenticatedUser().getEmail(); } @Override public String getFirstName() { - return authenticatedUser.getFirstName(); + return dataverseUser.getAuthenticatedUser().getFirstName(); } @Override public String getLastName() { - return authenticatedUser.getLastName(); + return dataverseUser.getAuthenticatedUser().getLastName(); } @Override diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java index d2568e5ed9a..a951fe26769 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java @@ -14,6 +14,10 @@ public class DataverseBuiltinUser { private String username; + private String encryptedPassword; + + private Integer passwordEncryptionVersion; + public void setId(Integer id) { this.id = id; } @@ -29,4 +33,12 @@ public Integer getId() { public String getUsername() { return username; } + + public Integer getPasswordEncryptionVersion() { + return passwordEncryptionVersion; + } + + public String getEncryptedPassword() { + return encryptedPassword; + } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseUser.java new file mode 100644 index 00000000000..d697fe52fc8 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseUser.java @@ -0,0 +1,20 @@ +package edu.harvard.iq.keycloak.auth.spi.models; + +public class DataverseUser { + + private final DataverseAuthenticatedUser authenticatedUser; + private final DataverseBuiltinUser builtinUser; + + public DataverseUser(DataverseAuthenticatedUser authenticatedUser, DataverseBuiltinUser builtinUser) { + this.authenticatedUser = authenticatedUser; + this.builtinUser = builtinUser; + } + + public DataverseAuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + public DataverseBuiltinUser getBuiltinUser() { + return builtinUser; + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java index 7159f5ebe78..487c3d2a214 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java @@ -1,9 +1,9 @@ package edu.harvard.iq.keycloak.auth.spi.providers; import edu.harvard.iq.keycloak.auth.spi.adapters.DataverseUserAdapter; -import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; -import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; -import edu.harvard.iq.keycloak.auth.spi.services.DataverseAPIService; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; +import edu.harvard.iq.keycloak.auth.spi.services.DataverseAuthenticationService; +import edu.harvard.iq.keycloak.auth.spi.services.DataverseUserService; import jakarta.persistence.EntityManager; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; @@ -14,9 +14,6 @@ import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.user.UserLookupProvider; -import org.keycloak.storage.StorageId; - -import java.util.List; /** * DataverseUserStorageProvider integrates Keycloak with Dataverse user storage. @@ -32,62 +29,31 @@ public class DataverseUserStorageProvider implements private final ComponentModel model; private final KeycloakSession session; private final EntityManager em; + private final DataverseUserService dataverseUserService; public DataverseUserStorageProvider(KeycloakSession session, ComponentModel model) { this.session = session; this.model = model; this.em = session.getProvider(JpaConnectionProvider.class, "user-store").getEntityManager(); + this.dataverseUserService = new DataverseUserService(session); } @Override public UserModel getUserById(RealmModel realm, String id) { - logger.infof("Fetching user by ID: %s", id); - String persistenceId = StorageId.externalId(id); - - DataverseBuiltinUser builtinUser = em.find(DataverseBuiltinUser.class, persistenceId); - if (builtinUser == null) { - logger.infof("User not found for external ID: %s", persistenceId); - return null; - } - - DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(builtinUser.getUsername()); - return (authenticatedUser != null) ? new DataverseUserAdapter(session, realm, model, builtinUser, authenticatedUser) : null; + DataverseUser dataverseUser = dataverseUserService.getUserById(id); + return (dataverseUser != null) ? new DataverseUserAdapter(session, realm, model, dataverseUser) : null; } @Override public UserModel getUserByUsername(RealmModel realm, String username) { - logger.infof("Fetching user by username: %s", username); - List users = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) - .setParameter("username", username) - .getResultList(); - - if (users.isEmpty()) { - logger.infof("User not found by username: %s", username); - return null; - } - - DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); - return (authenticatedUser != null) ? new DataverseUserAdapter(session, realm, model, users.get(0), authenticatedUser) : null; + DataverseUser dataverseUser = dataverseUserService.getUserByUsername(username); + return (dataverseUser != null) ? new DataverseUserAdapter(session, realm, model, dataverseUser) : null; } @Override public UserModel getUserByEmail(RealmModel realm, String email) { - logger.infof("Fetching user by email: %s", email); - List authUsers = em.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class) - .setParameter("email", email) - .getResultList(); - - if (authUsers.isEmpty()) { - logger.infof("User not found by email: %s", email); - return null; - } - - String username = authUsers.get(0).getUserIdentifier(); - List builtinUsers = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) - .setParameter("username", username) - .getResultList(); - - return (builtinUsers.isEmpty()) ? null : new DataverseUserAdapter(session, realm, model, builtinUsers.get(0), authUsers.get(0)); + DataverseUser dataverseUser = dataverseUserService.getUserByEmail(email); + return (dataverseUser != null) ? new DataverseUserAdapter(session, realm, model, dataverseUser) : null; } @Override @@ -109,8 +75,8 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) return false; } - DataverseAPIService dataverseAPIService = new DataverseAPIService(); - return dataverseAPIService.canLogInAsBuiltinUser(user.getUsername(), userCredential.getValue()); + DataverseAuthenticationService dataverseAuthenticationService = new DataverseAuthenticationService(dataverseUserService); + return dataverseAuthenticationService.canLogInAsBuiltinUser(user.getUsername(), userCredential.getValue()); } @Override @@ -120,21 +86,4 @@ public void close() { em.close(); } } - - /** - * Retrieves an authenticated user from Dataverse by username. - * - * @param username The username to look up. - * @return The authenticated user or null if not found. - */ - private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String username) { - try { - return em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class) - .setParameter("identifier", username) - .getSingleResult(); - } catch (Exception e) { - logger.infof("Could not find authenticated user by username: %s", username); - return null; - } - } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java new file mode 100644 index 00000000000..a1abb362550 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java @@ -0,0 +1,36 @@ +package edu.harvard.iq.keycloak.auth.spi.services; + +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; + +public class DataverseAuthenticationService { + + private final DataverseUserService dataverseUserService; + + public DataverseAuthenticationService(DataverseUserService dataverseUserService) { + this.dataverseUserService = dataverseUserService; + } + + /** + * Validates if a Dataverse built-in user can log in with the given credentials. + * + * @param usernameOrEmail The username or email of the Dataverse built-in user. + * @param password The password to be validated. + * @return {@code true} if the user can log in, {@code false} otherwise. + */ + public boolean canLogInAsBuiltinUser(String usernameOrEmail, String password) { + DataverseUser dataverseUser = this.dataverseUserService.getUserByUsername(usernameOrEmail); + + if (dataverseUser == null) { + dataverseUser = this.dataverseUserService.getUserByEmail(usernameOrEmail); + } + + if (dataverseUser == null) { + return false; + } + + DataverseBuiltinUser builtinUser = dataverseUser.getBuiltinUser(); + return PasswordEncryption.getVersion(builtinUser.getPasswordEncryptionVersion()) + .check(password, builtinUser.getEncryptedPassword()); + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java new file mode 100644 index 00000000000..91263661810 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java @@ -0,0 +1,90 @@ +package edu.harvard.iq.keycloak.auth.spi.services; + +import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; +import jakarta.persistence.EntityManager; +import org.jboss.logging.Logger; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.StorageId; + +import java.util.List; + +public class DataverseUserService { + + private static final Logger logger = Logger.getLogger(DataverseUserService.class); + + private final EntityManager em; + + public DataverseUserService(KeycloakSession session) { + this.em = session.getProvider(JpaConnectionProvider.class, "user-store").getEntityManager(); + } + + public DataverseUser getUserById(String id) { + logger.infof("Fetching user by ID: %s", id); + String persistenceId = StorageId.externalId(id); + + DataverseBuiltinUser builtinUser = em.find(DataverseBuiltinUser.class, persistenceId); + if (builtinUser == null) { + logger.infof("User not found for external ID: %s", persistenceId); + return null; + } + + DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(builtinUser.getUsername()); + + return new DataverseUser(authenticatedUser, builtinUser); + } + + public DataverseUser getUserByUsername(String username) { + logger.infof("Fetching user by username: %s", username); + List users = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) + .setParameter("username", username) + .getResultList(); + + if (users.isEmpty()) { + logger.infof("User not found by username: %s", username); + return null; + } + + DataverseAuthenticatedUser authenticatedUser = getAuthenticatedUserByUsername(username); + + return new DataverseUser(authenticatedUser, users.get(0)); + } + + public DataverseUser getUserByEmail(String email) { + logger.infof("Fetching user by email: %s", email); + List authUsers = em.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class) + .setParameter("email", email) + .getResultList(); + + if (authUsers.isEmpty()) { + logger.infof("User not found by email: %s", email); + return null; + } + + String username = authUsers.get(0).getUserIdentifier(); + List builtinUsers = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) + .setParameter("username", username) + .getResultList(); + + return new DataverseUser(authUsers.get(0), builtinUsers.get(0)); + } + + /** + * Retrieves an authenticated user from Dataverse by username. + * + * @param username The username to look up. + * @return The authenticated user or null if not found. + */ + private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String username) { + try { + return em.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class) + .setParameter("identifier", username) + .getSingleResult(); + } catch (Exception e) { + logger.infof("Could not find authenticated user by username: %s", username); + return null; + } + } +} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java new file mode 100644 index 00000000000..18b8b18ad76 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java @@ -0,0 +1,82 @@ +package edu.harvard.iq.keycloak.auth.spi.services; + +import org.mindrot.jbcrypt.BCrypt; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * Password encryption, supporting multiple encryption algorithms to + * allow migrations between them. + *

+ * When adding a new password hashing algorithm, implement the {@link Algorithm} + * interface, and add an instance of the implementation as the last element + * of the {@link #algorithms} array. The rest should pretty much happen automatically + * (e.g system will detect outdated passwords for users and initiate the password reset breakout). + * + * @author Ellen Kraffmiller + * @author Michael Bar-Sinai + */ +public final class PasswordEncryption implements java.io.Serializable { + + public interface Algorithm { + boolean check(String plainText, String hashed); + } + + /** + * The SHA algorithm, now considered not secure enough. + */ + private static final Algorithm SHA = new Algorithm() { + + private String encrypt(String plainText) { + try { + MessageDigest md = MessageDigest.getInstance("SHA"); + md.update(plainText.getBytes(StandardCharsets.UTF_8)); + byte[] raw = md.digest(); + return Base64.getEncoder().encodeToString(raw); + + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean check(String plainText, String hashed) { + return hashed.equals(encrypt(plainText)); + } + }; + + /** + * BCrypt, using a complexity factor of 10 (considered safe by 2015 standards). + */ + private static final Algorithm BCRYPT_10 = new Algorithm() { + + @Override + public boolean check(String plainText, String hashed) { + try { + return BCrypt.checkpw(plainText, hashed); + } catch (IllegalArgumentException iae) { + // the password was probably not hashed using bcrypt. + return false; + } + } + }; + + private static final Algorithm[] algorithms; + + static { + algorithms = new Algorithm[]{SHA, BCRYPT_10}; + } + + /** + * Prevent people instantiating this class. + */ + private PasswordEncryption() { + } + + public static Algorithm getVersion(int i) { + return algorithms[i]; + } +} diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java index 40f13f34598..d48cb921e8e 100644 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java @@ -5,6 +5,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.keycloak.component.ComponentModel; import org.keycloak.connections.jpa.JpaConnectionProvider; @@ -17,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +@Disabled class DataverseUserStorageProviderTest { private EntityManager entityManagerMock; diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java index b05abbdfbe7..b2b4d1f5f27 100644 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java @@ -1,6 +1,7 @@ package edu.harvard.iq.keycloak.auth.spi.services; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -10,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +@Disabled class DataverseAPIServiceTest { private HttpURLConnection connectionMock; From f1c1ca263d5ed626101c782be571454cdf8589b2 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 6 Mar 2025 15:39:26 +0000 Subject: [PATCH 40/70] Added: DataverseUserServiceTest --- .../services/DataverseUserServiceTest.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java new file mode 100644 index 00000000000..251931d0e5d --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java @@ -0,0 +1,150 @@ +package edu.harvard.iq.keycloak.auth.spi.services; + +import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Disabled +class DataverseUserServiceTest { + + private EntityManager entityManagerMock; + private DataverseUserService sut; + + @BeforeEach + void setUp() { + entityManagerMock = mock(EntityManager.class); + KeycloakSession sessionMock = mock(KeycloakSession.class); + + JpaConnectionProvider jpaConnectionProviderMock = mock(JpaConnectionProvider.class); + when(sessionMock.getProvider(JpaConnectionProvider.class, "user-store")).thenReturn(jpaConnectionProviderMock); + when(jpaConnectionProviderMock.getEntityManager()).thenReturn(entityManagerMock); + + sut = new DataverseUserService(sessionMock); + } + + @Test + void getUserById_userExists() { + String testUserId = "123"; + String testUsername = "testuser"; + + DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); + builtinUser.setId(1); + builtinUser.setUsername(testUsername); + + when(entityManagerMock.find(DataverseBuiltinUser.class, "123")).thenReturn(builtinUser); + TypedQuery authUserQuery = mock(TypedQuery.class); + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class)) + .thenReturn(authUserQuery); + when(authUserQuery.setParameter("identifier", testUsername)).thenReturn(authUserQuery); + + DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); + authUser.setUserIdentifier(testUsername); + when(authUserQuery.getSingleResult()).thenReturn(authUser); + + DataverseUser user = sut.getUserById(testUserId); + assertNotNull(user); + assertEquals(testUsername, user.getBuiltinUser().getUsername()); + } + + @Test + void getUserById_userNotFound() { + when(entityManagerMock.find(DataverseBuiltinUser.class, "123")).thenReturn(null); + assertNull(sut.getUserById("123")); + } + + @Test + void getUserByUsername_userExists() { + String testUsername = "testuser"; + + DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); + builtinUser.setUsername(testUsername); + builtinUser.setId(1); + + DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); + authUser.setUserIdentifier(testUsername); + + TypedQuery builtinUserQuery = mock(TypedQuery.class); + TypedQuery authUserQuery = mock(TypedQuery.class); + + when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) + .thenReturn(builtinUserQuery); + when(builtinUserQuery.setParameter("username", testUsername)).thenReturn(builtinUserQuery); + when(builtinUserQuery.getResultList()).thenReturn(Collections.singletonList(builtinUser)); + + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class)) + .thenReturn(authUserQuery); + when(authUserQuery.setParameter("identifier", testUsername)).thenReturn(authUserQuery); + when(authUserQuery.getSingleResult()).thenReturn(authUser); + + DataverseUser user = sut.getUserByUsername(testUsername); + assertNotNull(user); + assertEquals(testUsername, user.getBuiltinUser().getUsername()); + } + + @Test + void getUserByUsername_userNotFound() { + TypedQuery query = mock(TypedQuery.class); + when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) + .thenReturn(query); + when(query.setParameter("username", "unknown")).thenReturn(query); + when(query.getResultList()).thenReturn(Collections.emptyList()); + + assertNull(sut.getUserByUsername("unknown")); + } + + @Test + void getUserByEmail_userExists() { + String testEmail = "test@dataverse.org"; + String testUsername = "testuser"; + + DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); + authUser.setEmail(testEmail); + authUser.setId(1); + authUser.setUserIdentifier(testUsername); + + DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); + builtinUser.setUsername(testUsername); + builtinUser.setId(1); + + TypedQuery authUserQuery = mock(TypedQuery.class); + TypedQuery builtinUserQuery = mock(TypedQuery.class); + + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class)) + .thenReturn(authUserQuery); + when(authUserQuery.setParameter("email", testEmail)).thenReturn(authUserQuery); + when(authUserQuery.getResultList()).thenReturn(Collections.singletonList(authUser)); + + when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) + .thenReturn(builtinUserQuery); + when(builtinUserQuery.setParameter("username", testUsername)).thenReturn(builtinUserQuery); + when(builtinUserQuery.getResultList()).thenReturn(Collections.singletonList(builtinUser)); + + DataverseUser user = sut.getUserByEmail(testEmail); + assertNotNull(user); + assertEquals(testUsername, user.getBuiltinUser().getUsername()); + } + + @Test + void getUserByEmail_userNotFound() { + TypedQuery query = mock(TypedQuery.class); + when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class)) + .thenReturn(query); + when(query.setParameter("email", "unknown@dataverse.org")).thenReturn(query); + when(query.getResultList()).thenReturn(Collections.emptyList()); + + assertNull(sut.getUserByEmail("unknown@dataverse.org")); + } +} From 24e0d391385ffb5751220d1c07da6332b82d5e63 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 6 Mar 2025 16:53:44 +0000 Subject: [PATCH 41/70] Added: DataverseAuthenticationServiceTest --- .../auth/spi/models/DataverseBuiltinUser.java | 4 ++ .../DataverseAuthenticationService.java | 16 ++++- .../spi/services/DataverseAPIServiceTest.java | 50 -------------- .../DataverseAuthenticationServiceTest.java | 66 +++++++++++++++++++ 4 files changed, 84 insertions(+), 52 deletions(-) delete mode 100644 conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java create mode 100644 conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java index a951fe26769..b4dd59339d2 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/models/DataverseBuiltinUser.java @@ -38,6 +38,10 @@ public Integer getPasswordEncryptionVersion() { return passwordEncryptionVersion; } + public void setEncryptedPassword(String encryptedPassword) { + this.encryptedPassword = encryptedPassword; + } + public String getEncryptedPassword() { return encryptedPassword; } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java index a1abb362550..995662e1cb6 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationService.java @@ -7,8 +7,16 @@ public class DataverseAuthenticationService { private final DataverseUserService dataverseUserService; + private PasswordEncryption.Algorithm passwordEncryptionAlgorithm; + public DataverseAuthenticationService(DataverseUserService dataverseUserService) { + this(dataverseUserService, null); + } + + // Just for testing purposes, do not use + public DataverseAuthenticationService(DataverseUserService dataverseUserService, PasswordEncryption.Algorithm passwordEncryptionAlgorithm) { this.dataverseUserService = dataverseUserService; + this.passwordEncryptionAlgorithm = passwordEncryptionAlgorithm; } /** @@ -30,7 +38,11 @@ public boolean canLogInAsBuiltinUser(String usernameOrEmail, String password) { } DataverseBuiltinUser builtinUser = dataverseUser.getBuiltinUser(); - return PasswordEncryption.getVersion(builtinUser.getPasswordEncryptionVersion()) - .check(password, builtinUser.getEncryptedPassword()); + + if (passwordEncryptionAlgorithm == null) { + passwordEncryptionAlgorithm = PasswordEncryption.getVersion(builtinUser.getPasswordEncryptionVersion()); + } + + return passwordEncryptionAlgorithm.check(password, builtinUser.getEncryptedPassword()); } } diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java deleted file mode 100644 index b2b4d1f5f27..00000000000 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIServiceTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package edu.harvard.iq.keycloak.auth.spi.services; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@Disabled -class DataverseAPIServiceTest { - - private HttpURLConnection connectionMock; - private URL urlMock; - private DataverseAPIService sut; - - @BeforeEach - void setUp() throws Exception { - connectionMock = mock(HttpURLConnection.class); - urlMock = mock(URL.class); - when(urlMock.openConnection()).thenReturn(connectionMock); - - sut = new DataverseAPIService(urlMock); - } - - @Test - void canLogInAsBuiltinUser_validCredentials() throws Exception { - when(connectionMock.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - boolean result = sut.canLogInAsBuiltinUser("validUser", "validPass"); - assertTrue(result); - } - - @Test - void canLogInAsBuiltinUser_invalidCredentials() throws Exception { - when(connectionMock.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); - boolean result = sut.canLogInAsBuiltinUser("invalidUser", "invalidPass"); - assertFalse(result); - } - - @Test - void canLogInAsBuiltinUser_apiError() throws Exception { - when(urlMock.openConnection()).thenThrow(new IOException("Connection error")); - boolean result = sut.canLogInAsBuiltinUser("errorUser", "errorPass"); - assertFalse(result); - } -} diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java new file mode 100644 index 00000000000..142c39b6224 --- /dev/null +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java @@ -0,0 +1,66 @@ +package edu.harvard.iq.keycloak.auth.spi.services; + +import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; +import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@Disabled +class DataverseAuthenticationServiceTest { + + private DataverseUserService dataverseUserServiceMock; + private PasswordEncryption.Algorithm passwordEncryptionAlgorithmMock; + private DataverseAuthenticationService sut; + + @BeforeEach + void setUp() { + dataverseUserServiceMock = mock(DataverseUserService.class); + passwordEncryptionAlgorithmMock = mock(PasswordEncryption.Algorithm.class); + sut = new DataverseAuthenticationService(dataverseUserServiceMock, passwordEncryptionAlgorithmMock); + } + + private void setupUserMock(String identifier, boolean foundByUsername, boolean validPassword) { + String encryptedPassword = "encryptedPassword"; + DataverseUser dataverseUserMock = mock(DataverseUser.class); + DataverseBuiltinUser dataverseBuiltinUser = new DataverseBuiltinUser(); + dataverseBuiltinUser.setEncryptedPassword(encryptedPassword); + + when(dataverseUserMock.getBuiltinUser()).thenReturn(dataverseBuiltinUser); + when(passwordEncryptionAlgorithmMock.check(anyString(), eq(encryptedPassword))).thenReturn(validPassword); + + if (foundByUsername) { + when(dataverseUserServiceMock.getUserByUsername(identifier)).thenReturn(dataverseUserMock); + } else { + when(dataverseUserServiceMock.getUserByUsername(identifier)).thenReturn(null); + when(dataverseUserServiceMock.getUserByEmail(identifier)).thenReturn(dataverseUserMock); + } + } + + @Test + void canLogInAsBuiltinUser_userFoundByUsername_validCredentials() { + setupUserMock("username", true, true); + assertTrue(sut.canLogInAsBuiltinUser("username", "password")); + } + + @Test + void canLogInAsBuiltinUser_userFoundByUsername_invalidCredentials() { + setupUserMock("username", true, false); + assertFalse(sut.canLogInAsBuiltinUser("username", "password")); + } + + @Test + void canLogInAsBuiltinUser_userFoundByEmail_validCredentials() { + setupUserMock("user@dataverse.org", false, true); + assertTrue(sut.canLogInAsBuiltinUser("user@dataverse.org", "password")); + } + + @Test + void canLogInAsBuiltinUser_userFoundByEmail_invalidCredentials() { + setupUserMock("user@dataverse.org", false, false); + assertFalse(sut.canLogInAsBuiltinUser("user@dataverse.org", "password")); + } +} From 1423cf364783cbaeb3e7ad964125b663facb9be4 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 6 Mar 2025 17:03:15 +0000 Subject: [PATCH 42/70] Added: spi code cleanup and reactivated tests --- .../DataverseUserStorageProvider.java | 8 +- .../spi/services/DataverseAPIService.java | 65 -------- .../spi/services/DataverseUserService.java | 6 + .../DataverseUserStorageProviderTest.java | 153 ------------------ .../DataverseAuthenticationServiceTest.java | 36 ++--- .../services/DataverseUserServiceTest.java | 2 - 6 files changed, 24 insertions(+), 246 deletions(-) delete mode 100644 conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java delete mode 100644 conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java index 487c3d2a214..a0ff84d4c17 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java @@ -4,10 +4,8 @@ import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; import edu.harvard.iq.keycloak.auth.spi.services.DataverseAuthenticationService; import edu.harvard.iq.keycloak.auth.spi.services.DataverseUserService; -import jakarta.persistence.EntityManager; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; -import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.models.*; @@ -28,13 +26,11 @@ public class DataverseUserStorageProvider implements private final ComponentModel model; private final KeycloakSession session; - private final EntityManager em; private final DataverseUserService dataverseUserService; public DataverseUserStorageProvider(KeycloakSession session, ComponentModel model) { this.session = session; this.model = model; - this.em = session.getProvider(JpaConnectionProvider.class, "user-store").getEntityManager(); this.dataverseUserService = new DataverseUserService(session); } @@ -82,8 +78,6 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) @Override public void close() { logger.info("Closing DataverseUserStorageProvider"); - if (em != null) { - em.close(); - } + this.dataverseUserService.close(); } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java deleted file mode 100644 index a8969e4efde..00000000000 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAPIService.java +++ /dev/null @@ -1,65 +0,0 @@ -package edu.harvard.iq.keycloak.auth.spi.services; - -import org.jboss.logging.Logger; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -/** - * Provides API interaction methods for Dataverse authentication. - */ -public class DataverseAPIService { - - private static final Logger logger = Logger.getLogger(DataverseAPIService.class); - - private static final String DATAVERSE_BASE_URL = System.getenv("DATAVERSE_BASE_URL"); - private static final String DATAVERSE_API_URL = String.format("%s/api/builtin-users/%%s/canLoginWithGivenCredentials?password=%%s", DATAVERSE_BASE_URL); - - private URL requestUrl; - - public DataverseAPIService() { - } - - // Constructor for testing purposes - public DataverseAPIService(URL requestUrl) { - this.requestUrl = requestUrl; - } - - /** - * Validates if a Dataverse built-in user can log in with the given credentials. - * - * @param username The username of the Dataverse built-in user. - * @param password The password to be validated. - * @return {@code true} if the user can log in, {@code false} otherwise. - */ - public boolean canLogInAsBuiltinUser(String username, String password) { - HttpURLConnection connection = null; - - try { - if (requestUrl == null) { - String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); - String encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8); - String requestUrlString = String.format(DATAVERSE_API_URL, encodedUsername, encodedPassword); - requestUrl = new URL(requestUrlString); - } - connection = (HttpURLConnection) requestUrl.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Accept", "application/json"); - - int responseCode = connection.getResponseCode(); - logger.infof("Dataverse API response code for user '%s': %d", username, responseCode); - - return responseCode == HttpURLConnection.HTTP_OK; - } catch (IOException e) { - logger.errorf(e, "Error occurred while validating login for user '%s'", username); - return false; - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } -} diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java index 91263661810..6b03e181d81 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java @@ -71,6 +71,12 @@ public DataverseUser getUserByEmail(String email) { return new DataverseUser(authUsers.get(0), builtinUsers.get(0)); } + public void close() { + if (em != null) { + em.close(); + } + } + /** * Retrieves an authenticated user from Dataverse by username. * diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java deleted file mode 100644 index d48cb921e8e..00000000000 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderTest.java +++ /dev/null @@ -1,153 +0,0 @@ -package edu.harvard.iq.keycloak.auth.spi.providers; - -import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser; -import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.keycloak.component.ComponentModel; -import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; - -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@Disabled -class DataverseUserStorageProviderTest { - - private EntityManager entityManagerMock; - private RealmModel realmStub; - private DataverseUserStorageProvider sut; - - @BeforeEach - void setUp() { - entityManagerMock = mock(EntityManager.class); - realmStub = mock(RealmModel.class); - KeycloakSession sessionMock = mock(KeycloakSession.class); - - JpaConnectionProvider jpaConnectionProviderMock = mock(JpaConnectionProvider.class); - when(sessionMock.getProvider(JpaConnectionProvider.class, "user-store")).thenReturn(jpaConnectionProviderMock); - when(jpaConnectionProviderMock.getEntityManager()).thenReturn(entityManagerMock); - - sut = new DataverseUserStorageProvider(sessionMock, mock(ComponentModel.class)); - } - - @Test - void getUserById_userExists() { - String testUserId = "123"; - String testUsername = "testuser"; - - DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); - builtinUser.setId(1); - builtinUser.setUsername(testUsername); - - when(entityManagerMock.find(DataverseBuiltinUser.class, "123")).thenReturn(builtinUser); - TypedQuery authUserQuery = mock(TypedQuery.class); - when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class)) - .thenReturn(authUserQuery); - when(authUserQuery.setParameter("identifier", testUsername)).thenReturn(authUserQuery); - - DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); - authUser.setUserIdentifier(testUsername); - when(authUserQuery.getSingleResult()).thenReturn(authUser); - - UserModel user = sut.getUserById(realmStub, testUserId); - assertNotNull(user); - assertEquals(testUsername, user.getUsername()); - } - - @Test - void getUserById_userNotFound() { - when(entityManagerMock.find(DataverseBuiltinUser.class, "123")).thenReturn(null); - assertNull(sut.getUserById(realmStub, "123")); - } - - @Test - void getUserByUsername_userExists() { - String testUsername = "testuser"; - - DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); - builtinUser.setUsername(testUsername); - builtinUser.setId(1); - - DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); - authUser.setUserIdentifier(testUsername); - - TypedQuery builtinUserQuery = mock(TypedQuery.class); - TypedQuery authUserQuery = mock(TypedQuery.class); - - when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) - .thenReturn(builtinUserQuery); - when(builtinUserQuery.setParameter("username", testUsername)).thenReturn(builtinUserQuery); - when(builtinUserQuery.getResultList()).thenReturn(Collections.singletonList(builtinUser)); - - when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByIdentifier", DataverseAuthenticatedUser.class)) - .thenReturn(authUserQuery); - when(authUserQuery.setParameter("identifier", testUsername)).thenReturn(authUserQuery); - when(authUserQuery.getSingleResult()).thenReturn(authUser); - - UserModel user = sut.getUserByUsername(realmStub, testUsername); - assertNotNull(user); - assertEquals(testUsername, user.getUsername()); - } - - @Test - void getUserByUsername_userNotFound() { - TypedQuery query = mock(TypedQuery.class); - when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) - .thenReturn(query); - when(query.setParameter("username", "unknown")).thenReturn(query); - when(query.getResultList()).thenReturn(Collections.emptyList()); - - assertNull(sut.getUserByUsername(realmStub, "unknown")); - } - - @Test - void getUserByEmail_userExists() { - String testEmail = "test@dataverse.org"; - String testUsername = "testuser"; - - DataverseAuthenticatedUser authUser = new DataverseAuthenticatedUser(); - authUser.setEmail(testEmail); - authUser.setId(1); - authUser.setUserIdentifier(testUsername); - - DataverseBuiltinUser builtinUser = new DataverseBuiltinUser(); - builtinUser.setUsername(testUsername); - builtinUser.setId(1); - - TypedQuery authUserQuery = mock(TypedQuery.class); - TypedQuery builtinUserQuery = mock(TypedQuery.class); - - when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class)) - .thenReturn(authUserQuery); - when(authUserQuery.setParameter("email", testEmail)).thenReturn(authUserQuery); - when(authUserQuery.getResultList()).thenReturn(Collections.singletonList(authUser)); - - when(entityManagerMock.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class)) - .thenReturn(builtinUserQuery); - when(builtinUserQuery.setParameter("username", testUsername)).thenReturn(builtinUserQuery); - when(builtinUserQuery.getResultList()).thenReturn(Collections.singletonList(builtinUser)); - - UserModel user = sut.getUserByEmail(realmStub, testEmail); - assertNotNull(user); - assertEquals(testUsername, user.getUsername()); - } - - @Test - void getUserByEmail_userNotFound() { - TypedQuery query = mock(TypedQuery.class); - when(entityManagerMock.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class)) - .thenReturn(query); - when(query.setParameter("email", "unknown@dataverse.org")).thenReturn(query); - when(query.getResultList()).thenReturn(Collections.emptyList()); - - assertNull(sut.getUserByEmail(realmStub, "unknown@dataverse.org")); - } -} diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java index 142c39b6224..35f37973b71 100644 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseAuthenticationServiceTest.java @@ -3,13 +3,11 @@ import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser; import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -@Disabled class DataverseAuthenticationServiceTest { private DataverseUserService dataverseUserServiceMock; @@ -23,23 +21,6 @@ void setUp() { sut = new DataverseAuthenticationService(dataverseUserServiceMock, passwordEncryptionAlgorithmMock); } - private void setupUserMock(String identifier, boolean foundByUsername, boolean validPassword) { - String encryptedPassword = "encryptedPassword"; - DataverseUser dataverseUserMock = mock(DataverseUser.class); - DataverseBuiltinUser dataverseBuiltinUser = new DataverseBuiltinUser(); - dataverseBuiltinUser.setEncryptedPassword(encryptedPassword); - - when(dataverseUserMock.getBuiltinUser()).thenReturn(dataverseBuiltinUser); - when(passwordEncryptionAlgorithmMock.check(anyString(), eq(encryptedPassword))).thenReturn(validPassword); - - if (foundByUsername) { - when(dataverseUserServiceMock.getUserByUsername(identifier)).thenReturn(dataverseUserMock); - } else { - when(dataverseUserServiceMock.getUserByUsername(identifier)).thenReturn(null); - when(dataverseUserServiceMock.getUserByEmail(identifier)).thenReturn(dataverseUserMock); - } - } - @Test void canLogInAsBuiltinUser_userFoundByUsername_validCredentials() { setupUserMock("username", true, true); @@ -63,4 +44,21 @@ void canLogInAsBuiltinUser_userFoundByEmail_invalidCredentials() { setupUserMock("user@dataverse.org", false, false); assertFalse(sut.canLogInAsBuiltinUser("user@dataverse.org", "password")); } + + private void setupUserMock(String identifier, boolean foundByUsername, boolean validPassword) { + String encryptedPassword = "encryptedPassword"; + DataverseUser dataverseUserMock = mock(DataverseUser.class); + DataverseBuiltinUser dataverseBuiltinUser = new DataverseBuiltinUser(); + dataverseBuiltinUser.setEncryptedPassword(encryptedPassword); + + when(dataverseUserMock.getBuiltinUser()).thenReturn(dataverseBuiltinUser); + when(passwordEncryptionAlgorithmMock.check(anyString(), eq(encryptedPassword))).thenReturn(validPassword); + + if (foundByUsername) { + when(dataverseUserServiceMock.getUserByUsername(identifier)).thenReturn(dataverseUserMock); + } else { + when(dataverseUserServiceMock.getUserByUsername(identifier)).thenReturn(null); + when(dataverseUserServiceMock.getUserByEmail(identifier)).thenReturn(dataverseUserMock); + } + } } diff --git a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java index 251931d0e5d..bae96bfc9fc 100644 --- a/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java +++ b/conf/keycloak/builtin-users-spi/src/test/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserServiceTest.java @@ -6,7 +6,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.KeycloakSession; @@ -17,7 +16,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@Disabled class DataverseUserServiceTest { private EntityManager entityManagerMock; From e87eb8065873b42b44612d870fdfbf2e7c2946dd Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 10:13:17 +0000 Subject: [PATCH 43/70] Changed: log levels from info to debug --- .../providers/DataverseUserStorageProvider.java | 6 +++--- .../DataverseUserStorageProviderFactory.java | 2 +- .../auth/spi/services/DataverseUserService.java | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java index a0ff84d4c17..20e6eeaefa1 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProvider.java @@ -59,13 +59,13 @@ public boolean supportsCredentialType(String credentialType) { @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { - logger.infof("Checking credential configuration for user: %s, credentialType: %s", user.getUsername(), credentialType); + logger.debugf("Checking credential configuration for user: %s, credentialType: %s", user.getUsername(), credentialType); return false; } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { - logger.infof("Validating credentials for user: %s", user.getUsername()); + logger.debugf("Validating credentials for user: %s", user.getUsername()); if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel userCredential)) { return false; @@ -77,7 +77,7 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) @Override public void close() { - logger.info("Closing DataverseUserStorageProvider"); + logger.debug("Closing DataverseUserStorageProvider"); this.dataverseUserService.close(); } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java index 91f78f8efa0..688d530dd02 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/providers/DataverseUserStorageProviderFactory.java @@ -28,6 +28,6 @@ public String getHelpText() { @Override public void close() { - logger.info("<<<<<< Closing factory"); + logger.debug("<<<<<< Closing factory"); } } diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java index 6b03e181d81..d7cae489c20 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/DataverseUserService.java @@ -22,12 +22,12 @@ public DataverseUserService(KeycloakSession session) { } public DataverseUser getUserById(String id) { - logger.infof("Fetching user by ID: %s", id); + logger.debugf("Fetching user by ID: %s", id); String persistenceId = StorageId.externalId(id); DataverseBuiltinUser builtinUser = em.find(DataverseBuiltinUser.class, persistenceId); if (builtinUser == null) { - logger.infof("User not found for external ID: %s", persistenceId); + logger.debugf("User not found for external ID: %s", persistenceId); return null; } @@ -37,13 +37,13 @@ public DataverseUser getUserById(String id) { } public DataverseUser getUserByUsername(String username) { - logger.infof("Fetching user by username: %s", username); + logger.debugf("Fetching user by username: %s", username); List users = em.createNamedQuery("DataverseBuiltinUser.findByUsername", DataverseBuiltinUser.class) .setParameter("username", username) .getResultList(); if (users.isEmpty()) { - logger.infof("User not found by username: %s", username); + logger.debugf("User not found by username: %s", username); return null; } @@ -53,13 +53,13 @@ public DataverseUser getUserByUsername(String username) { } public DataverseUser getUserByEmail(String email) { - logger.infof("Fetching user by email: %s", email); + logger.debugf("Fetching user by email: %s", email); List authUsers = em.createNamedQuery("DataverseAuthenticatedUser.findByEmail", DataverseAuthenticatedUser.class) .setParameter("email", email) .getResultList(); if (authUsers.isEmpty()) { - logger.infof("User not found by email: %s", email); + logger.debugf("User not found by email: %s", email); return null; } @@ -89,7 +89,7 @@ private DataverseAuthenticatedUser getAuthenticatedUserByUsername(String usernam .setParameter("identifier", username) .getSingleResult(); } catch (Exception e) { - logger.infof("Could not find authenticated user by username: %s", username); + logger.debugf("Could not find authenticated user by username: %s", username); return null; } } From 93bf7eb3016f22836d0d57bd6493735861d0c1ee Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 10:26:41 +0000 Subject: [PATCH 44/70] Removed: canLoginWithGivenCredentials API endpoint --- doc/sphinx-guides/source/api/native-api.rst | 19 -------------- .../iq/dataverse/api/BuiltinUsers.java | 11 -------- src/main/java/propertyFiles/Bundle.properties | 4 --- .../iq/dataverse/api/BuiltinUsersIT.java | 25 ------------------- .../edu/harvard/iq/dataverse/api/UtilIT.java | 8 ------ 5 files changed, 67 deletions(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index dcd23e6af6b..7927d063dd6 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -4912,25 +4912,6 @@ Place this ``user-add.json`` file in your current directory and run the followin Optionally, you may use a third query parameter "sendEmailNotification=false" to explicitly disable sending an email notification to the new user. -Validate Builtin User Credentials -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Validates the provided credentials to determine if the user can log in with them. - -.. code-block:: bash - - export SERVER_URL=https://demo.dataverse.org - export USERNAME_OR_EMAIL=dataverseAdmin - export PASSWORD=admin1 - - curl -X GET "$SERVER_URL/api/builtin-users/canLoginWithGivenCredentials/$USERNAME_OR_EMAIL?password=$PASSWORD" - -The fully expanded example above (without environment variables) looks like this: - -.. code-block:: bash - - curl -X GET "https://demo.dataverse.org/api/builtin-users/canLoginWithGivenCredentials/dataverseAdmin?password=admin1" - Roles ----- diff --git a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java index d5b8297681f..e6991072d76 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/BuiltinUsers.java @@ -17,7 +17,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -import edu.harvard.iq.dataverse.util.BundleUtil; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; import jakarta.inject.Inject; @@ -128,16 +127,6 @@ public Response create(BuiltinUser user, @PathParam("password") String password, public Response createWithNotification(BuiltinUser user, @PathParam("password") String password, @PathParam("key") String key, @PathParam("sendEmailNotification") Boolean sendEmailNotification) { return internalSave(user, password, key, sendEmailNotification); } - - @GET - @Path("{username}/canLoginWithGivenCredentials") - public Response canLoginWithGivenCredentials(@PathParam("username") String username, @QueryParam("password") String password) { - AuthenticatedUser u = authenticationService.canLogInAsBuiltinUser(username, password); - - if (u == null) return badRequest(BundleUtil.getStringFromBundle("builtinUsers.canLogInAsBuiltinUser.errors.invalidCredentials")); - - return ok(BundleUtil.getStringFromBundle("builtinUsers.canLogInAsBuiltinUser.success")); - } // internalSave without providing an explicit "sendEmailNotification" private Response internalSave(BuiltinUser user, String password, String key) { diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 25b27d71469..928c1b07b7f 100644 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -3156,7 +3156,3 @@ sendfeedback.fromEmail.error.invalid=Invalid fromEmail: {0} #DataverseFeaturedItems.java dataverseFeaturedItems.errors.notFound=Could not find dataverse featured item with identifier {0} dataverseFeaturedItems.delete.successful=Successfully deleted dataverse featured item with identifier {0} - -#BuiltinUsers.java -builtinUsers.canLogInAsBuiltinUser.errors.invalidCredentials=Invalid credentials. -builtinUsers.canLogInAsBuiltinUser.success=User can log in with the given credentials. diff --git a/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java b/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java index c239ecfdfce..3fa15657483 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/BuiltinUsersIT.java @@ -378,24 +378,6 @@ public void testValidatePasswordsOutOfBoxSettings() { ); } - @Test - public void testCanLoginWithGivenCredentials() { - String usernameToCreate = getRandomUsername(); - Response createUserResponse = createUser(usernameToCreate, "firstName", "lastName", null); - createUserResponse.then().assertThat().statusCode(OK.getStatusCode()); - - JsonPath createdUser = JsonPath.from(createUserResponse.body().asString()); - String createdUsernameAndPassword = createdUser.getString("data.user." + usernameKey); - - // Valid credentials - Response canLoginWithGivenCredentialsResponse = UtilIT.canLoginWithGivenCredentials(createdUsernameAndPassword, createdUsernameAndPassword); - canLoginWithGivenCredentialsResponse.then().assertThat().statusCode(OK.getStatusCode()); - - // Invalid credentials - canLoginWithGivenCredentialsResponse = UtilIT.canLoginWithGivenCredentials(createdUsernameAndPassword, "wrongPassword"); - canLoginWithGivenCredentialsResponse.then().assertThat().statusCode(BAD_REQUEST.getStatusCode()); - } - private Response createUser(String username, String firstName, String lastName, String email) { String userAsJson = getUserAsJsonString(username, firstName, lastName, email); String password = getPassword(userAsJson); @@ -406,13 +388,6 @@ private Response createUser(String username, String firstName, String lastName, return response; } - private Response getApiTokenUsingEmail(String email, String password) { - Response response = given() - .contentType(ContentType.JSON) - .get("/api/builtin-users/" + email + "/api-token?username=" + email + "&password=" + password); - return response; - } - private Response getUserFromDatabase(String username) { Response getUserResponse = given() .get("/api/admin/authenticatedUsers/" + username + "/"); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index b99542573e0..c18056b2044 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1,7 +1,5 @@ package edu.harvard.iq.dataverse.api; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.harvard.iq.dataverse.api.dto.NewDataverseFeaturedItemDTO; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; @@ -53,7 +51,6 @@ import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.StringUtil; -import static org.apache.http.entity.ContentType.APPLICATION_JSON; import static org.junit.jupiter.api.Assertions.*; public class UtilIT { @@ -4604,9 +4601,4 @@ public static Response updateDataverseInputLevelDisplayOnCreate(String dataverse .contentType(ContentType.JSON) .put("/api/dataverses/" + dataverseAlias + "/inputLevels"); } - - static Response canLoginWithGivenCredentials(String username, String password) { - return given() - .get("/api/builtin-users/" + username + "/canLoginWithGivenCredentials" + "?password=" + password); - } } From 6b8526a52f35c2f97c735efe34400a155525e3dc Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 10:29:35 +0000 Subject: [PATCH 45/70] Refactor: getAuthenticatedUserWithProvider --- .../authorization/AuthenticationServiceBean.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 4f34b7083c0..2961e5b554b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -246,11 +246,11 @@ public AuthenticatedUser getAuthenticatedUserWithProvider( String identifier ) { .setParameter("identifier", identifier) .getSingleResult(); - AuthenticatedUserLookup aul = em.createNamedQuery("AuthenticatedUserLookup.findByAuthUser", AuthenticatedUserLookup.class) - .setParameter("authUser", authenticatedUser) - .getSingleResult(); - if (authenticatedUser != null) { + AuthenticatedUserLookup aul = em.createNamedQuery("AuthenticatedUserLookup.findByAuthUser", AuthenticatedUserLookup.class) + .setParameter("authUser", authenticatedUser) + .getSingleResult(); + authenticatedUser.setAuthProviderId(aul.getAuthenticationProviderId()); } From 2070589784ef0c23c1815c71b5aee823d54e507e Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 10:43:36 +0000 Subject: [PATCH 46/70] Refactor: lookupUserByOIDCBearerToken --- .../authorization/AuthenticationServiceBean.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 2961e5b554b..737dd25cacb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -995,7 +995,7 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws // Tokens in the cache should be removed after some (configurable) time. OAuth2UserRecord oAuth2UserRecord = verifyOIDCBearerTokenAndGetOAuth2UserRecord(bearerToken); if (FeatureFlags.API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH.enabled()) { - AuthenticatedUser builtinAuthenticatedUser = getBuiltinAuthenticatedUserByIdentifier(oAuth2UserRecord.getUsername()); + AuthenticatedUser builtinAuthenticatedUser = lookupUser(BuiltinAuthenticationProvider.PROVIDER_ID, oAuth2UserRecord.getUserRecordIdentifier().getUserIdInRepo()); return (builtinAuthenticatedUser != null) ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); } return lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); @@ -1052,12 +1052,4 @@ private List getAvailableOidcProviders() { .map(providerId -> (OIDCAuthProvider) getAuthenticationProvider(providerId)) .toList(); } - - private AuthenticatedUser getBuiltinAuthenticatedUserByIdentifier(String identifier) { - AuthenticatedUser builtinAuthenticatedUser = getAuthenticatedUserWithProvider(identifier); - if (builtinAuthenticatedUser != null && builtinAuthenticatedUser.getAuthProviderId().equals("builtin")) { - return builtinAuthenticatedUser; - } - return null; - } } From 0269cc7a93fd6ad4e2eff3edc7739938212ecc60 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 10:53:04 +0000 Subject: [PATCH 47/70] Changed: updated #11197 release notes --- doc/release-notes/11197-builtin-users-oidc-auth.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/11197-builtin-users-oidc-auth.md b/doc/release-notes/11197-builtin-users-oidc-auth.md index ce9091912ad..758c2c5f696 100644 --- a/doc/release-notes/11197-builtin-users-oidc-auth.md +++ b/doc/release-notes/11197-builtin-users-oidc-auth.md @@ -6,8 +6,14 @@ account when an identity match is found during OIDC API bearer token authenticat This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing the need for additional user registration steps. -### New bultin-users API endpoint +### Keycloak SPI for Built-In users -``/builtin-users/{username}/canLoginWithGivenCredentials`` +A Keycloak SPI, ``builtin-users-spi``, has been implemented that allows the use of Keycloak on instances with built-in +accounts for OIDC +authentication, enabling the use of the SPA on those instances. -Validates the provided credentials to determine if the user can log in with them. +Looking ahead, this authenticator SPI could also support mapping Shibboleth users coming in through Keycloak to existing +Shib users without changing the provider in the Dataverse database. However, this would require changes to the storage +provider to support more than just built-in users. + +The SPI code is available in the Dataverse code repository. From 7ba6edd26a0ec1d8c6ef26e96e3d8d8e37744bf9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 10:58:04 +0000 Subject: [PATCH 48/70] Changed: updated api-bearer-auth-use-builtin-user-on-id-match docs --- doc/sphinx-guides/source/installation/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 6d722ca6343..b9a6ddf5842 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3476,7 +3476,7 @@ please find all known feature flags below. Any of these flags can be activated u - Specifies that Terms of Service acceptance is handled by the IdP, eliminating the need to include ToS acceptance boolean parameter (termsAccepted) in the OIDC user registration request body. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. - ``Off`` * - api-bearer-auth-use-builtin-user-on-id-match - - Allows the use of a built-in user account when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this feature flag exposes the installation to potential user impersonation issues.** + - Allows the use of a built-in user account when an identity match is found during API bearer authentication. This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing the need for additional user registration steps. This feature only works when the feature flag ``api-bearer-auth`` is also enabled. **Caution: Enabling this feature flag exposes the installation to potential user impersonation issues depending on the specifics of the IdP configured (For example, if it is configured such that an attacker can create a new account in the IdP, or configured social login account, matching a Dataverse built-in account).** - ``Off`` * - avoid-expensive-solr-join - Changes the way Solr queries are constructed for public content (published Collections, Datasets and Files). It removes a very expensive Solr join on all such documents, improving overall performance, especially for large instances under heavy load. Before this feature flag is enabled, the corresponding indexing feature (see next feature flag) must be turned on and a full reindex performed (otherwise public objects are not going to be shown in search results). See :doc:`/admin/solr-search-index`. From 49c887d736843e81b1798a8acd41a41b99c59fd9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 7 Mar 2025 19:59:29 +0000 Subject: [PATCH 49/70] Fixed: AuthenticationServiceBeanTest --- .../AuthenticationServiceBeanTest.java | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index b01276e82ba..7451011bf5d 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -4,6 +4,7 @@ import com.nimbusds.oauth2.sdk.token.BearerAccessToken; import com.nimbusds.openid.connect.sdk.claims.UserInfo; import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationException; +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; @@ -18,6 +19,7 @@ import jakarta.persistence.TypedQuery; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.io.IOException; @@ -114,21 +116,34 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_noAccount() throws P @Test @JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth-use-builtin-user-on-id-match") - void testLookupUserByOIDCBearerToken_oneProvider_validToken_userNotPresentAsBuiltin_useBuiltinUserOnIdMatchFeatureFlagEnabled() throws ParseException, IOException, AuthorizationException, OAuth2Exception { + void testLookupUserByOIDCBearerToken_oneProvider_validToken_userNotPresentAsBuiltin_useBuiltinUserOnIdMatchFeatureFlagEnabled() + throws ParseException, IOException, AuthorizationException, OAuth2Exception { + // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); - // Setting up authenticated builtin user is found - setupBuiltinUserOnIdMatchFeatureFlagQueriesWithResult(null); + // Spy on the SUT to verify method calls + AuthenticationServiceBean spySut = Mockito.spy(sut); - // Setting up an authenticated user is found - AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser()); + // Setting up an authenticated user is found (but only after the second call to lookupUser, that is, not coming from the builtin user provider) + AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser(), true); // When invoking lookupUserByOIDCBearerToken - User actualUser = sut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); + User actualUser = spySut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); // Then the actual user should match the expected authenticated user assertEquals(authenticatedUser, actualUser); + + // Capture calls to lookupUser + ArgumentCaptor providerIdCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor userIdCaptor = ArgumentCaptor.forClass(String.class); + + // Ensure lookupUser is called twice + Mockito.verify(spySut, Mockito.times(2)).lookupUser(providerIdCaptor.capture(), userIdCaptor.capture()); + + // Assert that the first call was with expected parameters + assertEquals(BuiltinAuthenticationProvider.PROVIDER_ID, providerIdCaptor.getAllValues().get(0)); + assertEquals("testUserId", userIdCaptor.getAllValues().get(0)); } @Test @@ -137,15 +152,28 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsBuilt // Given a single OIDC provider that returns a valid user identifier setUpOIDCProviderWhichValidatesToken(); - // Setting up authenticated builtin user is found - AuthenticatedUser authenticatedUser = new AuthenticatedUser(); - setupBuiltinUserOnIdMatchFeatureFlagQueriesWithResult(authenticatedUser); + // Spy on the SUT to verify method calls + AuthenticationServiceBean spySut = Mockito.spy(sut); + + // Setting up an authenticated user is found + AuthenticatedUser authenticatedUser = setupAuthenticatedUserByAuthPrvIDQueryWithResult(new AuthenticatedUser()); // When invoking lookupUserByOIDCBearerToken - User actualUser = sut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); + User actualUser = spySut.lookupUserByOIDCBearerToken(TEST_BEARER_TOKEN); // Then the actual user should match the expected authenticated user assertEquals(authenticatedUser, actualUser); + + // Capture calls to lookupUser + ArgumentCaptor providerIdCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor userIdCaptor = ArgumentCaptor.forClass(String.class); + + // Ensure lookupUser is called once + Mockito.verify(spySut, Mockito.times(1)).lookupUser(providerIdCaptor.capture(), userIdCaptor.capture()); + + // Assert that lookupUser is called with expected parameters + assertEquals(BuiltinAuthenticationProvider.PROVIDER_ID, providerIdCaptor.getAllValues().get(0)); + assertEquals("testUserId", userIdCaptor.getAllValues().get(0)); } private void setupAuthenticatedUserQueryWithNoResult() { @@ -183,26 +211,21 @@ private OIDCAuthProvider stubOIDCAuthProvider(String providerID) { } private AuthenticatedUser setupAuthenticatedUserByAuthPrvIDQueryWithResult(AuthenticatedUser authenticatedUser) { + return setupAuthenticatedUserByAuthPrvIDQueryWithResult(authenticatedUser, false); + } + + private AuthenticatedUser setupAuthenticatedUserByAuthPrvIDQueryWithResult(AuthenticatedUser authenticatedUser, boolean returnNullOnFirstCall) { TypedQuery queryStub = Mockito.mock(TypedQuery.class); AuthenticatedUserLookup lookupStub = Mockito.mock(AuthenticatedUserLookup.class); Mockito.when(lookupStub.getAuthenticatedUser()).thenReturn(authenticatedUser); - Mockito.when(queryStub.getSingleResult()).thenReturn(lookupStub); + if (returnNullOnFirstCall) { + Mockito.when(queryStub.getSingleResult()) + .thenThrow(new NoResultException()) + .thenReturn(lookupStub); + } else { + Mockito.when(queryStub.getSingleResult()).thenReturn(lookupStub); + } Mockito.when(sut.em.createNamedQuery("AuthenticatedUserLookup.findByAuthPrvID_PersUserId", AuthenticatedUserLookup.class)).thenReturn(queryStub); return authenticatedUser; } - - private void setupBuiltinUserOnIdMatchFeatureFlagQueriesWithResult(AuthenticatedUser authenticatedUser) { - TypedQuery authenticatedUserQueryStub = Mockito.mock(TypedQuery.class); - Mockito.when(authenticatedUserQueryStub.getSingleResult()).thenReturn(authenticatedUser); - Mockito.when(authenticatedUserQueryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(authenticatedUserQueryStub); - - TypedQuery authenticatedUserLookupQueryStub = Mockito.mock(TypedQuery.class); - AuthenticatedUserLookup authenticatedUserLookupMock = Mockito.mock(AuthenticatedUserLookup.class); - Mockito.when(authenticatedUserLookupMock.getAuthenticationProviderId()).thenReturn("builtin"); - Mockito.when(authenticatedUserLookupQueryStub.getSingleResult()).thenReturn(authenticatedUserLookupMock); - Mockito.when(authenticatedUserLookupQueryStub.setParameter(Mockito.anyString(), Mockito.any())).thenReturn(authenticatedUserLookupQueryStub); - - Mockito.when(sut.em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class)).thenReturn(authenticatedUserQueryStub); - Mockito.when(sut.em.createNamedQuery("AuthenticatedUserLookup.findByAuthUser", AuthenticatedUserLookup.class)).thenReturn(authenticatedUserLookupQueryStub); - } } From a7a751d2b2adab1efca90f9ae4fb34c4d33f232d Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 11 Mar 2025 16:23:02 +0000 Subject: [PATCH 50/70] Changed: Keycloak version updated to v25 --- conf/keycloak/Dockerfile | 4 +++- conf/keycloak/builtin-users-spi/pom.xml | 2 +- conf/keycloak/setup-spi.sh | 11 +++++++---- docker-compose-dev.yml | 4 +++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/conf/keycloak/Dockerfile b/conf/keycloak/Dockerfile index fbb6a654312..5c3d966c593 100644 --- a/conf/keycloak/Dockerfile +++ b/conf/keycloak/Dockerfile @@ -14,7 +14,9 @@ RUN mvn clean package # ------------------------------------------ # Stage 2: Build Keycloak Image # ------------------------------------------ -FROM quay.io/keycloak/keycloak:22.0 +FROM quay.io/keycloak/keycloak:25.0.6 + +ENV KC_HEALTH_ENABLED=true # Copy SPI JAR from builder stage COPY --from=builder /app/target/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar /opt/keycloak/providers/ diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index 51397036103..2aac61de91f 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -100,7 +100,7 @@ - 22.0.0 + 25.0.6 17 3.2.0 0.4 diff --git a/conf/keycloak/setup-spi.sh b/conf/keycloak/setup-spi.sh index 92640916f56..f287d71a039 100755 --- a/conf/keycloak/setup-spi.sh +++ b/conf/keycloak/setup-spi.sh @@ -4,12 +4,15 @@ echo "Waiting for Keycloak to be fully up..." # Loop until the health check returns 200 while true; do - HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://keycloak:8090/health") - if [ "$HTTP_RESPONSE" -eq 200 ]; then - echo "Keycloak is up! (HTTP $HTTP_RESPONSE)" + RESPONSE=$(curl -s -w "\n%{http_code}" "http://keycloak:9000/health") + HTTP_BODY=$(echo "$RESPONSE" | head -n -1) # Extract response body + HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) # Extract HTTP status code + + if [ "$HTTP_CODE" -eq 200 ]; then + echo "Keycloak is up! (HTTP $HTTP_CODE)" break else - echo "Health check failed. Waiting..." + echo "Health check failed (HTTP $HTTP_CODE). Response: $HTTP_BODY" sleep 5 fi done diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index bd5813fa20c..d2ff8f3d87e 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -191,7 +191,9 @@ services: dataverse: aliases: - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) - command: start-dev --import-realm --health-enabled=true --http-port=8090 # change port to 8090, so within the network and external the same port is used + command: start-dev --verbose --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used + expose: + - "9000" ports: - "8090:8090" From 781614b81cf32416c8453bdbc64353fd295bc8c0 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 19 Mar 2025 15:02:56 +0000 Subject: [PATCH 51/70] Changed: Dataverse version in since section of feature flag --- .../java/edu/harvard/iq/dataverse/settings/FeatureFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index d619650acb2..f3b9a1bf180 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -68,7 +68,7 @@ public enum FeatureFlags { * {@link #API_BEARER_AUTH} is enabled.

* * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth-use-builtin-user-on-id-match" - * @since Dataverse @TODO: + * @since Dataverse @6.7: */ API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH("api-bearer-auth-use-builtin-user-on-id-match"), /** From 97489e846e0a96973f94fb8d165b877704a4d6e4 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 10:12:00 +0000 Subject: [PATCH 52/70] Added: note to PasswordEncryption SPI class to advice about syncing the class with the source Dataverse one --- .../iq/keycloak/auth/spi/services/PasswordEncryption.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java index 18b8b18ad76..f8ecd4232b3 100644 --- a/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java +++ b/conf/keycloak/builtin-users-spi/src/main/java/edu/harvard/iq/keycloak/auth/spi/services/PasswordEncryption.java @@ -16,6 +16,10 @@ * of the {@link #algorithms} array. The rest should pretty much happen automatically * (e.g system will detect outdated passwords for users and initiate the password reset breakout). * + * NOTE: This class is a copy of the one in + * {@code edu.harvard.iq.dataverse.authorization.providers.builtin} + * within the Dataverse application and must stay in sync with it. + * * @author Ellen Kraffmiller * @author Michael Bar-Sinai */ From f5de1d183f461215784395d9c3127ee489032308 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 11:04:09 +0000 Subject: [PATCH 53/70] Changed: image name in dev_keycloak container --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 6369b96c931..3cd9773b163 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -175,7 +175,7 @@ services: build: context: ./conf/keycloak dockerfile: Dockerfile - image: gdcc/keycloak:unstable + image: dev_keycloak hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin From 82a90a0d295572694ff2a5be6534386dc2556a95 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 11:35:35 +0000 Subject: [PATCH 54/70] Changed: keycloak image name --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 3cd9773b163..18e19f07c83 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -175,7 +175,7 @@ services: build: context: ./conf/keycloak dockerfile: Dockerfile - image: dev_keycloak + image: keycloak hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin From 62d3b1338d035b66ea59d56b8fe4e1076184b85c Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 12:02:01 +0000 Subject: [PATCH 55/70] Stash: upgrading to kc 26 --- conf/keycloak/Dockerfile | 10 +++++++++- .../keycloak/builtin-users-spi/conf/quarkus.properties | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/conf/keycloak/Dockerfile b/conf/keycloak/Dockerfile index 5c3d966c593..b79c279259b 100644 --- a/conf/keycloak/Dockerfile +++ b/conf/keycloak/Dockerfile @@ -14,8 +14,16 @@ RUN mvn clean package # ------------------------------------------ # Stage 2: Build Keycloak Image # ------------------------------------------ -FROM quay.io/keycloak/keycloak:25.0.6 +FROM quay.io/keycloak/keycloak:26.1.0 +# Add the Oracle JDBC jars +ARG ORACLE_JDBC_VERSION=21.3.0.0 +ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc11/${ORACLE_JDBC_VERSION}/ojdbc11-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/ojdbc11.jar +ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/nls/orai18n/${ORACLE_JDBC_VERSION}/orai18n-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/orai18n.jar + +# Database build parameter +ENV KC_DB=oracle +# Health build parameter ENV KC_HEALTH_ENABLED=true # Copy SPI JAR from builder stage diff --git a/conf/keycloak/builtin-users-spi/conf/quarkus.properties b/conf/keycloak/builtin-users-spi/conf/quarkus.properties index 26aa7616235..2bf9514d367 100644 --- a/conf/keycloak/builtin-users-spi/conf/quarkus.properties +++ b/conf/keycloak/builtin-users-spi/conf/quarkus.properties @@ -4,7 +4,7 @@ quarkus.datasource.user-store.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.password=${DATAVERSE_DB_PASSWORD} quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver -quarkus.datasource.user-store.jdbc.transactions=disabled +quarkus.datasource.user-store.jdbc.transactions=xa quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.jdbc.recovery.password=${DATAVERSE_DB_PASSWORD} From 49a6cf0865d0d329999a865452fd6f286baa9933 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 12:25:17 +0000 Subject: [PATCH 56/70] Changed: Keycloak upgraded to 26 --- conf/keycloak/Dockerfile | 2 -- conf/keycloak/builtin-users-spi/conf/quarkus.properties | 3 ++- docker-compose-dev.yml | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/conf/keycloak/Dockerfile b/conf/keycloak/Dockerfile index b79c279259b..2fcf5b91d26 100644 --- a/conf/keycloak/Dockerfile +++ b/conf/keycloak/Dockerfile @@ -21,8 +21,6 @@ ARG ORACLE_JDBC_VERSION=21.3.0.0 ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc11/${ORACLE_JDBC_VERSION}/ojdbc11-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/ojdbc11.jar ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/nls/orai18n/${ORACLE_JDBC_VERSION}/orai18n-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/orai18n.jar -# Database build parameter -ENV KC_DB=oracle # Health build parameter ENV KC_HEALTH_ENABLED=true diff --git a/conf/keycloak/builtin-users-spi/conf/quarkus.properties b/conf/keycloak/builtin-users-spi/conf/quarkus.properties index 2bf9514d367..64ce6d898c5 100644 --- a/conf/keycloak/builtin-users-spi/conf/quarkus.properties +++ b/conf/keycloak/builtin-users-spi/conf/quarkus.properties @@ -4,7 +4,8 @@ quarkus.datasource.user-store.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.password=${DATAVERSE_DB_PASSWORD} quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver -quarkus.datasource.user-store.jdbc.transactions=xa +quarkus.datasource.user-store.jdbc.transactions=disabled +quarkus.transaction-manager.unsafe-multiple-last-resources=allow quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER} quarkus.datasource.user-store.jdbc.recovery.password=${DATAVERSE_DB_PASSWORD} diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 18e19f07c83..dc0de1ff293 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -182,6 +182,10 @@ services: - KEYCLOAK_ADMIN_PASSWORD=kcpassword - KEYCLOAK_LOGLEVEL=DEBUG - KC_HOSTNAME_STRICT=false + - KC_DB=postgres + - KC_DB_URL=jdbc:postgresql://postgres:5432/dataverse + - KC_DB_USERNAME=${DATAVERSE_DB_USER} + - KC_DB_PASSWORD=secret - DATAVERSE_DB_HOST=postgres - DATAVERSE_DB_PORT=5432 - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} From ba08b5f5e28697fcc7e1e666ae39ea689e17b594 Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 12:29:18 +0000 Subject: [PATCH 57/70] Changed: upgraded Oracle JDBC version in Keycloak image --- conf/keycloak/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/keycloak/Dockerfile b/conf/keycloak/Dockerfile index 2fcf5b91d26..fbbf5e22a98 100644 --- a/conf/keycloak/Dockerfile +++ b/conf/keycloak/Dockerfile @@ -17,7 +17,7 @@ RUN mvn clean package FROM quay.io/keycloak/keycloak:26.1.0 # Add the Oracle JDBC jars -ARG ORACLE_JDBC_VERSION=21.3.0.0 +ARG ORACLE_JDBC_VERSION=23.7.0.25.01 ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc11/${ORACLE_JDBC_VERSION}/ojdbc11-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/ojdbc11.jar ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/nls/orai18n/${ORACLE_JDBC_VERSION}/orai18n-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/orai18n.jar From 0fc1bc913b6bbcbe64ab828fb8caadb929b0747a Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 15:04:54 +0000 Subject: [PATCH 58/70] Changed: Keycloak version upgraded to 26.1.4 --- conf/keycloak/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/keycloak/Dockerfile b/conf/keycloak/Dockerfile index fbbf5e22a98..76088f402c7 100644 --- a/conf/keycloak/Dockerfile +++ b/conf/keycloak/Dockerfile @@ -14,7 +14,7 @@ RUN mvn clean package # ------------------------------------------ # Stage 2: Build Keycloak Image # ------------------------------------------ -FROM quay.io/keycloak/keycloak:26.1.0 +FROM quay.io/keycloak/keycloak:26.1.4 # Add the Oracle JDBC jars ARG ORACLE_JDBC_VERSION=23.7.0.25.01 From ffa040f24c6ded9ec12fa49ce64bb9decf9de88a Mon Sep 17 00:00:00 2001 From: GPortas Date: Thu, 27 Mar 2025 15:27:36 +0000 Subject: [PATCH 59/70] Changed: keycloak.version in the SPI to 26.1.4 --- conf/keycloak/builtin-users-spi/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/keycloak/builtin-users-spi/pom.xml b/conf/keycloak/builtin-users-spi/pom.xml index 2aac61de91f..afb3495c2be 100644 --- a/conf/keycloak/builtin-users-spi/pom.xml +++ b/conf/keycloak/builtin-users-spi/pom.xml @@ -100,7 +100,7 @@ - 25.0.6 + 26.1.4 17 3.2.0 0.4 From 5e983afaa1952366aa8baf8d95e0d68225c5604f Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 28 Mar 2025 08:39:51 +0000 Subject: [PATCH 60/70] Changed: image name for keycloak container --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index dc0de1ff293..7a0819061da 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -175,7 +175,7 @@ services: build: context: ./conf/keycloak dockerfile: Dockerfile - image: keycloak + image: gdcc/keycloak hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin From f062ef57c9b0548811ee0514eccc25984d93e768 Mon Sep 17 00:00:00 2001 From: GPortas Date: Sun, 30 Mar 2025 16:39:17 +0100 Subject: [PATCH 61/70] Changed: Keycloak using custom image / spi to separate compose file within the conf subdirectory --- .gitignore | 1 + conf/keycloak/.env | 5 + conf/keycloak/docker-compose-dev.yml | 309 +++++++++++++++++++++++++++ docker-compose-dev.yml | 35 +-- 4 files changed, 319 insertions(+), 31 deletions(-) create mode 100644 conf/keycloak/.env create mode 100644 conf/keycloak/docker-compose-dev.yml diff --git a/.gitignore b/.gitignore index 514f82116de..bb9686ae629 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,6 @@ src/main/webapp/resources/images/cc0.png.thumb140 src/main/webapp/resources/images/dataverseproject.png.thumb140 # Docker development volumes +/conf/keycloak/docker-dev-volumes /docker-dev-volumes /.vs diff --git a/conf/keycloak/.env b/conf/keycloak/.env new file mode 100644 index 00000000000..6d99d85b3a7 --- /dev/null +++ b/conf/keycloak/.env @@ -0,0 +1,5 @@ +APP_IMAGE=gdcc/dataverse:unstable +POSTGRES_VERSION=17 +DATAVERSE_DB_USER=dataverse +SOLR_VERSION=9.8.0 +SKIP_DEPLOY=0 \ No newline at end of file diff --git a/conf/keycloak/docker-compose-dev.yml b/conf/keycloak/docker-compose-dev.yml new file mode 100644 index 00000000000..55400a7a97d --- /dev/null +++ b/conf/keycloak/docker-compose-dev.yml @@ -0,0 +1,309 @@ +version: "2.4" + +services: + + dev_dataverse: + container_name: "dev_dataverse" + hostname: dataverse + image: ${APP_IMAGE} + restart: on-failure + user: payara + environment: + DATAVERSE_DB_HOST: postgres + DATAVERSE_DB_PASSWORD: secret + DATAVERSE_DB_USER: ${DATAVERSE_DB_USER} + ENABLE_JDWP: "1" + ENABLE_RELOAD: "1" + SKIP_DEPLOY: "${SKIP_DEPLOY}" + DATAVERSE_JSF_REFRESH_PERIOD: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH: "1" + DATAVERSE_FEATURE_INDEX_HARVESTED_METADATA_SOURCE: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH_PROVIDE_MISSING_CLAIMS: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH: "1" + DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" + DATAVERSE_MAIL_MTA_HOST: "smtp" + DATAVERSE_AUTH_OIDC_ENABLED: "1" + DATAVERSE_AUTH_OIDC_CLIENT_ID: test + DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 + DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test + DATAVERSE_SPI_EXPORTERS_DIRECTORY: "/dv/exporters" + # These two oai settings are here to get HarvestingServerIT to pass + dataverse_oai_server_maxidentifiers: "2" + dataverse_oai_server_maxrecords: "2" + JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 + -Ddataverse.files.file1.type=file + -Ddataverse.files.file1.label=Filesystem + -Ddataverse.files.file1.directory=${STORAGE_DIR}/store + -Ddataverse.files.localstack1.type=s3 + -Ddataverse.files.localstack1.label=LocalStack + -Ddataverse.files.localstack1.custom-endpoint-url=http://localstack:4566 + -Ddataverse.files.localstack1.custom-endpoint-region=us-east-2 + -Ddataverse.files.localstack1.bucket-name=mybucket + -Ddataverse.files.localstack1.path-style-access=true + -Ddataverse.files.localstack1.upload-redirect=true + -Ddataverse.files.localstack1.download-redirect=true + -Ddataverse.files.localstack1.access-key=default + -Ddataverse.files.localstack1.secret-key=default + -Ddataverse.files.minio1.type=s3 + -Ddataverse.files.minio1.label=MinIO + -Ddataverse.files.minio1.custom-endpoint-url=http://minio:9000 + -Ddataverse.files.minio1.custom-endpoint-region=us-east-1 + -Ddataverse.files.minio1.bucket-name=mybucket + -Ddataverse.files.minio1.path-style-access=true + -Ddataverse.files.minio1.upload-redirect=false + -Ddataverse.files.minio1.download-redirect=false + -Ddataverse.files.minio1.access-key=4cc355_k3y + -Ddataverse.files.minio1.secret-key=s3cr3t_4cc355_k3y + -Ddataverse.pid.providers=fake + -Ddataverse.pid.default-provider=fake + -Ddataverse.pid.fake.type=FAKE + -Ddataverse.pid.fake.label=FakeDOIProvider + -Ddataverse.pid.fake.authority=10.5072 + -Ddataverse.pid.fake.shoulder=FK2/ + #-Ddataverse.lang.directory=/dv/lang + ports: + - "8080:8080" # HTTP (Dataverse Application) + - "4949:4848" # HTTPS (Payara Admin Console) + - "9009:9009" # JDWP + - "8686:8686" # JMX + networks: + - dataverse + depends_on: + - dev_postgres + - dev_solr + - dev_dv_initializer + volumes: + - ./docker-dev-volumes/app/data:/dv + - ./docker-dev-volumes/app/secrets:/secrets + - ../../target/dataverse:/opt/payara/deployments/dataverse:ro + tmpfs: + - /dumps:mode=770,size=2052M,uid=1000,gid=1000 + - /tmp:mode=770,size=2052M,uid=1000,gid=1000 + mem_limit: 2147483648 # 2 GiB + mem_reservation: 1024m + privileged: false + + dev_bootstrap: + container_name: "dev_bootstrap" + image: gdcc/configbaker:unstable + restart: "no" + command: + - bootstrap.sh + - dev + networks: + - dataverse + volumes: + - ./docker-dev-volumes/solr/data:/var/solr + + dev_dv_initializer: + container_name: "dev_dv_initializer" + image: gdcc/configbaker:unstable + restart: "no" + command: + - sh + - -c + - "fix-fs-perms.sh dv" + volumes: + - ./docker-dev-volumes/app/data:/dv + + dev_postgres: + container_name: "dev_postgres" + hostname: postgres + image: postgres:${POSTGRES_VERSION} + restart: on-failure + environment: + - POSTGRES_USER=${DATAVERSE_DB_USER} + - POSTGRES_PASSWORD=secret + ports: + - "5432:5432" + networks: + - dataverse + volumes: + - ./docker-dev-volumes/postgresql/data:/var/lib/postgresql/data + + dev_solr_initializer: + container_name: "dev_solr_initializer" + image: gdcc/configbaker:unstable + restart: "no" + command: + - sh + - -c + - "fix-fs-perms.sh solr && cp -a /template/* /solr-template" + volumes: + - ./docker-dev-volumes/solr/data:/var/solr + - ./docker-dev-volumes/solr/conf:/solr-template + + dev_solr: + container_name: "dev_solr" + hostname: "solr" + image: solr:${SOLR_VERSION} + depends_on: + - dev_solr_initializer + restart: on-failure + ports: + - "8983:8983" + networks: + - dataverse + command: + - "solr-precreate" + - "collection1" + - "/template" + volumes: + - ./docker-dev-volumes/solr/data:/var/solr + - ./docker-dev-volumes/solr/conf:/template + + dev_smtp: + container_name: "dev_smtp" + hostname: "smtp" + image: maildev/maildev:2.0.5 + restart: on-failure + ports: + - "25:25" # smtp server + - "1080:1080" # web ui + environment: + - MAILDEV_SMTP_PORT=25 + - MAILDEV_MAIL_DIRECTORY=/mail + networks: + - dataverse + #volumes: + # - ./docker-dev-volumes/smtp/data:/mail + tmpfs: + - /mail:mode=770,size=128M,uid=1000,gid=1000 + + dev_keycloak: + container_name: "dev_keycloak" + build: + context: ./conf/keycloak + dockerfile: Dockerfile + image: gdcc/keycloak + hostname: keycloak + environment: + - KEYCLOAK_ADMIN=kcadmin + - KEYCLOAK_ADMIN_PASSWORD=kcpassword + - KEYCLOAK_LOGLEVEL=DEBUG + - KC_HOSTNAME_STRICT=false + - KC_DB=postgres + - KC_DB_URL=jdbc:postgresql://postgres:5432/dataverse + - KC_DB_USERNAME=${DATAVERSE_DB_USER} + - KC_DB_PASSWORD=secret + - DATAVERSE_DB_HOST=postgres + - DATAVERSE_DB_PORT=5432 + - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} + - DATAVERSE_DB_PASSWORD=secret + - DATAVERSE_BASE_URL=http://dataverse:8080 + networks: + dataverse: + aliases: + - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) + command: start-dev --verbose --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used + expose: + - "9000" + ports: + - "8090:8090" + + dev_keycloak_initializer: + image: alpine:latest + container_name: "dev_keycloak_initializer" + depends_on: + - dev_keycloak + environment: + - KEYCLOAK_ADMIN=kcadmin + - KEYCLOAK_ADMIN_PASSWORD=kcpassword + volumes: + - ./setup-spi.sh:/usr/local/bin/setup-spi.sh + command: [ "/bin/sh", "-c", "apk add --no-cache curl jq && /usr/local/bin/setup-spi.sh" ] + networks: + - dataverse + + # This proxy configuration is only intended to be used for development purposes! + # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! + dev_proxy: + image: caddy:2-alpine + # The command below is enough to enable using the admin gui, but it will not rewrite location headers to HTTP. + # To achieve rewriting from https:// to http://, we need a simple configuration file + #command: ["caddy", "reverse-proxy", "-f", ":4848", "-t", "https://dataverse:4848", "--insecure"] + command: ["caddy", "run", "-c", "/Caddyfile"] + ports: + - "4848:4848" # Will expose Payara Admin Console (HTTPS) as HTTP + restart: always + volumes: + - ../proxy/Caddyfile:/Caddyfile:ro + depends_on: + - dev_dataverse + networks: + - dataverse + + dev_localstack: + container_name: "dev_localstack" + hostname: "localstack" + image: localstack/localstack:2.3.2 + restart: on-failure + ports: + - "127.0.0.1:4566:4566" + environment: + - DEBUG=${DEBUG-} + - DOCKER_HOST=unix:///var/run/docker.sock + - HOSTNAME_EXTERNAL=localstack + networks: + - dataverse + volumes: + - ../localstack:/etc/localstack/init/ready.d + tmpfs: + - /localstack:mode=770,size=128M,uid=1000,gid=1000 + + dev_minio: + container_name: "dev_minio" + hostname: "minio" + image: minio/minio + restart: on-failure + ports: + - "9000:9000" + - "9001:9001" + networks: + - dataverse + volumes: + - ./docker-dev-volumes/minio_storage:/data + environment: + MINIO_ROOT_USER: 4cc355_k3y + MINIO_ROOT_PASSWORD: s3cr3t_4cc355_k3y + command: server /data + + previewers-provider: + container_name: previewers-provider + hostname: previewers-provider + image: trivadis/dataverse-previewers-provider:latest + ports: + - "9080:9080" + networks: + - dataverse + environment: + # have nginx match the port we run previewers on + - NGINX_HTTP_PORT=9080 + - PREVIEWERS_PROVIDER_URL=http://localhost:9080 + - VERSIONS="v1.4,betatest" + # https://docs.docker.com/reference/compose-file/services/#platform + # https://github.com/fabric8io/docker-maven-plugin/issues/1750 + platform: linux/amd64 + + register-previewers: + container_name: register-previewers + hostname: register-previewers + image: trivadis/dataverse-deploy-previewers:latest + networks: + - dataverse + environment: + - DATAVERSE_URL=http://dataverse:8080 + - TIMEOUT=10m + - PREVIEWERS_PROVIDER_URL=http://localhost:9080 + # Uncomment to specify which previewers you want. Otherwise you get all of them. + #- INCLUDE_PREVIEWERS=text,html,pdf,csv,comma-separated-values,tsv,tab-separated-values,jpeg,png,gif,markdown,x-markdown + - EXCLUDE_PREVIEWERS= + - REMOVE_EXISTING=true + command: + - deploy + restart: "no" + platform: linux/amd64 + +networks: + dataverse: + driver: bridge diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 7a0819061da..7c536c2d12d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -19,7 +19,6 @@ services: DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_FEATURE_INDEX_HARVESTED_METADATA_SOURCE: "1" DATAVERSE_FEATURE_API_BEARER_AUTH_PROVIDE_MISSING_CLAIMS: "1" - DATAVERSE_FEATURE_API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" @@ -60,7 +59,7 @@ services: -Ddataverse.pid.fake.label=FakeDOIProvider -Ddataverse.pid.fake.authority=10.5072 -Ddataverse.pid.fake.shoulder=FK2/ - #-Ddataverse.lang.directory=/dv/lang + #-Ddataverse.lang.directory=/dv/lang ports: - "8080:8080" # HTTP (Dataverse Application) - "4949:4848" # HTTPS (Payara Admin Console) @@ -172,48 +171,22 @@ services: dev_keycloak: container_name: "dev_keycloak" - build: - context: ./conf/keycloak - dockerfile: Dockerfile - image: gdcc/keycloak + image: 'quay.io/keycloak/keycloak:21.0' hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin - KEYCLOAK_ADMIN_PASSWORD=kcpassword - KEYCLOAK_LOGLEVEL=DEBUG - KC_HOSTNAME_STRICT=false - - KC_DB=postgres - - KC_DB_URL=jdbc:postgresql://postgres:5432/dataverse - - KC_DB_USERNAME=${DATAVERSE_DB_USER} - - KC_DB_PASSWORD=secret - - DATAVERSE_DB_HOST=postgres - - DATAVERSE_DB_PORT=5432 - - DATAVERSE_DB_USER=${DATAVERSE_DB_USER} - - DATAVERSE_DB_PASSWORD=secret - - DATAVERSE_BASE_URL=http://dataverse:8080 networks: dataverse: aliases: - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) - command: start-dev --verbose --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used - expose: - - "9000" + command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used ports: - "8090:8090" - - dev_keycloak_initializer: - image: alpine:latest - container_name: "dev_keycloak_initializer" - depends_on: - - dev_keycloak - environment: - - KEYCLOAK_ADMIN=kcadmin - - KEYCLOAK_ADMIN_PASSWORD=kcpassword volumes: - - ./conf/keycloak/setup-spi.sh:/usr/local/bin/setup-spi.sh - command: [ "/bin/sh", "-c", "apk add --no-cache curl jq && /usr/local/bin/setup-spi.sh" ] - networks: - - dataverse + - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' # This proxy configuration is only intended to be used for development purposes! # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! From c376299c7f521883757e66160000e0242d87ed5c Mon Sep 17 00:00:00 2001 From: GPortas Date: Sun, 30 Mar 2025 16:46:59 +0100 Subject: [PATCH 62/70] Added: explanatory comment to conf/keycloak/docker-compose-dev --- conf/keycloak/docker-compose-dev.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/conf/keycloak/docker-compose-dev.yml b/conf/keycloak/docker-compose-dev.yml index 55400a7a97d..afc6dae194a 100644 --- a/conf/keycloak/docker-compose-dev.yml +++ b/conf/keycloak/docker-compose-dev.yml @@ -1,3 +1,12 @@ +# This file is designed for testing Keycloak authentication using the +# Dataverse Builtin Users SPI. +# +# Keycloak is deployed using a custom-built image, defined by a Dockerfile +# located in this directory. This allows for a controlled +# and flexible development setup. Note that this image is currently +# intended for development and testing purposes only and should be used +# accordingly in non-production environments. + version: "2.4" services: From effe1dd95d0e5b49a0616229f78e697a6b189a77 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 31 Mar 2025 10:07:48 -0400 Subject: [PATCH 63/70] revert compost file to how it was #11157 --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 7c536c2d12d..8db5b52777d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -59,7 +59,7 @@ services: -Ddataverse.pid.fake.label=FakeDOIProvider -Ddataverse.pid.fake.authority=10.5072 -Ddataverse.pid.fake.shoulder=FK2/ - #-Ddataverse.lang.directory=/dv/lang + #-Ddataverse.lang.directory=/dv/lang ports: - "8080:8080" # HTTP (Dataverse Application) - "4949:4848" # HTTPS (Payara Admin Console) From 425942abf70c04f1ef0ab535c9b2dd2b4cfff599 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 31 Mar 2025 10:20:56 -0400 Subject: [PATCH 64/70] tweak release note snippet #11157 --- doc/release-notes/11197-builtin-users-oidc-auth.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/release-notes/11197-builtin-users-oidc-auth.md b/doc/release-notes/11197-builtin-users-oidc-auth.md index 758c2c5f696..4a9d3292eaf 100644 --- a/doc/release-notes/11197-builtin-users-oidc-auth.md +++ b/doc/release-notes/11197-builtin-users-oidc-auth.md @@ -1,14 +1,16 @@ ### API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH feature flag -Introduced a new feature flag, ``API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH``, which allows the use of a built-in user +A new feature flag called `API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH` has been introduced, which allows the use of a built-in user account when an identity match is found during OIDC API bearer token authentication. This feature enables automatic association of an incoming IdP identity with an existing built-in user account, bypassing the need for additional user registration steps. +See [the guides](https://dataverse-guide--11193.org.readthedocs.build/en/11193/installation/config.html#feature-flags), #11193, #11197, and #11314. + ### Keycloak SPI for Built-In users -A Keycloak SPI, ``builtin-users-spi``, has been implemented that allows the use of Keycloak on instances with built-in +A Keycloak SPI, `builtin-users-spi`, has been implemented that allows the use of Keycloak on instances with built-in accounts for OIDC authentication, enabling the use of the SPA on those instances. @@ -16,4 +18,4 @@ Looking ahead, this authenticator SPI could also support mapping Shibboleth user Shib users without changing the provider in the Dataverse database. However, this would require changes to the storage provider to support more than just built-in users. -The SPI code is available in the Dataverse code repository. +The SPI code is available in the Dataverse code repository (`conf/keycloak/builtin-users-spi`). From 357b1b60ad1917bdcf6fd5aaa1fe9f9cc309cfe0 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 31 Mar 2025 15:32:46 +0100 Subject: [PATCH 65/70] Changed: Keycloak version upgraded to 26.1.4 in all places --- conf/keycloak/docker-compose.yml | 2 +- conf/keycloak/run-keycloak.sh | 2 +- docker-compose-dev.yml | 2 +- .../oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/keycloak/docker-compose.yml b/conf/keycloak/docker-compose.yml index 12b2382bd3d..272d8ace363 100644 --- a/conf/keycloak/docker-compose.yml +++ b/conf/keycloak/docker-compose.yml @@ -3,7 +3,7 @@ version: "3.9" services: keycloak: - image: 'quay.io/keycloak/keycloak:21.0' + image: 'quay.io/keycloak/keycloak:26.1.4' command: - "start-dev" - "--import-realm" diff --git a/conf/keycloak/run-keycloak.sh b/conf/keycloak/run-keycloak.sh index ddc5108bee4..9f851a558c7 100755 --- a/conf/keycloak/run-keycloak.sh +++ b/conf/keycloak/run-keycloak.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -DOCKER_IMAGE="quay.io/keycloak/keycloak:21.0" +DOCKER_IMAGE="quay.io/keycloak/keycloak:26.1.4" KEYCLOAK_USER="kcadmin" KEYCLOAK_PASSWORD="kcpassword" KEYCLOAK_PORT=8090 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 7c536c2d12d..9838e5414ce 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -171,7 +171,7 @@ services: dev_keycloak: container_name: "dev_keycloak" - image: 'quay.io/keycloak/keycloak:21.0' + image: 'quay.io/keycloak/keycloak:26.1.4' hostname: keycloak environment: - KEYCLOAK_ADMIN=kcadmin diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java index 58b792691b9..50e1c57035f 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java @@ -69,7 +69,7 @@ class OIDCAuthenticationProviderFactoryIT { // The realm JSON resides in conf/keycloak/test-realm.json and gets avail here using in pom.xml @Container - static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:22.0") + static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:26.1.4") .withRealmImportFile("keycloak/test-realm.json") .withAdminUsername(adminUser) .withAdminPassword(adminPassword); From 7b413f9d02fa7cb597e5b1f1905cded506f4ed53 Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 31 Mar 2025 15:59:19 +0100 Subject: [PATCH 66/70] Changed: reverted keycloak version in IT --- .../oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java index 50e1c57035f..58b792691b9 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java @@ -69,7 +69,7 @@ class OIDCAuthenticationProviderFactoryIT { // The realm JSON resides in conf/keycloak/test-realm.json and gets avail here using in pom.xml @Container - static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:26.1.4") + static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:22.0") .withRealmImportFile("keycloak/test-realm.json") .withAdminUsername(adminUser) .withAdminPassword(adminPassword); From 139b286ac86b402d819b6717ffa7bb10f0983d6d Mon Sep 17 00:00:00 2001 From: GPortas Date: Mon, 31 Mar 2025 16:22:25 +0100 Subject: [PATCH 67/70] Fixed: OIDCAuthenticationProviderFactoryIT to work with Keycloak 26.1.4 --- pom.xml | 2 +- .../OIDCAuthenticationProviderFactoryIT.java | 25 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 2b6d1f1c52c..0c783d5204d 100644 --- a/pom.xml +++ b/pom.xml @@ -733,7 +733,7 @@ com.github.dasniko testcontainers-keycloak - 3.0.0 + 3.6.0 test diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java index 58b792691b9..3f2afb1927e 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java @@ -19,10 +19,7 @@ import org.htmlunit.FailingHttpStatusCodeException; import org.htmlunit.WebClient; import org.htmlunit.WebResponse; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlInput; -import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlSubmitInput; +import org.htmlunit.html.*; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -69,7 +66,7 @@ class OIDCAuthenticationProviderFactoryIT { // The realm JSON resides in conf/keycloak/test-realm.json and gets avail here using in pom.xml @Container - static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:22.0") + static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:26.1.4") .withRealmImportFile("keycloak/test-realm.json") .withAdminUsername(adminUser) .withAdminPassword(adminPassword); @@ -186,8 +183,7 @@ void testAuthorizationCodeFlowWithPKCE() throws Exception { OIDCAuthProvider oidcAuthProvider = getProvider(); String authzUrl = oidcAuthProvider.buildAuthzUrl(state, callbackUrl); - //System.out.println(authzUrl); - + try (WebClient webClient = new WebClient()) { webClient.getOptions().setCssEnabled(false); webClient.getOptions().setJavaScriptEnabled(false); @@ -200,12 +196,12 @@ void testAuthorizationCodeFlowWithPKCE() throws Exception { HtmlForm form = loginPage.getForms().get(0); HtmlInput username = form.getInputByName("username"); HtmlInput password = form.getInputByName("password"); - HtmlSubmitInput submit = form.getInputByName("login"); - + HtmlButton submitButton = (HtmlButton) loginPage.getElementById("kc-login"); + username.type(realmAdminUser); password.type(realmAdminPassword); - - FailingHttpStatusCodeException exception = assertThrows(FailingHttpStatusCodeException.class, submit::click); + + FailingHttpStatusCodeException exception = assertThrows(FailingHttpStatusCodeException.class, submitButton::click); assertEquals(302, exception.getStatusCode()); WebResponse response = exception.getResponse(); @@ -213,14 +209,13 @@ void testAuthorizationCodeFlowWithPKCE() throws Exception { String callbackLocation = response.getResponseHeaderValue("Location"); assertTrue(callbackLocation.startsWith(callbackUrl)); - //System.out.println(callbackLocation); - + String queryPart = callbackLocation.trim().split("\\?")[1]; Map parameters = Pattern.compile("\\s*&\\s*") .splitAsStream(queryPart) .map(s -> s.split("=", 2)) .collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1]: "")); - //System.out.println(map); + assertTrue(parameters.containsKey("code")); assertTrue(parameters.containsKey("state")); @@ -237,4 +232,4 @@ void testAuthorizationCodeFlowWithPKCE() throws Exception { throw e; } } -} \ No newline at end of file +} From fcc133a1440e0806a6110365eb778b973e159627 Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 4 Apr 2025 10:58:26 +0100 Subject: [PATCH 68/70] Fixed: docker build context in keycloak/docker-compose-dev --- conf/keycloak/docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/keycloak/docker-compose-dev.yml b/conf/keycloak/docker-compose-dev.yml index afc6dae194a..7356161ec47 100644 --- a/conf/keycloak/docker-compose-dev.yml +++ b/conf/keycloak/docker-compose-dev.yml @@ -182,7 +182,7 @@ services: dev_keycloak: container_name: "dev_keycloak" build: - context: ./conf/keycloak + context: . dockerfile: Dockerfile image: gdcc/keycloak hostname: keycloak From 0ea888d212af601f810ff8f4e03dacf9071a3409 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 9 Apr 2025 11:40:57 +0100 Subject: [PATCH 69/70] Fixed: looking up builtin user by username when API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH enabled --- .../iq/dataverse/authorization/AuthenticationServiceBean.java | 3 ++- .../authorization/AuthenticationServiceBeanTest.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 0e0d37f486c..c115f946612 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -65,6 +65,7 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; +import org.apache.commons.logging.Log; /** * AuthenticationService is for general authentication-related operations. @@ -996,7 +997,7 @@ public AuthenticatedUser lookupUserByOIDCBearerToken(String bearerToken) throws // Tokens in the cache should be removed after some (configurable) time. OAuth2UserRecord oAuth2UserRecord = verifyOIDCBearerTokenAndGetOAuth2UserRecord(bearerToken); if (FeatureFlags.API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH.enabled()) { - AuthenticatedUser builtinAuthenticatedUser = lookupUser(BuiltinAuthenticationProvider.PROVIDER_ID, oAuth2UserRecord.getUserRecordIdentifier().getUserIdInRepo()); + AuthenticatedUser builtinAuthenticatedUser = lookupUser(BuiltinAuthenticationProvider.PROVIDER_ID, oAuth2UserRecord.getUsername()); return (builtinAuthenticatedUser != null) ? builtinAuthenticatedUser : lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); } return lookupUser(oAuth2UserRecord.getUserRecordIdentifier()); diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java index 7451011bf5d..2a5642d5659 100644 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBeanTest.java @@ -143,7 +143,7 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_userNotPresentAsBuil // Assert that the first call was with expected parameters assertEquals(BuiltinAuthenticationProvider.PROVIDER_ID, providerIdCaptor.getAllValues().get(0)); - assertEquals("testUserId", userIdCaptor.getAllValues().get(0)); + assertEquals("testUsername", userIdCaptor.getAllValues().get(0)); } @Test @@ -173,7 +173,7 @@ void testLookupUserByOIDCBearerToken_oneProvider_validToken_userIsPresentAsBuilt // Assert that lookupUser is called with expected parameters assertEquals(BuiltinAuthenticationProvider.PROVIDER_ID, providerIdCaptor.getAllValues().get(0)); - assertEquals("testUserId", userIdCaptor.getAllValues().get(0)); + assertEquals("testUsername", userIdCaptor.getAllValues().get(0)); } private void setupAuthenticatedUserQueryWithNoResult() { From e52b5afda9697a9aa54d5d2051a2da2f9907eeb6 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 9 Apr 2025 11:43:01 +0100 Subject: [PATCH 70/70] Removed: unused import --- .../iq/dataverse/authorization/AuthenticationServiceBean.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index c115f946612..b49fa70cea1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -65,7 +65,6 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; -import org.apache.commons.logging.Log; /** * AuthenticationService is for general authentication-related operations.