diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a1fd44b5a..826879f9e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -356,8 +356,8 @@ jobs: - testSuite: AppContext04TestSuite compose-profile: with-transform-core-aio - testSuite: AppContext05TestSuite - compose-profile: default - mvn-options: '"-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth"' + compose-profile: with-ids + mvn-options: '-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth -Dauthentication.chain=identity-service1:identity-service,alfrescoNtlm1:alfrescoNtlm' - testSuite: AppContext06TestSuite compose-profile: with-transform-core-aio - testSuite: AppContextExtraTestSuite @@ -381,6 +381,8 @@ jobs: run: bash ./scripts/ci/init.sh - name: "Set transformers tag" run: echo "TRANSFORMERS_TAG=$(mvn help:evaluate -Dexpression=dependency.alfresco-transform-core.version -q -DforceStdout)" >> $GITHUB_ENV + - name: "Set the host IP" + run: echo "HOST_IP=$(hostname -I | cut -f1 -d' ')" >> $GITHUB_ENV - name: "Generate Keystores and Truststores for Mutual TLS configuration" if: ${{ matrix.mtls }} run: | @@ -393,11 +395,7 @@ jobs: echo "HOSTNAME_VERIFICATION_DISABLED=false" >> "$GITHUB_ENV" fi - name: "Set up the environment" - run: | - if [ -e ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh ]; then - bash ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh - fi - docker-compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d + run: docker-compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d - name: "Run tests" run: mvn -B test -pl repository -am -Dtest=${{ matrix.testSuite }} -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco ${{ matrix.mvn-options }} - name: "Clean Maven cache" diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java index a4712341a8..fa6becfa2b 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java @@ -523,13 +523,13 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest private RemoteUserMapper createRemoteUserMapperToUseForTheTest(boolean useIdentityService) { PersonService personServiceLocal = (PersonService) applicationContext.getBean("PersonService"); + RemoteUserMapper remoteUserMapper; if (useIdentityService) { InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper(); interceptingRemoteUserMapper.setActive(true); - interceptingRemoteUserMapper.setPersonService(personServiceLocal); - interceptingRemoteUserMapper.setIdentityServiceFacade(null); + interceptingRemoteUserMapper.setJitProvisioningHandler(null); interceptingRemoteUserMapper.setUserIdToReturn(user2); remoteUserMapper = interceptingRemoteUserMapper; } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java index c22628df93..90d0d9e0a6 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java @@ -50,6 +50,9 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati private IdentityServiceFacade identityServiceFacade; /** enabled flag for the identity service subsystem**/ private boolean active; + + private IdentityServiceJITProvisioningHandler jitProvisioningHandler; + private boolean allowGuestLogin; public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade) @@ -62,6 +65,12 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati this.allowGuestLogin = allowGuestLogin; } + public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler) + { + this.jitProvisioningHandler = jitProvisioningHandler; + } + + @Override public void authenticateImpl(String userName, char[] password) throws AuthenticationException { if (identityServiceFacade == null) @@ -77,10 +86,13 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati try { // Attempt to verify user credentials - identityServiceFacade.authorize(AuthorizationGrant.password(userName, new String(password))); + IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.password(userName, String.valueOf(password))); + String normalizedUsername = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(accessTokenAuthorization.getAccessToken().getTokenValue()) + .map(OIDCUserInfo::username) + .orElseThrow(() -> new AuthenticationException("Failed to extract username from token and user info endpoint.")); // Verification was successful so treat as authenticated user - setCurrentUser(userName); + setCurrentUser(normalizedUsername); } catch (IdentityServiceFacadeException e) { diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java index 0efe457d1d..8569fea5c7 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java @@ -30,6 +30,7 @@ import static java.util.Objects.requireNonNull; import java.time.Instant; import java.util.Objects; +import java.util.Optional; /** * Allows to interact with the Identity Service @@ -52,6 +53,14 @@ public interface IdentityServiceFacade */ DecodedAccessToken decodeToken(String token) throws TokenDecodingException; + /** + * Gets claims about the authenticated user, + * such as name and email address, via the UserInfo endpoint of the OpenID provider. + * @param token {@link String} with encoded access token value. + * @return {@link OIDCUserInfo} containing user claims. + */ + Optional getUserInfo(String token); + class IdentityServiceFacadeException extends RuntimeException { public IdentityServiceFacadeException(String message) @@ -78,6 +87,20 @@ public interface IdentityServiceFacade } } + class UserInfoException extends IdentityServiceFacadeException + { + + UserInfoException(String message) + { + super(message); + } + + UserInfoException(String message, Throwable cause) + { + super(message, cause); + } + } + class TokenDecodingException extends IdentityServiceFacadeException { TokenDecodingException(String message) diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java index 192bffd660..44ac5f6298 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java @@ -188,6 +188,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean getUserInfo(String token) + { + return getTargetFacade().getUserInfo(token); + } + private IdentityServiceFacade getTargetFacade() { return ofNullable(targetFacade.get()) @@ -224,9 +230,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean clientRegistrationProvider, BiFunction jwtDecoderProvider) { - this.httpClientProvider = Objects.requireNonNull(httpClientProvider); - this.clientRegistrationProvider = Objects.requireNonNull(clientRegistrationProvider); - this.jwtDecoderProvider = Objects.requireNonNull(jwtDecoderProvider); + this.httpClientProvider = requireNonNull(httpClientProvider); + this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider); + this.jwtDecoderProvider = requireNonNull(jwtDecoderProvider); } private IdentityServiceFacade createIdentityServiceFacade() @@ -259,7 +265,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean. + * #L% + */ + +package org.alfresco.repo.security.authentication.identityservice; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.nimbusds.openid.connect.sdk.claims.PersonClaims; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.lang3.StringUtils; + +/** + * This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo} + * from {@link IdentityServiceFacade.DecodedAccessToken} or {@link UserInfo} + * and creates a new user if it does not exist in the repository. + */ +public class IdentityServiceJITProvisioningHandler +{ + private final IdentityServiceFacade identityServiceFacade; + private final PersonService personService; + private final TransactionService transactionService; + + private final Function> mapTokenToUserInfoResponse = token -> { + Optional firstName = Optional.ofNullable(token.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)) + .filter(String.class::isInstance) + .map(String.class::cast); + Optional lastName = Optional.ofNullable(token.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)) + .filter(String.class::isInstance) + .map(String.class::cast); + Optional email = Optional.ofNullable(token.getClaim(PersonClaims.EMAIL_CLAIM_NAME)) + .filter(String.class::isInstance) + .map(String.class::cast); + + return Optional.ofNullable(token.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)) + .filter(String.class::isInstance) + .map(String.class::cast) + .map(this::normalizeUserId) + .map(username -> new OIDCUserInfo(username, firstName.orElse(""), lastName.orElse(""), email.orElse(""))); + }; + + public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade, + PersonService personService, + TransactionService transactionService) + { + this.identityServiceFacade = identityServiceFacade; + this.personService = personService; + this.transactionService = transactionService; + } + + public Optional extractUserInfoAndCreateUserIfNeeded(String bearerToken) + { + Optional userInfoResponse = Optional.ofNullable(bearerToken) + .filter(Predicate.not(String::isEmpty)) + .flatMap(token -> extractUserInfoResponseFromAccessToken(token) + .filter(userInfo -> StringUtils.isNotEmpty(userInfo.username())) + .or(() -> extractUserInfoResponseFromEndpoint(token))); + + if (transactionService.isReadOnly() || userInfoResponse.isEmpty()) + { + return userInfoResponse; + } + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() + { + @Override + public Optional doWork() throws Exception + { + return userInfoResponse.map(userInfo -> { + if (userInfo.username() != null && personService.createMissingPeople() + && !personService.personExists(userInfo.username())) + { + + if (!userInfo.allFieldsNotEmpty()) + { + userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo); + } + Map properties = new HashMap<>(); + properties.put(ContentModel.PROP_USERNAME, userInfo.username()); + properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName()); + properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName()); + properties.put(ContentModel.PROP_EMAIL, userInfo.email()); + properties.put(ContentModel.PROP_ORGID, ""); + properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null); + + properties.put(ContentModel.PROP_SIZE_CURRENT, 0L); + properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota + + personService.createPerson(properties); + } + return userInfo; + }); + } + }, AuthenticationUtil.getSystemUserName()); + } + + private Optional extractUserInfoResponseFromAccessToken(String bearerToken) + { + return Optional.ofNullable(bearerToken) + .map(identityServiceFacade::decodeToken) + .flatMap(mapTokenToUserInfoResponse); + } + + private Optional extractUserInfoResponseFromEndpoint(String bearerToken) + { + return identityServiceFacade.getUserInfo(bearerToken) + .filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty()) + .map(userInfo -> new OIDCUserInfo(normalizeUserId(userInfo.username()), + Optional.ofNullable(userInfo.firstName()).orElse(""), + Optional.ofNullable(userInfo.lastName()).orElse(""), + Optional.ofNullable(userInfo.email()).orElse(""))); + } + + /** + * Normalizes a user id, taking into account existing user accounts and case sensitivity settings. + * + * @param userId the user id + * @return the string + */ + private String normalizeUserId(final String userId) + { + if (userId == null) + { + return null; + } + + String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public String doWork() throws Exception + { + return personService.getUserIdentifier(userId); + } + }, AuthenticationUtil.getSystemUserName()); + + return normalized == null ? userId : normalized; + } + +} diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java index 5f5fd40778..2c91ef75ac 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java @@ -32,10 +32,8 @@ import java.util.Optional; import org.alfresco.repo.management.subsystems.ActivateableBean; import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.authentication.external.RemoteUserMapper; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException; -import org.alfresco.service.cmr.security.PersonService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -50,19 +48,16 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolv public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean { private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class); - static final String USERNAME_CLAIM = "preferred_username"; - + /** Is the mapper enabled */ private boolean isEnabled; /** Are token validation failures handled silently? */ private boolean isValidationFailureSilent; - /** The person service. */ - private PersonService personService; - private BearerTokenResolver bearerTokenResolver; - private IdentityServiceFacade identityServiceFacade; + + private IdentityServiceJITProvisioningHandler jitProvisioningHandler; /** * Sets the active flag @@ -83,26 +78,15 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa { this.isValidationFailureSilent = silent; } - - /** - * Sets the person service. - * - * @param personService - * the person service - */ - public void setPersonService(PersonService personService) - { - this.personService = personService; - } public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { this.bearerTokenResolver = bearerTokenResolver; } - public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade) + public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler) { - this.identityServiceFacade = identityServiceFacade; + this.jitProvisioningHandler = jitProvisioningHandler; } /* @@ -121,14 +105,13 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa } try { - String headerUserId = extractUserFromHeader(request); + String normalizedUserId = extractUserFromHeader(request); - if (headerUserId != null) + + if (normalizedUserId != null) { // Normalize the user ID taking into account case sensitivity settings - String normalizedUserId = normalizeUserId(headerUserId); LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId)); - return normalizedUserId; } } @@ -179,11 +162,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa return null; } - final Optional possibleUsername = Optional.ofNullable(bearerToken) - .map(identityServiceFacade::decodeToken) - .map(t -> t.getClaim(USERNAME_CLAIM)) - .filter(String.class::isInstance) - .map(String.class::cast); + final Optional possibleUsername = jitProvisioningHandler + .extractUserInfoAndCreateUserIfNeeded(bearerToken) + .map(OIDCUserInfo::username); if (possibleUsername.isEmpty()) { @@ -191,39 +172,10 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa return null; } - String username = possibleUsername.get(); - LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(username)); + String normalizedUsername = possibleUsername.get(); + LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(normalizedUsername)); - return username; - } - - /** - * Normalizes a user id, taking into account existing user accounts and case sensitivity settings. - * - * @param userId - * the user id - * @return the string - */ - private String normalizeUserId(final String userId) - { - if (userId == null) - { - return null; - } - - String normalized = AuthenticationUtil.runAs(new RunAsWork() - { - public String doWork() throws Exception - { - return personService.getUserIdentifier(userId); - } - }, AuthenticationUtil.getSystemUserName()); - - if (LOGGER.isDebugEnabled()) - { - LOGGER.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized)); - } - - return normalized == null ? userId : normalized; + return normalizedUsername; } + } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OIDCUserInfo.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OIDCUserInfo.java new file mode 100644 index 0000000000..5f8a3c25d6 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OIDCUserInfo.java @@ -0,0 +1,73 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2023 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.authentication.identityservice; + +import java.util.stream.Stream; + +/** + * Contains a set of required claims about the authentication of an End-User. + */ +public class OIDCUserInfo +{ + private final String username; + private final String firstName; + private final String lastName; + private final String email; + + public OIDCUserInfo(String username, String firstName, String lastName, String email) + { + this.username = username; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + } + + public String username() + { + return username; + } + + public String firstName() + { + return firstName; + } + + public String lastName() + { + return lastName; + } + + public String email() + { + return email; + } + + public boolean allFieldsNotEmpty() + { + return Stream.of(username, firstName, lastName, email).allMatch(field -> field != null && !field.isEmpty()); + } + +} diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java index c20419cf79..6dcb7dda84 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java @@ -23,13 +23,24 @@ * along with Alfresco. If not, see . * #L% */ + package org.alfresco.repo.security.authentication.identityservice; import static java.util.Objects.requireNonNull; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Instant; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; + +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.UserInfoRequest; +import com.nimbusds.openid.connect.sdk.UserInfoResponse; +import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -100,6 +111,42 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade return new SpringAccessTokenAuthorization(response); } + @Override + public Optional getUserInfo(String tokenParameter) + { + return Optional.ofNullable(tokenParameter) + .filter(Predicate.not(String::isEmpty)) + .flatMap(token -> Optional.ofNullable(clientRegistration) + .map(ClientRegistration::getProviderDetails) + .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint) + .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUri) + .flatMap(uri -> { + try + { + return Optional.of(new UserInfoRequest(new URI(uri), new BearerAccessToken(token)).toHTTPRequest().send()); + } + catch (IOException | URISyntaxException e) + { + LOGGER.warn("Failed to get user information. Reason: " + e.getMessage()); + return Optional.empty(); + } + }) + .flatMap(httpResponse -> { + try + { + return Optional.of(UserInfoResponse.parse(httpResponse)); + } + catch (ParseException e) + { + LOGGER.warn("Failed to parse user info response. Reason: " + e.getMessage()); + return Optional.empty(); + } + }) + .map(UserInfoResponse::toSuccessResponse) + .map(UserInfoSuccessResponse::getUserInfo)) + .map(userInfo -> new OIDCUserInfo(userInfo.getPreferredUsername(), userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress())); + } + @Override public DecodedAccessToken decodeToken(String token) { diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml index 93be0fb893..483bdd5b04 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml @@ -24,6 +24,9 @@ + + + @@ -147,17 +150,20 @@ ${identity-service.authentication.validation.failure.silent} - - - - - + + + + + + + + diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index fee8a20d49..0977a7d3b0 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -26,6 +26,7 @@ package org.alfresco; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest; import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest; import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest; import org.alfresco.util.testing.category.DBTests; @@ -143,6 +144,7 @@ import org.junit.runners.Suite; IdentityServiceFacadeFactoryBeanTest.class, LazyInstantiatingIdentityServiceFacadeUnitTest.class, SpringBasedIdentityServiceFacadeUnitTest.class, + IdentityServiceJITProvisioningHandlerUnitTest.class, org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class, org.alfresco.repo.security.authentication.PasswordHashingTest.class, org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class, diff --git a/repository/src/test/java/org/alfresco/AppContext05TestSuite.java b/repository/src/test/java/org/alfresco/AppContext05TestSuite.java index 4bf1627dad..6e92701079 100644 --- a/repository/src/test/java/org/alfresco/AppContext05TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext05TestSuite.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -60,6 +60,7 @@ import org.junit.runners.Suite; org.alfresco.repo.security.authentication.external.DefaultRemoteUserMapperTest.class, org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponentTest.class, org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapperTest.class, + org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerTest.class, org.alfresco.repo.security.authentication.subsystems.SubsystemChainingFtpAuthenticatorTest.class, org.alfresco.repo.security.authentication.external.LocalAuthenticationServiceTest.class, org.alfresco.repo.domain.contentdata.ContentDataDAOTest.class, diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java index 166647f4cc..5ca4b96162 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.net.ConnectException; +import java.util.Optional; import org.alfresco.error.ExceptionStackUtil; import org.alfresco.repo.security.authentication.AuthenticationContext; @@ -66,6 +67,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest @Autowired private PersonService personService; + + private IdentityServiceJITProvisioningHandler jitProvisioning; private IdentityServiceFacade mockIdentityServiceFacade; @Before @@ -77,7 +80,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest authComponent.setNodeService(nodeService); authComponent.setPersonService(personService); + jitProvisioning = mock(IdentityServiceJITProvisioningHandler.class); mockIdentityServiceFacade = mock(IdentityServiceFacade.class); + authComponent.setJitProvisioningHandler(jitProvisioning); authComponent.setIdentityServiceFacade(mockIdentityServiceFacade); } @@ -134,8 +139,13 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest { final AuthorizationGrant grant = AuthorizationGrant.password("username", "password"); AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class); + IdentityServiceFacade.AccessToken accessToken = mock(IdentityServiceFacade.AccessToken.class); + when(authorization.getAccessToken()).thenReturn(accessToken); + when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN"); when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization); + when(jitProvisioning.extractUserInfoAndCreateUserIfNeeded("JWT_TOKEN")) + .thenReturn(Optional.of(new OIDCUserInfo("username", "", "", ""))); authComponent.authenticateImpl("username", "password".toCharArray()); diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java index 5b07c83db0..9f543c6190 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java @@ -25,7 +25,6 @@ */ package org.alfresco.repo.security.authentication.identityservice; -import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,6 +32,7 @@ import static org.mockito.Mockito.when; import java.util.Map; import java.util.UUID; +import com.nimbusds.openid.connect.sdk.claims.PersonClaims; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator; import org.junit.Test; @@ -64,7 +64,7 @@ public class IdentityServiceFacadeFactoryBeanTest final Map claims = decodedToken.getClaims(); assertThat(claims).isNotNull() .isNotEmpty() - .containsEntry(USERNAME_CLAIM, "piotrek"); + .containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "piotrek"); } @Test diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java new file mode 100644 index 0000000000..ffa0b633c6 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerTest.java @@ -0,0 +1,162 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2023 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.authentication.identityservice; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Optional; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory; +import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +@SuppressWarnings("PMD.AvoidAccessibilityAlteration") +public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest +{ + private static final String IDS_USERNAME = "johndoe123"; + + private PersonService personService; + private NodeService nodeService; + private TransactionService transactionService; + private IdentityServiceFacade identityServiceFacade; + private IdentityServiceJITProvisioningHandler jitProvisioningHandler; + + @Before + public void setup() + { + personService = (PersonService) applicationContext.getBean("personService"); + nodeService = (NodeService) applicationContext.getBean("nodeService"); + transactionService = (TransactionService) applicationContext.getBean("transactionService"); + DefaultChildApplicationContextManager childApplicationContextManager = (DefaultChildApplicationContextManager) applicationContext + .getBean("Authentication"); + ChildApplicationContextFactory childApplicationContextFactory = childApplicationContextManager.getChildApplicationContextFactory( + "identity-service1"); + + identityServiceFacade = (IdentityServiceFacade) childApplicationContextFactory.getApplicationContext() + .getBean("identityServiceFacade"); + jitProvisioningHandler = (IdentityServiceJITProvisioningHandler) childApplicationContextFactory.getApplicationContext() + .getBean("jitProvisioningHandler"); + IdentityServiceConfig identityServiceConfig = (IdentityServiceConfig) childApplicationContextFactory.getApplicationContext() + .getBean("identityServiceConfig"); + identityServiceConfig.setAllowAnyHostname(true); + identityServiceConfig.setClientKeystore(null); + identityServiceConfig.setDisableTrustManager(true); + } + + @Test + public void shouldCreateNonExistingUserInRepo() + { + assertFalse(personService.personExists(IDS_USERNAME)); + + IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = + identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password")); + + Optional userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + accessTokenAuthorization.getAccessToken().getTokenValue()); + + NodeRef person = personService.getPerson(IDS_USERNAME); + + assertTrue(userInfoOptional.isPresent()); + assertEquals(IDS_USERNAME, userInfoOptional.get().username()); + assertEquals("John", userInfoOptional.get().firstName()); + assertEquals("Doe", userInfoOptional.get().lastName()); + assertEquals("johndoe@test.com", userInfoOptional.get().email()); + assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME)); + assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME)); + assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME)); + assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL)); + } + + @Test + public void shouldCallUserInfoEndpointAndCreateUser() throws IllegalAccessException, NoSuchFieldException + { + assertFalse(personService.personExists(IDS_USERNAME)); + + IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = + identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password")); + + String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue(); + IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class); + when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null); + when(idsServiceFacadeMock.getUserInfo(accessToken)).thenReturn(identityServiceFacade.getUserInfo(accessToken)); + + // Replace the original facade with a mocked one to prevent user information from being extracted from the access token. + Field declaredField = jitProvisioningHandler.getClass() + .getDeclaredField("identityServiceFacade"); + declaredField.setAccessible(true); + declaredField.set(jitProvisioningHandler, idsServiceFacadeMock); + + Optional userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + accessToken); + + declaredField.set(jitProvisioningHandler, identityServiceFacade); + + NodeRef person = personService.getPerson(IDS_USERNAME); + + assertTrue(userInfoOptional.isPresent()); + assertEquals(IDS_USERNAME, userInfoOptional.get().username()); + assertEquals("John", userInfoOptional.get().firstName()); + assertEquals("Doe", userInfoOptional.get().lastName()); + assertEquals("johndoe@test.com", userInfoOptional.get().email()); + assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME)); + assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME)); + assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME)); + assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL)); + verify(idsServiceFacadeMock).decodeToken(accessToken); + verify(idsServiceFacadeMock).getUserInfo(accessToken); + } + + @After + public void tearDown() + { + AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork() + { + @Override + public Void doWork() throws Exception + { + transactionService.getRetryingTransactionHelper() + .doInTransaction((RetryingTransactionCallback) () -> { + personService.deletePerson(IDS_USERNAME); + return null; + }); + return null; + } + }); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java new file mode 100644 index 0000000000..a49bbb69a5 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceJITProvisioningHandlerUnitTest.java @@ -0,0 +1,196 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2023 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.repo.security.authentication.identityservice; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.util.Optional; + +import com.nimbusds.openid.connect.sdk.claims.PersonClaims; + +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class IdentityServiceJITProvisioningHandlerUnitTest +{ + + @Mock + private IdentityServiceFacade identityServiceFacade; + + @Mock + private PersonService personService; + + @Mock + private IdentityServiceFacade.DecodedAccessToken decodedAccessToken; + + @Mock + private TransactionService transactionService; + + @Mock + private OIDCUserInfo userInfo; + + private IdentityServiceJITProvisioningHandler jitProvisioningHandler; + + private static final String JWT_TOKEN = "myToken"; + + @Before + public void setup() + { + initMocks(this); + + when(transactionService.isReadOnly()).thenReturn(false); + when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken); + when(personService.createMissingPeople()).thenReturn(true); + jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, + personService, transactionService); + } + + @Test + public void shouldExtractUserInfoForExistingUser() + { + when(personService.personExists("johny123")).thenReturn(true); + when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123"); + + Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + JWT_TOKEN); + + assertTrue(result.isPresent()); + assertEquals("johny123", result.get().username()); + assertFalse(result.get().allFieldsNotEmpty()); + verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN); + } + + @Test + public void shouldExtractUserInfoFromAccessTokenAndCreateUser() + { + when(personService.personExists("johny123")).thenReturn(false); + + when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123"); + when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn("John"); + when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn("Doe"); + when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn("johny123@email.com"); + + Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + JWT_TOKEN); + + assertTrue(result.isPresent()); + assertEquals("johny123", result.get().username()); + assertEquals("John", result.get().firstName()); + assertEquals("Doe", result.get().lastName()); + assertEquals("johny123@email.com", result.get().email()); + assertTrue(result.get().allFieldsNotEmpty()); + verify(personService).createPerson(any()); + verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN); + } + + @Test + public void shouldExtractUserInfoFromUserInfoEndpointAndCreateUser() + { + when(userInfo.username()).thenReturn("johny123"); + when(userInfo.firstName()).thenReturn("John"); + when(userInfo.lastName()).thenReturn("Doe"); + when(userInfo.email()).thenReturn("johny123@email.com"); + + when(personService.personExists("johny123")).thenReturn(false); + + when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123"); + when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo)); + + Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + JWT_TOKEN); + + assertTrue(result.isPresent()); + assertEquals("johny123", result.get().username()); + assertEquals("John", result.get().firstName()); + assertEquals("Doe", result.get().lastName()); + assertEquals("johny123@email.com", result.get().email()); + assertTrue(result.get().allFieldsNotEmpty()); + verify(personService).createPerson(any()); + verify(identityServiceFacade).getUserInfo(JWT_TOKEN); + } + + @Test + public void shouldReturnEmptyOptionalIfUsernameNotExtracted() + { + + when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo)); + + Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + JWT_TOKEN); + + assertFalse(result.isPresent()); + verify(personService, never()).createPerson(any()); + verify(identityServiceFacade).getUserInfo(JWT_TOKEN); + } + + @Test + public void shouldCallUserInfoEndpointToGetUsername() + { + when(personService.personExists("johny123")).thenReturn(true); + + when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(""); + + when(userInfo.username()).thenReturn("johny123"); + when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo)); + + Optional result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded( + JWT_TOKEN); + + assertTrue(result.isPresent()); + assertEquals("johny123", result.get().username()); + assertEquals("", result.get().firstName()); + assertEquals("", result.get().lastName()); + assertEquals("", result.get().email()); + assertFalse(result.get().allFieldsNotEmpty()); + verify(personService, never()).createPerson(any()); + verify(identityServiceFacade).getUserInfo(JWT_TOKEN); + } + + @Test + public void shouldNotCallUserInfoEndpointIfTokenIsNullOrEmpty() + { + jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(null); + jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(""); + + verify(personService, never()).createPerson(any()); + verify(identityServiceFacade, never()).decodeToken(null); + verify(identityServiceFacade, never()).decodeToken(""); + verify(identityServiceFacade, never()).getUserInfo(null); + verify(identityServiceFacade, never()).getUserInfo(""); + } + +} diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java index ae94c6bc95..d35a8deb45 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java @@ -25,9 +25,6 @@ */ package org.alfresco.repo.security.authentication.identityservice; -import static java.util.Optional.ofNullable; - -import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -38,13 +35,15 @@ import java.util.Map; import java.util.Vector; import java.util.function.Supplier; -import jakarta.servlet.http.HttpServletRequest; +import com.nimbusds.openid.connect.sdk.claims.PersonClaims; +import jakarta.servlet.http.HttpServletRequest; import junit.framework.TestCase; import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; /** @@ -92,16 +91,19 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase private IdentityServiceRemoteUserMapper givenMapper(Map> tokenToUser) { + final TransactionService transactionService = mock(TransactionService.class); final IdentityServiceFacade facade = mock(IdentityServiceFacade.class); + final PersonService personService = mock(PersonService.class); + when(transactionService.isReadOnly()).thenReturn(true); when(facade.decodeToken(anyString())) .thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class)))); - final PersonService personService = mock(PersonService.class); when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class)); + final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService); + final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper(); - mapper.setIdentityServiceFacade(facade); - mapper.setPersonService(personService); + mapper.setJitProvisioningHandler(jitProvisioning); mapper.setActive(true); mapper.setBearerTokenResolver(new DefaultBearerTokenResolver()); @@ -160,7 +162,7 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase @Override public Object getClaim(String claim) { - return USERNAME_CLAIM.equals(claim) ? usernameSupplier.get() : null; + return PersonClaims.PREFERRED_USERNAME_CLAIM_NAME.equals(claim) ? usernameSupplier.get() : null; } } } diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java index 1bba1071c6..9d32a2ccce 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java @@ -25,6 +25,7 @@ */ package org.alfresco.repo.security.authentication.identityservice; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -73,11 +74,24 @@ public class SpringBasedIdentityServiceFacadeUnitTest .havingCause().withNoCause().withMessage("Expected"); } + + @Test + public void shouldReturnEmptyOptionalOnFailure() + { + final RestOperations restOperations = mock(RestOperations.class); + final JwtDecoder jwtDecoder = mock(JwtDecoder.class); + final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder); + + + assertThat(facade.getUserInfo(TOKEN).isEmpty()).isTrue(); + } + private ClientRegistration testRegistration() { return ClientRegistration.withRegistrationId("test") .tokenUri("http://localhost") .clientId("test") + .userInfoUri("http://localhost/userinfo") .authorizationGrantType(AuthorizationGrantType.PASSWORD) .build(); } diff --git a/repository/src/test/resources/alfresco-global.properties b/repository/src/test/resources/alfresco-global.properties index 2aa3bbaeab..bdbec71ca8 100644 --- a/repository/src/test/resources/alfresco-global.properties +++ b/repository/src/test/resources/alfresco-global.properties @@ -1,15 +1,8 @@ # Test identity service authentication overrides #identity-service.auth-server-url=http://192.168.0.1:8180/auth identity-service.realm=alfresco -identity-service.realm-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l\ -BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM\ -uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W\ -ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD\ -P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j\ -QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx\ -jwIDAQAB identity-service.ssl-required=external -identity-service.resource=test +identity-service.resource=alfresco identity-service.public-client=false identity-service.confidential-port=100 identity-service.use-resource-role-mappings=true @@ -41,11 +34,11 @@ identity-service.min-time-between-jwks-requests=60 identity-service.public-key-cache-ttl=3600 identity-service.enable-pkce=true identity-service.ignore-oauth-query-parameter=true -identity-service.credentials.secret=11111 +identity-service.credentials.secret= identity-service.credentials.provider=secret identity-service.client-socket-timeout=1000 identity-service.client-connection-timeout=3000 -identity-service.authentication.enable-username-password-authentication=false +identity-service.authentication.enable-username-password-authentication=true # Use a date in the past, so data is read straight away rather than being scheduled in tests. A few ms is too late. mimetype.config.cronExpression=0 0 0 ? JAN * 1970 diff --git a/repository/src/test/resources/realms/alfresco-realm.json b/repository/src/test/resources/realms/alfresco-realm.json index 8555a970af..0a11c3d706 100644 --- a/repository/src/test/resources/realms/alfresco-realm.json +++ b/repository/src/test/resources/realms/alfresco-realm.json @@ -1866,6 +1866,46 @@ ], "totp": false, "username": "testuser" + }, + { + "clientRoles": { + "account": [ + "manage-account", + "view-profile" + ] + }, + "credentials": [ + { + "algorithm": "pbkdf2", + "counter": 0, + "digits": 0, + "hashIterations": 20000, + "hashedSaltedValue": "+A2UrlK6T33IHVutjQj9k8S8kMco1IMnmCTngEg+PE+2vO4jJScux6wcltsRIYILv5ggcS3PI7tbsynq5u39sQ==", + "period": 0, + "salt": "IyVlItIo27bmACSLi4yQkA==", + "type": "password" + } + ], + "disableableCredentialTypes": [ + "password" + ], + "email": "johndoe@test.com", + "emailVerified": false, + "enabled": true, + "firstName": "John", + "groups": [ + "/admin", + "/testgroup" + ], + "lastName": "Doe", + "realmRoles": [ + "uma_authorization", + "user", + "offline_access", + "test_role" + ], + "totp": false, + "username": "johndoe123" } ], "keycloakVersion": "8.0.1", diff --git a/scripts/ci/docker-compose/docker-compose.yaml b/scripts/ci/docker-compose/docker-compose.yaml index 36ea0c7089..40bf97c523 100644 --- a/scripts/ci/docker-compose/docker-compose.yaml +++ b/scripts/ci/docker-compose/docker-compose.yaml @@ -10,7 +10,7 @@ services: - "8090:8090" postgres: image: postgres:15.4 - profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio"] + profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio", "with-ids"] environment: - POSTGRES_PASSWORD=alfresco - POSTGRES_USER=alfresco @@ -19,7 +19,7 @@ services: ports: - "5433:5432" activemq: - profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio"] + profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio", "with-ids"] image: alfresco/alfresco-activemq:5.18.3-jre17-rockylinux8 ports: - "5672:5672" # AMQP @@ -57,3 +57,15 @@ services: CLIENT_SSL_TRUST_STORE: "file:/tengineAIO.truststore" CLIENT_SSL_TRUST_STORE_PASSWORD: "password" CLIENT_SSL_TRUST_STORE_TYPE: "JCEKS" + alfresco-identity-service: + profiles: ["with-ids"] + image: quay.io/alfresco/alfresco-identity-service:2.0.0 + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_DB=dev-mem + command: ["start-dev", "--import-realm", "--http-relative-path=/auth", "--hostname=localhost", "--http-enabled=true"] + volumes: + - ../../../repository/src/test/resources/realms/alfresco-realm.json:/opt/keycloak/data/import/alfresco-realm.json + ports: + - 8999:8080 \ No newline at end of file diff --git a/scripts/ci/tests/AppContext05TestSuite-setup.sh b/scripts/ci/tests/AppContext05TestSuite-setup.sh deleted file mode 100644 index c8458de3c8..0000000000 --- a/scripts/ci/tests/AppContext05TestSuite-setup.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -echo "=========================== Starting AppContext05TestSuite setup ===========================" -PS4="\[\e[35m\]+ \[\e[m\]" -set -vex -pushd "$(dirname "${BASH_SOURCE[0]}")/../../../" - -mkdir -p "${HOME}/tmp" -cp repository/src/test/resources/realms/alfresco-realm.json "${HOME}/tmp" -echo "HOST_IP=$(hostname -I | cut -f1 -d' ')" >> $GITHUB_ENV -docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e DB_VENDOR=h2 -p 8999:8080 -e KEYCLOAK_IMPORT=/tmp/alfresco-realm.json -v $HOME/tmp/alfresco-realm.json:/tmp/alfresco-realm.json alfresco/alfresco-identity-service:1.2 - -popd -set +vex -echo "=========================== Finishing AppContext05TestSuite setup ==========================" \ No newline at end of file