diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java index 9f7f5db104730ab9cdf3d28993c56f96800f7ac1..bfdc73ae9a22bf0728ab70a73237032eb4fe0a23 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java @@ -45,11 +45,12 @@ import org.apereo.cas.authentication.principal.ClientCredential; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.web.support.WebUtils; +import org.pac4j.core.context.JEEContext; +import org.pac4j.core.context.session.SessionStore; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContextHolder; -import javax.servlet.http.HttpServletRequest; +import lombok.val; /** * Post-processor which also handles the surrogation in the authentication delegation. @@ -60,28 +61,34 @@ public class DelegatedSurrogateAuthenticationPostProcessor extends SurrogateAuth private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(DelegatedSurrogateAuthenticationPostProcessor.class); + private final SessionStore sessionStore; + public DelegatedSurrogateAuthenticationPostProcessor(final SurrogateAuthenticationService surrogateAuthenticationService, final ServicesManager servicesManager, final ApplicationEventPublisher applicationEventPublisher, final AuditableExecution registeredServiceAccessStrategyEnforcer, - final AuditableExecution surrogateEligibilityAuditableExecution) { + final AuditableExecution surrogateEligibilityAuditableExecution, + final SessionStore sessionStore) { super(surrogateAuthenticationService, servicesManager, applicationEventPublisher, registeredServiceAccessStrategyEnforcer, surrogateEligibilityAuditableExecution); + this.sessionStore = sessionStore; } @Override public void process(final AuthenticationBuilder builder, final AuthenticationTransaction transaction) throws AuthenticationException { - final Credential credential = transaction.getPrimaryCredential().get(); + val credential = transaction.getPrimaryCredential().get(); if (credential instanceof ClientCredential) { - final ClientCredential clientCredential = (ClientCredential) credential; - final RequestContext requestContext = RequestContextHolder.getRequestContext(); - final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); - final String surrogate = (String) request.getAttribute(Constants.SURROGATE); - if (surrogate != null) { - LOGGER.debug("surrogate: {} found after authentication delegation -> overriding credential", surrogate); - final SurrogateUsernamePasswordCredential newCredential = new SurrogateUsernamePasswordCredential(); + val clientCredential = (ClientCredential) credential; + val requestContext = RequestContextHolder.getRequestContext(); + val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); + val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); + val webContext = new JEEContext(request, response, sessionStore); + val surrogateInSession = sessionStore.get(webContext, Constants.SURROGATE).orElse(null); + if (surrogateInSession != null) { + LOGGER.debug("surrogate: {} found after authentication delegation -> overriding credential", surrogateInSession); + val newCredential = new SurrogateUsernamePasswordCredential(); newCredential.setUsername(clientCredential.getUserProfile().getId()); - newCredential.setSurrogateUsername(surrogate); + newCredential.setSurrogateUsername((String) surrogateInSession); WebUtils.putCredential(requestContext, newCredential); final AuthenticationTransaction newTransaction = DefaultAuthenticationTransaction.of(transaction.getService(), newCredential); @@ -90,7 +97,6 @@ public class DelegatedSurrogateAuthenticationPostProcessor extends SurrogateAuth return; } } else { - super.process(builder, transaction); } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationService.java index 5c0c7369ccb05daa3193c3a0a7af1bccda626a18..072a03748b0a42342ce3d6cf3dac0ecbe9e22ba2 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationService.java @@ -41,10 +41,7 @@ import fr.gouv.vitamui.commons.api.exception.VitamUIException; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; -import fr.gouv.vitamui.iam.common.dto.SubrogationDto; import fr.gouv.vitamui.iam.common.enums.SubrogationStatusEnum; -import lombok.Getter; -import lombok.Setter; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.surrogate.BaseSurrogateAuthenticationService; @@ -52,13 +49,13 @@ import org.apereo.cas.services.ServicesManager; import java.util.List; +import lombok.val; + /** * Specific surrogate REST based on the IAM API. * * */ -@Getter -@Setter public class IamSurrogateRestAuthenticationService extends BaseSurrogateAuthenticationService { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(IamSurrogateRestAuthenticationService.class); @@ -76,10 +73,10 @@ public class IamSurrogateRestAuthenticationService extends BaseSurrogateAuthenti @Override public boolean canAuthenticateAsInternal(final String surrogate, final Principal principal, final Service service) { - final String id = principal.getId(); + val id = (String) principal.getAttributes().get(UserPrincipalResolver.SUPER_USER_ID_ATTRIBUTE).get(0); boolean canAuthenticate = false; try { - final List<SubrogationDto> subrogations = casExternalRestClient.getSubrogationsBySuperUserId(utils.buildContext(id), id); + val subrogations = casExternalRestClient.getSubrogationsBySuperUserId(utils.buildContext(id), id); canAuthenticate = subrogations .stream() .filter(s -> s.getStatus() == SubrogationStatusEnum.ACCEPTED) diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/SurrogatedUserPrincipalFactory.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/SurrogatedUserPrincipalFactory.java deleted file mode 100644 index d5921efe6dea388b94a0864d4e3a4ad63e0ddcf1..0000000000000000000000000000000000000000 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/SurrogatedUserPrincipalFactory.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) - * and the signatories of the "VITAM - Accord du Contributeur" agreement. - * - * contact@programmevitam.fr - * - * This software is a computer program whose purpose is to implement - * implement a digital archiving front-office system for the secure and - * efficient high volumetry VITAM solution. - * - * This software is governed by the CeCILL-C license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-C - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-C license and that you accept its terms. - */ -package fr.gouv.vitamui.cas.authentication; - -import fr.gouv.vitamui.commons.api.exception.VitamUIException; -import fr.gouv.vitamui.commons.api.logger.VitamUILogger; -import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; -import lombok.RequiredArgsConstructor; -import org.apereo.cas.authentication.principal.Principal; -import org.apereo.cas.authentication.principal.PrincipalFactory; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Principal factory used for surrogation to find the real user and its profile. - * - * - */ -@RequiredArgsConstructor -public class SurrogatedUserPrincipalFactory implements PrincipalFactory { - - private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(SurrogatedUserPrincipalFactory.class); - - private final UserPrincipalResolver resolver; - - @Override - public Principal createPrincipal(final String username) { - throw new UnsupportedOperationException("This method cannot be used"); - } - - @Override - public Principal createPrincipal(final String id, final Map<String, List<Object>> attributes) { - LOGGER.debug("Creating username: {}", id); - try { - final Principal principal = resolver.resolve(id, new HashMap<>()); - LOGGER.debug("principal: {}", principal); - return principal; - - } catch (final VitamUIException e) { - LOGGER.error("Cannot get surrogated user: {}", id, e); - } - return null; - } -} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserAuthenticationHandler.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserAuthenticationHandler.java index 18cf370d8c7a602b64f7c61aa31924d3e28915e2..300b951813c5ee9b1723898a3af6e8f247a87d62 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserAuthenticationHandler.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserAuthenticationHandler.java @@ -63,15 +63,13 @@ import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.commons.api.exception.VitamUIException; import fr.gouv.vitamui.commons.api.exception.InvalidAuthenticationException; import fr.gouv.vitamui.commons.api.exception.TooManyRequestsException; -import fr.gouv.vitamui.commons.rest.client.ExternalHttpContext; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import fr.gouv.vitamui.commons.api.domain.UserDto; import fr.gouv.vitamui.commons.api.enums.UserStatusEnum; -import org.springframework.webflow.context.ExternalContext; -import org.springframework.webflow.core.collection.MutableAttributeMap; -import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.RequestContextHolder; +import lombok.val; + /** * Authentication handler to check the username/password on the IAM API. * @@ -99,19 +97,19 @@ public class UserAuthenticationHandler extends AbstractUsernamePasswordAuthentic protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential, final String originalPassword) throws GeneralSecurityException, PreventedException { - final String username = transformedCredential.getUsername().toLowerCase(); - final RequestContext requestContext = RequestContextHolder.getRequestContext(); + val username = transformedCredential.getUsername().toLowerCase(); + val requestContext = RequestContextHolder.getRequestContext(); String surrogate = null; String ip = null; if (requestContext != null) { - final MutableAttributeMap<Object> flow = requestContext.getFlowScope(); + val flow = requestContext.getFlowScope(); if (flow != null) { - final Object credential = flow.get("credential"); + val credential = flow.get("credential"); if (credential instanceof SurrogateUsernamePasswordCredential) { surrogate = ((SurrogateUsernamePasswordCredential) credential).getSurrogateUsername(); } } - final ExternalContext externalContext = requestContext.getExternalContext(); + val externalContext = requestContext.getExternalContext(); if (externalContext != null) { ip = ((HttpServletRequest) externalContext.getNativeRequest()).getHeader(ipHeaderName); } @@ -119,9 +117,9 @@ public class UserAuthenticationHandler extends AbstractUsernamePasswordAuthentic LOGGER.debug("Authenticating username: {} / surrogate: {} / IP: {}", username, surrogate, ip); - final ExternalHttpContext context = utils.buildContext(username); + val context = utils.buildContext(username); try { - final UserDto user = casExternalRestClient.login(context, username, originalPassword, surrogate, ip); + val user = casExternalRestClient.login(context, username, originalPassword, surrogate, ip); if (user != null) { if (mustChangePassword(user)) { LOGGER.info("Password expired for: {}", username); @@ -158,7 +156,7 @@ public class UserAuthenticationHandler extends AbstractUsernamePasswordAuthentic } protected boolean mustChangePassword(final UserDto user) { - final OffsetDateTime pwdExpirationDate = user.getPasswordExpirationDate(); + val pwdExpirationDate = user.getPasswordExpirationDate(); return (pwdExpirationDate == null || pwdExpirationDate.isBefore(OffsetDateTime.now())); } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java index 3dc60b13b08a524c9e4de82a6b6f136dd7249d66..0aa50da217d4e5f49bcab6b12c121adc1f41b4da 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java @@ -71,8 +71,6 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.USER_ID_ATTRIBUTE; import java.util.*; -import javax.servlet.http.HttpServletRequest; - import lombok.RequiredArgsConstructor; import org.apereo.cas.authentication.AuthenticationHandler; import org.apereo.cas.authentication.Credential; @@ -84,8 +82,8 @@ import org.apereo.cas.authentication.principal.PrincipalFactory; import org.apereo.cas.authentication.principal.PrincipalResolver; import org.apereo.cas.web.support.WebUtils; import org.apereo.services.persondir.IPersonAttributeDao; -import org.springframework.webflow.core.collection.MutableAttributeMap; -import org.springframework.webflow.execution.RequestContext; +import org.pac4j.core.context.JEEContext; +import org.pac4j.core.context.session.SessionStore; import org.springframework.webflow.execution.RequestContextHolder; import fr.gouv.vitamui.cas.util.Constants; @@ -99,6 +97,8 @@ import fr.gouv.vitamui.commons.api.utils.CasJsonWrapper; import fr.gouv.vitamui.commons.security.client.dto.AuthUserDto; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; +import lombok.val; + /** * Resolver to retrieve the user. * @@ -107,9 +107,9 @@ import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; @RequiredArgsConstructor public class UserPrincipalResolver implements PrincipalResolver { - private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(UserPrincipalResolver.class); + public static final String SUPER_USER_ID_ATTRIBUTE = "superUserId"; - private final boolean finalSurrogationCall; + private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(UserPrincipalResolver.class); private final PrincipalFactory principalFactory; @@ -117,55 +117,49 @@ public class UserPrincipalResolver implements PrincipalResolver { private final Utils utils; + private final SessionStore sessionStore; + @Override public Principal resolve(final Credential credential, final Optional<Principal> principal, final Optional<AuthenticationHandler> handler) { - return resolve(principal.get().getId(), principal.get().getAttributes()); - } - public Principal resolve(final String username, final Map<String, List<Object>> oldAttributes) { - String superUsername = null; - final RequestContext requestContext = RequestContextHolder.getRequestContext(); - if (requestContext != null) { - final MutableAttributeMap<Object> flow = requestContext.getFlowScope(); - // try to find the super user from the regular surrogation flow or in an authentication delegation - if (flow != null) { - final Object credential = flow.get("credential"); - if (credential instanceof SurrogateUsernamePasswordCredential) { - superUsername = ((SurrogateUsernamePasswordCredential) credential).getUsername(); - } - else if (credential instanceof ClientCredential) { - final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); - final boolean hasSurrogateInRequest = request.getAttribute(Constants.SURROGATE) != null; - if (hasSurrogateInRequest) { - superUsername = ((ClientCredential) credential).getUserProfile().getId(); - } - } + val userId = principal.get().getId(); + val requestContext = RequestContextHolder.getRequestContext(); + + boolean surrogationCall; + String username; + String superUsername; + if (credential instanceof SurrogateUsernamePasswordCredential) { + val surrogationCredential = (SurrogateUsernamePasswordCredential) credential; + username = surrogationCredential.getSurrogateUsername(); + superUsername = surrogationCredential.getUsername(); + surrogationCall = true; + } else if (credential instanceof UsernamePasswordCredential) { + username = userId; + superUsername = null; + surrogationCall = false; + } else { // ClientCredential + val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); + val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); + val webContext = new JEEContext(request, response, sessionStore); + val surrogateInSession = sessionStore.get(webContext, Constants.SURROGATE).orElse(null); + if (surrogateInSession != null) { + username = (String) surrogateInSession; + superUsername = userId; + surrogationCall = true; + } else { + username = userId; + superUsername = null; + surrogationCall = false; } } - boolean authToken = true; - // this is not called by the surrogation principal factory, but the surrogation is in progress, it will be called again - // spare the extra info on this call by not requesting the auth token - if (!finalSurrogationCall && superUsername != null) { - LOGGER.debug("No final surrogation call but surrogation in progress -> no auth token"); - authToken = false; - } - - LOGGER.debug("Resolving username: {} | superUsername: {} | authToken: {} | finalSurrogationCall: {}", username, superUsername, authToken, - finalSurrogationCall); + LOGGER.debug("Resolving username: {} | superUsername: {} | surrogationCall: {}", username, superUsername, surrogationCall); - String embedded = null; - if (authToken) { - embedded = AUTH_TOKEN_PARAMETER; - if (finalSurrogationCall) { - embedded += "," + SURROGATION_PARAMETER; - } - else if (requestContext == null) { - embedded += "," + API_PARAMETER; - } - } - else if (finalSurrogationCall) { - embedded = SURROGATION_PARAMETER; + String embedded = AUTH_TOKEN_PARAMETER; + if (surrogationCall) { + embedded += "," + SURROGATION_PARAMETER; + } else if (requestContext == null) { + embedded += "," + API_PARAMETER; } LOGGER.debug("Computed embedded: {}", embedded); final UserDto user = casExternalRestClient.getUserByEmail(utils.buildContext(username), username, Optional.ofNullable(embedded)); @@ -177,7 +171,7 @@ public class UserPrincipalResolver implements PrincipalResolver { LOGGER.debug("User cannot login: {} - User {}", username, user.toString()); return null; } - final Map<String, List<Object>> attributes = new HashMap<>(); + val attributes = new HashMap<String, List<Object>>(); attributes.put(USER_ID_ATTRIBUTE, Collections.singletonList(user.getId())); attributes.put(CUSTOMER_ID_ATTRIBUTE, Collections.singletonList(user.getCustomerId())); attributes.put(EMAIL_ATTRIBUTE, Collections.singletonList(username)); @@ -198,11 +192,11 @@ public class UserPrincipalResolver implements PrincipalResolver { attributes.put(PASSWORD_EXPIRATION_DATE_ATTRIBUTE, Collections.singletonList(user.getPasswordExpirationDate())); attributes.put(GROUP_ID_ATTRIBUTE, Collections.singletonList(user.getGroupId())); attributes.put(ADDRESS_ATTRIBUTE, Collections.singletonList(new CasJsonWrapper(user.getAddress()))); - if (finalSurrogationCall) { + if (surrogationCall) { attributes.put(SUPER_USER_ATTRIBUTE, Collections.singletonList(superUsername)); final UserDto superUser = casExternalRestClient.getUserByEmail(utils.buildContext(superUsername), superUsername, Optional.empty()); attributes.put(SUPER_USER_IDENTIFIER_ATTRIBUTE, Collections.singletonList(superUser.getIdentifier())); - + attributes.put(SUPER_USER_ID_ATTRIBUTE, Collections.singletonList(superUser.getId())); } if (user instanceof AuthUserDto) { final AuthUserDto authUser = (AuthUserDto) user; @@ -222,7 +216,8 @@ public class UserPrincipalResolver implements PrincipalResolver { @Override public boolean supports(final Credential credential) { - return credential instanceof UsernamePasswordCredential || credential instanceof ClientCredential; + return credential instanceof UsernamePasswordCredential || credential instanceof ClientCredential + || credential instanceof SurrogateUsernamePasswordCredential; } @Override diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java index f20fec9411bc2d8e89a1e1ead674baac068c70f1..bbc58298e6f54e7a4a5ce33b1387cc120adc91e5 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java @@ -36,12 +36,11 @@ */ package fr.gouv.vitamui.cas.config; +import fr.gouv.vitamui.cas.authentication.DelegatedSurrogateAuthenticationPostProcessor; import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer; -import org.apereo.cas.authentication.AuthenticationHandler; -import org.apereo.cas.authentication.AuthenticationMetaDataPopulator; -import org.apereo.cas.authentication.AuthenticationPostProcessor; +import org.apereo.cas.authentication.*; import org.apereo.cas.authentication.principal.PrincipalFactory; +import org.apereo.cas.authentication.principal.PrincipalResolver; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.services.ServicesManager; @@ -50,6 +49,7 @@ import org.apereo.cas.ticket.accesstoken.OAuth20AccessTokenFactory; import org.apereo.cas.ticket.accesstoken.OAuth20DefaultAccessToken; import org.apereo.cas.token.JwtBuilder; import org.apereo.cas.util.crypto.CipherExecutor; +import org.pac4j.core.context.session.SessionStore; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -63,8 +63,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; -import fr.gouv.vitamui.cas.authentication.DelegatedSurrogateAuthenticationPostProcessor; -import fr.gouv.vitamui.cas.authentication.SurrogatedUserPrincipalFactory; import fr.gouv.vitamui.cas.authentication.UserAuthenticationHandler; import fr.gouv.vitamui.cas.authentication.UserPrincipalResolver; import fr.gouv.vitamui.cas.provider.ProvidersService; @@ -168,34 +166,40 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Value("${vitamui.cas.identity}") private String casIdentity; + @Autowired + @Qualifier("delegatedClientDistributedSessionStore") + private SessionStore delegatedClientDistributedSessionStore; + @Bean public UserAuthenticationHandler userAuthenticationHandler() { return new UserAuthenticationHandler(servicesManager, principalFactory, casRestClient(), utils(), ipHeaderName); } @Bean - public UserPrincipalResolver userResolver() { - return new UserPrincipalResolver(false, principalFactory, casRestClient(), utils()); - } - - @Bean - public UserPrincipalResolver userResolverForSurrogation() { - return new UserPrincipalResolver(true, principalFactory, casRestClient(), utils()); + @RefreshScope + public PrincipalResolver surrogatePrincipalResolver() { + return new UserPrincipalResolver(principalFactory, casRestClient(), utils(), delegatedClientDistributedSessionStore); } @Bean public AuthenticationEventExecutionPlanConfigurer registerInternalHandler() { - return plan -> plan.registerAuthenticationHandlerWithPrincipalResolver(userAuthenticationHandler(), userResolver()); + return plan -> plan.registerAuthenticationHandlerWithPrincipalResolver(userAuthenticationHandler(), surrogatePrincipalResolver()); } @Bean public AuthenticationEventExecutionPlanConfigurer pac4jAuthenticationEventExecutionPlanConfigurer() { return plan -> { - plan.registerAuthenticationHandlerWithPrincipalResolver(clientAuthenticationHandler, userResolver()); + plan.registerAuthenticationHandlerWithPrincipalResolver(clientAuthenticationHandler, surrogatePrincipalResolver()); plan.registerAuthenticationMetadataPopulator(clientAuthenticationMetaDataPopulator); }; } + @Bean + public AuthenticationPostProcessor surrogateAuthenticationPostProcessor() { + return new DelegatedSurrogateAuthenticationPostProcessor(surrogateAuthenticationService, servicesManager, eventPublisher, + registeredServiceAccessStrategyEnforcer, surrogateEligibilityAuditableExecution, delegatedClientDistributedSessionStore); + } + @Bean public IamExternalRestClientFactory iamRestClientFactory() { LOGGER.debug("Iam client factory: {}", iamClientProperties); @@ -222,17 +226,6 @@ public class AppConfig extends BaseTicketCatalogConfigurer { return new Saml2ClientBuilder(); } - @Bean - public PrincipalFactory surrogatePrincipalFactory() { - return new SurrogatedUserPrincipalFactory(userResolverForSurrogation()); - } - - @Bean - public AuthenticationPostProcessor surrogateAuthenticationPostProcessor() { - return new DelegatedSurrogateAuthenticationPostProcessor(surrogateAuthenticationService, servicesManager, eventPublisher, - registeredServiceAccessStrategyEnforcer, surrogateEligibilityAuditableExecution); - } - @Bean public IdentityProviderHelper identityProviderHelper() { return new IdentityProviderHelper(); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java index 65a538a03173ca366f309c460c40b3d40dd6ed24..5b5607c5d62985b267a0bfa64441214aea4d51fd 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java @@ -193,9 +193,13 @@ public class WebflowConfig { @Autowired private Utils utils; + @Autowired + private DelegatedClientWebflowManager delegatedClientWebflowManager; + @Bean public DispatcherAction dispatcherAction() { - return new DispatcherAction(providersService, identityProviderHelper, casRestClient, surrogationSeparator, utils); + return new DispatcherAction(providersService, identityProviderHelper, casRestClient, + surrogationSeparator, utils, delegatedClientDistributedSessionStore.getObject()); } @Bean @@ -236,7 +240,7 @@ public class WebflowConfig { builtClients.getObject(), servicesManager.getObject(), registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer.getObject(), - delegatedClientWebflowManager(), + delegatedClientWebflowManager, authenticationSystemSupport.getObject(), casProperties, authenticationRequestServiceSelectionStrategies.getObject(), @@ -252,17 +256,6 @@ public class WebflowConfig { surrogationSeparator); } - @RefreshScope - @Bean - public DelegatedClientWebflowManager delegatedClientWebflowManager() { - return new CustomDelegatedClientWebflowManager(ticketRegistry, - ticketFactory, - casProperties, - authenticationRequestServiceSelectionStrategies.getObject(), - argumentExtractor.getObject() - ); - } - @Bean @RefreshScope public Action terminateSessionAction() { diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/CustomDelegatedClientWebflowManager.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/CustomDelegatedClientWebflowManager.java deleted file mode 100644 index 4bea0c2e4e5ae4d491a96d45636c160b9937a5b4..0000000000000000000000000000000000000000 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/CustomDelegatedClientWebflowManager.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) - * and the signatories of the "VITAM - Accord du Contributeur" agreement. - * - * contact@programmevitam.fr - * - * This software is a computer program whose purpose is to implement - * implement a digital archiving front-office system for the secure and - * efficient high volumetry VITAM solution. - * - * This software is governed by the CeCILL-C license under French law and - * abiding by the rules of distribution of free software. You can use, - * modify and/ or redistribute the software under the terms of the CeCILL-C - * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". - * - * As a counterpart to the access to the source code and rights to copy, - * modify and redistribute granted by the license, users are provided only - * with a limited warranty and the software's author, the holder of the - * economic rights, and the successive licensors have only limited - * liability. - * - * In this respect, the user's attention is drawn to the risks associated - * with loading, using, modifying and/or developing or reproducing the - * software by the user in light of its specific status of free software, - * that may mean that it is complicated to manipulate, and that also - * therefore means that it is reserved for developers and experienced - * professionals having in-depth computer knowledge. Users are therefore - * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. - * - * The fact that you are presently reading this means that you have had - * knowledge of the CeCILL-C license and that you accept its terms. - */ -package fr.gouv.vitamui.cas.webflow; - -import fr.gouv.vitamui.cas.util.Constants; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.principal.Service; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.ticket.TicketFactory; -import org.apereo.cas.ticket.TransientSessionTicket; -import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.web.DelegatedClientWebflowManager; -import org.apereo.cas.web.support.ArgumentExtractor; -import org.pac4j.core.context.JEEContext; -import org.pac4j.core.context.WebContext; -import org.springframework.webflow.execution.RequestContext; - -import java.io.Serializable; -import java.util.Map; - -/** - * A webflow manager for authentication delegation which saves/restores the surrogate. - * - * - */ -public class CustomDelegatedClientWebflowManager extends DelegatedClientWebflowManager { - - public CustomDelegatedClientWebflowManager(final TicketRegistry ticketRegistry, - final TicketFactory ticketFactory, - final CasConfigurationProperties casProperties, - final AuthenticationServiceSelectionPlan authenticationRequestServiceSelectionStrategies, - final ArgumentExtractor argumentExtractor) { - super(ticketRegistry, ticketFactory, casProperties, authenticationRequestServiceSelectionStrategies, argumentExtractor); - } - - @Override - protected Map<String, Serializable> buildTicketProperties(final JEEContext webContext) { - final Map<String, Serializable> properties = super.buildTicketProperties(webContext); - - properties.put(Constants.SURROGATE, (String) webContext.getRequestAttribute(Constants.SURROGATE).orElse(null)); - - return properties; - } - - @Override - protected Service restoreDelegatedAuthenticationRequest(final RequestContext requestContext, final WebContext webContext, - final TransientSessionTicket ticket) { - - webContext.setRequestAttribute(Constants.SURROGATE, ticket.getProperties().get(Constants.SURROGATE)); - - return super.restoreDelegatedAuthenticationRequest(requestContext, webContext, ticket); - } -} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java index c90475a1afef1ad60c9716dc86f0e7b1a0e081b5..28a8cbc693f2e147387e2e0898464836385fe82a 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java @@ -43,7 +43,6 @@ import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.commons.api.CommonConstants; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; -import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.CentralAuthenticationService; @@ -66,17 +65,14 @@ import org.apereo.cas.web.support.WebUtils; import org.pac4j.core.client.Clients; import org.pac4j.core.context.JEEContext; import org.pac4j.core.context.session.SessionStore; -import org.pac4j.saml.client.SAML2Client; -import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; -import java.util.Optional; + +import lombok.val; /** * Custom authentication delegation: @@ -141,10 +137,10 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu public Event doExecute(final RequestContext context) { // save a label in the webflow - final MutableAttributeMap<Object> flowScope = context.getFlowScope(); + val flowScope = context.getFlowScope(); flowScope.put(Constants.PORTAL_URL, vitamuiPortalUrl); - final Event event = super.doExecute(context); + val event = super.doExecute(context); if ("error".equals(event.getId())) { // extract and parse the request username if provided @@ -167,13 +163,13 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu } // get the idp if it exists - final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context); - final String idp = getIdpValue(request); + val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context); + val idp = getIdpValue(request); LOGGER.debug("Provided idp: {}", idp); if (StringUtils.isNotBlank(idp)) { TicketGrantingTicket tgt = null; - final String tgtId = WebUtils.getTicketGrantingTicketId(context); + val tgtId = WebUtils.getTicketGrantingTicketId(context); if (tgtId != null) { tgt = ticketRegistry.getTicket(tgtId, TicketGrantingTicket.class); } @@ -182,11 +178,11 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu if (tgt == null || tgt.isExpired()) { // if it matches an existing IdP, save it and redirect - final Optional<IdentityProviderDto> optProvider = identityProviderHelper.findByTechnicalName(providersService.getProviders(), idp); + val optProvider = identityProviderHelper.findByTechnicalName(providersService.getProviders(), idp); if (optProvider.isPresent()) { - final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); + val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); response.addCookie(utils.buildIdpCookie(idp, casProperties.getTgc())); - final SAML2Client client = ((SamlIdentityProviderDto) optProvider.get()).getSaml2Client(); + val client = ((SamlIdentityProviderDto) optProvider.get()).getSaml2Client(); LOGGER.debug("Force redirect to the SAML IdP: {}", client.getName()); try { return utils.performClientRedirection(this, client, context); @@ -207,7 +203,7 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu if (StringUtils.isNotBlank(idp)) { return idp; } - final Cookie cookie = org.springframework.web.util.WebUtils.getCookie(request, CommonConstants.IDP_PARAMETER); + val cookie = org.springframework.web.util.WebUtils.getCookie(request, CommonConstants.IDP_PARAMETER); if (cookie != null) { return cookie.getValue(); } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java index fcbc21509d1e3d50ba5924d97daa7e9059e1bea6..6a51c276feeba46888512611bc14f91ed9dd0898 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java @@ -40,7 +40,6 @@ import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; -import fr.gouv.vitamui.commons.api.domain.UserDto; import fr.gouv.vitamui.commons.api.enums.UserStatusEnum; import fr.gouv.vitamui.commons.api.exception.InvalidFormatException; import fr.gouv.vitamui.commons.api.exception.NotFoundException; @@ -52,14 +51,17 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang.StringUtils; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.web.support.WebUtils; +import org.pac4j.core.context.JEEContext; +import org.pac4j.core.context.session.SessionStore; import org.springframework.webflow.action.AbstractAction; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; -import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Optional; +import lombok.val; + /** * This class can dispatch the user: * - either to the password page @@ -87,11 +89,13 @@ public class DispatcherAction extends AbstractAction { private final Utils utils; + private final SessionStore sessionStore; + @Override protected Event doExecute(final RequestContext requestContext) throws IOException { - final UsernamePasswordCredential credential = WebUtils.getCredential(requestContext, UsernamePasswordCredential.class); - final String username = credential.getUsername().toLowerCase(); + val credential = WebUtils.getCredential(requestContext, UsernamePasswordCredential.class); + val username = credential.getUsername().toLowerCase(); String dispatchedUser = username; String surrogate = null; if (username.contains(surrogationSeparator)) { @@ -106,7 +110,7 @@ public class DispatcherAction extends AbstractAction { // if the user is disabled, send him to a specific page (ignore not found users: it will fail when checking login/password) try { - final UserDto dispatcherUserDto = casExternalRestClient.getUserByEmail(utils.buildContext(dispatchedUser), dispatchedUser, Optional.empty()); + val dispatcherUserDto = casExternalRestClient.getUserByEmail(utils.buildContext(dispatchedUser), dispatchedUser, Optional.empty()); if (dispatcherUserDto != null && dispatcherUserDto.getStatus() != UserStatusEnum.ENABLED) { return userDisabled(dispatchedUser); } @@ -116,7 +120,7 @@ public class DispatcherAction extends AbstractAction { } if (surrogate != null) { try { - final UserDto surrogateDto = casExternalRestClient.getUserByEmail(utils.buildContext(surrogate), surrogate, Optional.empty()); + val surrogateDto = casExternalRestClient.getUserByEmail(utils.buildContext(surrogate), surrogate, Optional.empty()); if (surrogateDto != null && surrogateDto.getStatus() != UserStatusEnum.ENABLED) { LOGGER.error("Bad status for surrogate: {}", surrogate); return userDisabled(surrogate); @@ -127,24 +131,26 @@ public class DispatcherAction extends AbstractAction { } } - final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); + val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); boolean isInternal; - final SamlIdentityProviderDto provider = (SamlIdentityProviderDto) identityProviderHelper.findByUserIdentifier(providersService.getProviders(), dispatchedUser).orElse(null); + val provider = (SamlIdentityProviderDto) identityProviderHelper.findByUserIdentifier(providersService.getProviders(), dispatchedUser).orElse(null); if (provider != null) { isInternal = provider.getInternal(); } else { return new Event(this, BAD_CONFIGURATION); } + val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); + val webContext = new JEEContext(request, response, sessionStore); if (isInternal) { - request.removeAttribute(Constants.SURROGATE); + sessionStore.set(webContext, Constants.SURROGATE, null); LOGGER.debug("Redirect the user to the password page..."); return success(); } else { - // save the surrogate as a request attribute if he exists for the DelegatedClientWebflowManager + // save the surrogate in the session to be retrieved by the UserPrincipalResolver and DelegatedSurrogateAuthenticationPostProcessor if (surrogate != null) { LOGGER.debug("Saving surrogate for after authentication delegation: {}", surrogate); - request.setAttribute(Constants.SURROGATE, surrogate); + sessionStore.set(webContext, Constants.SURROGATE, surrogate); } return utils.performClientRedirection(this, provider.getSaml2Client(), requestContext); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/SelectRedirectAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/SelectRedirectAction.java index 3564f0c25f5a91a42109fc09d84e446c9ad8ab39..ff25f295c28d6bc02a72fd64f2f3030dcee29599 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/SelectRedirectAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/SelectRedirectAction.java @@ -47,6 +47,8 @@ import org.springframework.webflow.action.AbstractAction; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; +import lombok.val; + /** * Select the appropriate redirect action: directly to the service or via the "secure connexion" page. * @@ -63,9 +65,9 @@ public class SelectRedirectAction extends AbstractAction { protected Event doExecute(final RequestContext requestContext) { boolean isFromNewLogin = false; - final String stId = WebUtils.getServiceTicketFromRequestScope(requestContext); + val stId = WebUtils.getServiceTicketFromRequestScope(requestContext); if (stId != null) { - final ServiceTicket st = centralAuthenticationService.getTicket(stId, ServiceTicket.class); + val st = centralAuthenticationService.getTicket(stId, ServiceTicket.class); if (st != null) { isFromNewLogin = st.isFromNewLogin(); } diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationServiceTest.java index d3e227c8c96a223e60c404dede1ba458bc7f1e4d..35b08641b978300935f7adec757a237ebf306577 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateRestAuthenticationServiceTest.java @@ -6,6 +6,7 @@ import fr.gouv.vitamui.commons.rest.client.ExternalHttpContext; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import fr.gouv.vitamui.iam.common.dto.SubrogationDto; import fr.gouv.vitamui.iam.common.enums.SubrogationStatusEnum; +import org.apereo.cas.authentication.principal.DefaultPrincipalFactory; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.services.ServicesManager; import org.junit.Before; @@ -16,10 +17,14 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import static org.mockito.Mockito.*; import static org.junit.Assert.*; +import lombok.val; + /** * Tests {@link IamSurrogateRestAuthenticationService}. * @@ -48,43 +53,48 @@ public final class IamSurrogateRestAuthenticationServiceTest { @Test public void testCanAuthenticateOk() { - when(casExternalRestClient.getSubrogationsBySuperUserId(any(ExternalHttpContext.class), eq(SU_ID))).thenReturn(Arrays.asList(buildSubrogation())); + when(casExternalRestClient.getSubrogationsBySuperUserId(any(ExternalHttpContext.class), eq(SU_ID))).thenReturn(Arrays.asList(surrogation())); - final Principal principal = () -> SU_ID; - assertTrue(service.canAuthenticateAsInternal(SURROGATE, principal, null)); + assertTrue(service.canAuthenticateAsInternal(SURROGATE, principal(), null)); } @Test public void testCanAuthenticateCannotSurrogate() { - final SubrogationDto subrogation = buildSubrogation(); + final SubrogationDto subrogation = surrogation(); subrogation.setSurrogate("anotherUser"); when(casExternalRestClient.getSubrogationsBySuperUserId(any(ExternalHttpContext.class), eq(SU_ID))) .thenReturn(Arrays.asList(subrogation)); - final Principal principal = () -> SU_ID; - assertFalse(service.canAuthenticateAsInternal(SURROGATE, principal, null)); + assertFalse(service.canAuthenticateAsInternal(SURROGATE, principal(), null)); } @Test public void testCanAuthenticateNotAccepted() { - final SubrogationDto subrogation = buildSubrogation(); + final SubrogationDto subrogation = surrogation(); subrogation.setStatus(SubrogationStatusEnum.CREATED); when(casExternalRestClient.getSubrogationsBySuperUserId(any(ExternalHttpContext.class), eq(SU_ID))) .thenReturn(Arrays.asList(subrogation)); - final Principal principal = () -> SU_ID; - assertFalse(service.canAuthenticateAsInternal(SURROGATE, principal, null)); + assertFalse(service.canAuthenticateAsInternal(SURROGATE, principal(), null)); } @Test(expected = UnsupportedOperationException.class) public void testGetAccounts() { when(casExternalRestClient.getSubrogationsBySuperUserEmail(any(ExternalHttpContext.class), eq(SU_EMAIL))) - .thenReturn(Arrays.asList(buildSubrogation())); + .thenReturn(Arrays.asList(surrogation())); service.getEligibleAccountsForSurrogateToProxy(SU_EMAIL); } - private SubrogationDto buildSubrogation() { + private Principal principal() { + val attributes = new HashMap<String, List<Object>>(); + attributes.put(UserPrincipalResolver.SUPER_USER_ID_ATTRIBUTE, Arrays.asList(SU_ID)); + + val factory = new DefaultPrincipalFactory(); + return factory.createPrincipal("x", attributes); + } + + private SubrogationDto surrogation() { final SubrogationDto subrogation = new SubrogationDto(); subrogation.setSurrogate(SURROGATE); subrogation.setSuperUser(SU_EMAIL); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java index 7267736ed479db8beee9c2219e9b37293f3adbd4..a8029130d83b0d35a4277246223b685aecefd548 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java @@ -1,6 +1,8 @@ package fr.gouv.vitamui.cas.authentication; +import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; +import fr.gouv.vitamui.cas.webflow.actions.BaseWebflowActionTest; import fr.gouv.vitamui.commons.api.CommonConstants; import fr.gouv.vitamui.commons.api.domain.AddressDto; import fr.gouv.vitamui.commons.api.domain.GroupDto; @@ -13,19 +15,24 @@ import fr.gouv.vitamui.commons.api.utils.CasJsonWrapper; import fr.gouv.vitamui.commons.rest.client.ExternalHttpContext; import fr.gouv.vitamui.commons.security.client.dto.AuthUserDto; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; +import org.apereo.cas.authentication.SurrogateUsernamePasswordCredential; +import org.apereo.cas.authentication.credential.UsernamePasswordCredential; +import org.apereo.cas.authentication.principal.ClientCredential; import org.apereo.cas.authentication.principal.DefaultPrincipalFactory; import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.authentication.principal.PrincipalFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.pac4j.core.context.JEEContext; +import org.pac4j.core.context.session.SessionStore; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.webflow.execution.RequestContext; -import org.springframework.webflow.execution.RequestContextHolder; import java.util.*; +import static fr.gouv.vitamui.commons.api.CommonConstants.SUPER_USER_ATTRIBUTE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -34,6 +41,8 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import lombok.val; + /** * Tests {@link UserPrincipalResolver}. * @@ -42,11 +51,15 @@ import static org.mockito.Mockito.when; @RunWith(SpringRunner.class) @ContextConfiguration(classes = ServerIdentityAutoConfiguration.class) @TestPropertySource(locations = "classpath:/application-test.properties") -public final class UserPrincipalResolverTest { +public final class UserPrincipalResolverTest extends BaseWebflowActionTest { private static final String USERNAME = "jleleu@test.com"; + private static final String ADMIN = "admin@test.com"; + + private static final String PWD = "password"; - private static final String ID = "1234"; + private static final String USERNAME_ID = "jleleu"; + private static final String ADMIN_ID = "admin"; private static final String ROLE_NAME = "role1"; @@ -54,13 +67,19 @@ public final class UserPrincipalResolverTest { private CasExternalRestClient casExternalRestClient; + private PrincipalFactory principalFactory; + + private SessionStore sessionStore; + @Before public void setUp() { + super.setUp(); + casExternalRestClient = mock(CasExternalRestClient.class); final Utils utils = new Utils(casExternalRestClient, null, 0, null); - resolver = new UserPrincipalResolver(false, new DefaultPrincipalFactory(), casExternalRestClient, utils); - final RequestContext context = mock(RequestContext.class); - RequestContextHolder.setRequestContext(context); + principalFactory = new DefaultPrincipalFactory(); + sessionStore = mock(SessionStore.class); + resolver = new UserPrincipalResolver(principalFactory, casExternalRestClient, utils, sessionStore); } @Test @@ -68,24 +87,84 @@ public final class UserPrincipalResolverTest { when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))).thenReturn(userProfile(UserStatusEnum.ENABLED)); - final Principal principal = resolver.resolve(USERNAME, new HashMap<>()); - assertEquals(ID, principal.getId()); + final Principal principal = resolver.resolve(new UsernamePasswordCredential(USERNAME, PWD), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty()); + + assertEquals(USERNAME_ID, principal.getId()); + final Map<String, List<Object>> attributes = principal.getAttributes(); + assertEquals(USERNAME, attributes.get(CommonConstants.EMAIL_ATTRIBUTE).get(0)); + assertEquals(Arrays.asList(ROLE_NAME), attributes.get(CommonConstants.ROLES_ATTRIBUTE)); + assertNull(attributes.get(SUPER_USER_ATTRIBUTE)); + } + + @Test + public void testResolveAuthnDelegation() { + when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), + eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))).thenReturn(userProfile(UserStatusEnum.ENABLED)); + when(sessionStore.get(any(JEEContext.class), eq(Constants.SURROGATE))).thenReturn(Optional.empty()); + + final Principal principal = resolver.resolve(new ClientCredential(), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty()); + + assertEquals(USERNAME_ID, principal.getId()); final Map<String, List<Object>> attributes = principal.getAttributes(); assertEquals(USERNAME, attributes.get(CommonConstants.EMAIL_ATTRIBUTE).get(0)); assertEquals(Arrays.asList(ROLE_NAME), attributes.get(CommonConstants.ROLES_ATTRIBUTE)); + assertNull(attributes.get(SUPER_USER_ATTRIBUTE)); } - @Test public void testResolveAddressDeserializSuccessfully() { + @Test + public void testResolveSurrogateUser() { + when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), + eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER + "," + CommonConstants.SURROGATION_PARAMETER)))) + .thenReturn(userProfile(UserStatusEnum.ENABLED)); + when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(ADMIN), + eq(Optional.empty()))).thenReturn(adminProfile()); + + val credential = new SurrogateUsernamePasswordCredential(); + credential.setUsername(ADMIN); + credential.setSurrogateUsername(USERNAME); + final Principal principal = resolver.resolve(credential, Optional.of(principalFactory.createPrincipal(ADMIN)), Optional.empty()); + + assertEquals(USERNAME_ID, principal.getId()); + final Map<String, List<Object>> attributes = principal.getAttributes(); + assertEquals(USERNAME, attributes.get(CommonConstants.EMAIL_ATTRIBUTE).get(0)); + assertEquals(Arrays.asList(ROLE_NAME), attributes.get(CommonConstants.ROLES_ATTRIBUTE)); + assertEquals(ADMIN, attributes.get(SUPER_USER_ATTRIBUTE).get(0)); + } + + @Test + public void testResolveAuthnDelegationSurrogate() { + when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), + eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER + "," + CommonConstants.SURROGATION_PARAMETER)))) + .thenReturn(userProfile(UserStatusEnum.ENABLED)); + when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(ADMIN), + eq(Optional.empty()))).thenReturn(adminProfile()); + when(sessionStore.get(any(JEEContext.class), eq(Constants.SURROGATE))).thenReturn(Optional.of(USERNAME)); + + final Principal principal = resolver.resolve(new ClientCredential(), + Optional.of(principalFactory.createPrincipal(ADMIN)), Optional.empty()); + + assertEquals(USERNAME_ID, principal.getId()); + final Map<String, List<Object>> attributes = principal.getAttributes(); + assertEquals(USERNAME, attributes.get(CommonConstants.EMAIL_ATTRIBUTE).get(0)); + assertEquals(Arrays.asList(ROLE_NAME), attributes.get(CommonConstants.ROLES_ATTRIBUTE)); + assertEquals(ADMIN, attributes.get(SUPER_USER_ATTRIBUTE).get(0)); + } + + @Test + public void testResolveAddressDeserializSuccessfully() { AuthUserDto authUserDto = userProfile(UserStatusEnum.ENABLED); when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))) .thenReturn(authUserDto); - final Principal principal = resolver.resolve(USERNAME, new HashMap<>()); + final Principal principal = resolver.resolve(new UsernamePasswordCredential(USERNAME, PWD), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty()); + assertEquals(USERNAME_ID, principal.getId()); AddressDto addressDto = (AddressDto) ((CasJsonWrapper) principal.getAttributes().get(CommonConstants.ADDRESS_ATTRIBUTE).get(0)).getData(); - - assertEquals(ID, principal.getId()); assertThat(addressDto).isEqualToComparingFieldByField(authUserDto.getAddress()); + assertNull(principal.getAttributes().get(SUPER_USER_ATTRIBUTE)); } @Test @@ -93,7 +172,8 @@ public final class UserPrincipalResolverTest { when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))).thenReturn(null); - assertNull(resolver.resolve(USERNAME, new HashMap<>())); + assertNull(resolver.resolve(new UsernamePasswordCredential(USERNAME, PWD), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty())); } @Test @@ -102,7 +182,8 @@ public final class UserPrincipalResolverTest { eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))) .thenReturn(userProfile(UserStatusEnum.DISABLED)); - assertNull(resolver.resolve(USERNAME, new HashMap<>())); + assertNull(resolver.resolve(new UsernamePasswordCredential(USERNAME, PWD), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty())); } @Test @@ -110,12 +191,21 @@ public final class UserPrincipalResolverTest { when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(USERNAME), eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))).thenReturn(userProfile(UserStatusEnum.BLOCKED)); - assertNull(resolver.resolve(USERNAME, new HashMap<>())); + assertNull(resolver.resolve(new UsernamePasswordCredential(USERNAME, PWD), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty())); + } + + private AuthUserDto adminProfile() { + return profile(UserStatusEnum.ENABLED, ADMIN_ID); } private AuthUserDto userProfile(final UserStatusEnum status) { + return profile(status, USERNAME_ID); + } + + private AuthUserDto profile(final UserStatusEnum status, final String id) { final AuthUserDto user = new AuthUserDto(); - user.setId(ID); + user.setId(id); user.setStatus(status); user.setType(UserTypeEnum.NOMINATIVE); AddressDto address = new AddressDto(); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/BaseWebflowActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/BaseWebflowActionTest.java index ae75f1e3d45938f86572ed853670196e779ebba0..9d414610d6698caed26b021c6c6fae52372492af 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/BaseWebflowActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/BaseWebflowActionTest.java @@ -15,6 +15,7 @@ import org.springframework.webflow.engine.FlowVariable; import org.springframework.webflow.execution.FlowExecutionContext; import org.springframework.webflow.execution.FlowSession; import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.RequestContextHolder; import org.springframework.webflow.test.MockParameterMap; import javax.servlet.http.HttpServletRequest; @@ -82,5 +83,7 @@ public abstract class BaseWebflowActionTest { final FlowSession flowSession = mock(FlowSession.class); when(flowContext.getActiveSession()).thenReturn(flowSession); when(flowSession.getScope()).thenReturn(new LocalAttributeMap<>()); + + RequestContextHolder.setRequestContext(context); } } diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java index c6e8dbbeffcd83de360117ca40ab38e1277fa4e0..d7745918419651ce9ff3fefc1b1189a9b47a5008 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java @@ -17,6 +17,7 @@ import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.pac4j.core.context.session.SessionStore; import org.pac4j.saml.client.SAML2Client; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @@ -65,7 +66,7 @@ public final class DispatcherActionTest extends BaseWebflowActionTest { casExternalRestClient = mock(CasExternalRestClient.class); final Utils utils = new Utils(casExternalRestClient, null, 0, null); - action = new DispatcherAction(providersService, identityProviderHelper, casExternalRestClient, ",", utils); + action = new DispatcherAction(providersService, identityProviderHelper, casExternalRestClient, ",", utils, mock(SessionStore.class)); final SAML2Client client = new SAML2Client(); provider = new SamlIdentityProviderDto(new IdentityProviderDto(), client);