Compare commits

..

9 Commits

Author SHA1 Message Date
cezary-witkowski
0e5b66c637 [ACS-9167] Clear more caches 2025-03-21 17:11:38 +01:00
cezary-witkowski
0c17a1c617 [ACS-9167] Improved integration test 2025-03-21 16:01:10 +01:00
cezary-witkowski
87ecfc9290 [ACS-9167] Header years 2025-03-21 14:50:45 +01:00
cezary-witkowski
520e06dd49 [ACS-9167] Added integration tests 2025-03-21 14:50:07 +01:00
cezary-witkowski
f271697a8e [ACS-9167] Fix bugs found in testing 2025-03-21 14:49:16 +01:00
cezary-witkowski
e106502363 [ACS-9167] PMD fix 2025-03-20 12:08:12 +01:00
cezary-witkowski
8811a73a8d [ACS-9167] Updated header year 2025-03-20 11:53:59 +01:00
cezary-witkowski
44bca1d416 [ACS-9167] Adding missing mappings 2025-03-19 16:34:02 +01:00
cezary-witkowski
3b476670e0 [ACS-9167] Added second query to count all nodes matching the query to fix pagination having incorrect totalItems which in turn breaks pagination on frontend when using smart folders. 2025-03-19 15:01:05 +01:00
59 changed files with 544 additions and 915 deletions

View File

@@ -147,7 +147,7 @@ jobs:
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
- uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- uses: Alfresco/ya-pmd-scan@v4.3.0
- uses: Alfresco/ya-pmd-scan@v4.1.0
with:
classpath-build-command: "mvn test-compile -ntp -Pags -pl \"-:alfresco-community-repo-docker\""

View File

@@ -1607,7 +1607,7 @@
"filename": "repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 48,
"line_number": 47,
"is_secret": false
}
],
@@ -1868,5 +1868,5 @@
}
]
},
"generated_at": "2025-03-21T13:01:19Z"
"generated_at": "2025-03-17T14:00:53Z"
}

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-automation-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<build>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<modules>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<build>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<modules>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -9,6 +9,6 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
</project>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<modules>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<organization>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<developers>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<developers>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<properties>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<developers>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<properties>

View File

@@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name>
@@ -153,7 +153,7 @@
<connection>scm:git:https://github.com/Alfresco/alfresco-community-repo.git</connection>
<developerConnection>scm:git:https://github.com/Alfresco/alfresco-community-repo.git</developerConnection>
<url>https://github.com/Alfresco/alfresco-community-repo</url>
<tag>25.2.0.4</tag>
<tag>HEAD</tag>
</scm>
<distributionManagement>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.4</version>
<version>25.2.0.3-SNAPSHOT</version>
</parent>
<dependencies>

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -90,6 +90,8 @@ public class DBQueryEngine implements QueryEngine
protected static final Log logger = LogFactory.getLog(DBQueryEngine.class);
protected static final String SELECT_BY_DYNAMIC_QUERY = "alfresco.metadata.query.select_byDynamicQuery";
protected static final String COUNT_BY_DYNAMIC_QUERY = "alfresco.metadata.query.count_byDynamicQuery";
protected static final QueryTemplate QUERY_TEMPLATE = new QueryTemplate(SELECT_BY_DYNAMIC_QUERY, COUNT_BY_DYNAMIC_QUERY);
private static final int DEFAULT_MIN_PAGING_BATCH_SIZE = 2500;
@@ -252,7 +254,7 @@ public class DBQueryEngine implements QueryEngine
}
/* (non-Javadoc)
*
*
* @see org.alfresco.repo.search.impl.querymodel.QueryEngine#executeQuery(org.alfresco.repo.search.impl.querymodel.Query, org.alfresco.repo.search.impl.querymodel.QueryOptions, org.alfresco.repo.search.impl.querymodel.FunctionEvaluationContext) */
@Override
public QueryEngineResults executeQuery(Query query, QueryOptions options, FunctionEvaluationContext functionContext)
@@ -331,10 +333,10 @@ public class DBQueryEngine implements QueryEngine
return asQueryEngineResults(resultSet);
}
protected String pickQueryTemplate(QueryOptions options, DBQuery dbQuery)
protected QueryTemplate pickQueryTemplate(QueryOptions options, DBQuery dbQuery)
{
logger.debug("- using standard table for the query");
return SELECT_BY_DYNAMIC_QUERY;
return QUERY_TEMPLATE;
}
private ResultSet selectNodesWithPermissions(QueryOptions options, DBQuery dbQuery)
@@ -370,7 +372,8 @@ public class DBQueryEngine implements QueryEngine
int requiredNodes = computeRequiredNodesCount(options);
logger.debug("- query sent to the database");
performTmdqSelect(pickQueryTemplate(options, dbQuery), dbQuery, requiredNodes, new ResultHandler<Node>() {
QueryTemplate queryTemplate = pickQueryTemplate(options, dbQuery);
performTmdqSelect(queryTemplate, dbQuery, requiredNodes, new ResultHandler<Node>() {
@Override
public void handleResult(ResultContext<? extends Node> context)
{
@@ -425,8 +428,7 @@ public class DBQueryEngine implements QueryEngine
}
}
});
int numberFound = nodes.size();
int numberFound = countSelectedNodes(queryTemplate, dbQuery);
nodes.removeAll(Collections.singleton(null));
DBResultSet rs = createResultSet(options, nodes, numberFound);
@@ -437,8 +439,17 @@ public class DBQueryEngine implements QueryEngine
return frs;
}
private void performTmdqSelect(String statement, DBQuery dbQuery, int requiredNodes, ResultHandler<Node> handler)
protected int countSelectedNodes(QueryTemplate queryTemplate, DBQuery dbQuery)
{
dbQuery.setLimit(0);
dbQuery.setOffset(0);
String countQuery = queryTemplate.count();
return template.selectOne(countQuery, dbQuery);
}
private void performTmdqSelect(QueryTemplate queryTemplate, DBQuery dbQuery, int requiredNodes, ResultHandler<Node> handler)
{
String statement = queryTemplate.select();
if (usePagingQuery)
{
performTmdqSelectPaging(statement, dbQuery, requiredNodes, handler);
@@ -457,8 +468,7 @@ public class DBQueryEngine implements QueryEngine
private void performTmdqSelectPaging(String statement, DBQuery dbQuery, int requiredNodes, ResultHandler<Node> handler)
{
int batchStart = 0;
int batchSize = requiredNodes * 2;
batchSize = Math.min(Math.max(batchSize, minPagingBatchSize), maxPagingBatchSize);
int batchSize = calculateBatchSize(requiredNodes);
DefaultResultContext<Node> resultCtx = new DefaultResultContext<>();
while (!resultCtx.isStopped())
{
@@ -485,6 +495,21 @@ public class DBQueryEngine implements QueryEngine
}
}
private int calculateBatchSize(int requiredNodes)
{
int batchSize;
if (requiredNodes > Integer.MAX_VALUE / 2)
{
// preventing overflow
batchSize = Integer.MAX_VALUE;
}
else
{
batchSize = requiredNodes * 2;
}
return Math.min(Math.max(batchSize, minPagingBatchSize), maxPagingBatchSize);
}
private DBResultSet createResultSet(QueryOptions options, List<Node> nodes, int numberFound)
{
DBResultSet dbResultSet = new DBResultSet(options.getAsSearchParmeters(), nodes, nodeDAO, nodeService, tenantService, Integer.MAX_VALUE);
@@ -524,7 +549,7 @@ public class DBQueryEngine implements QueryEngine
}
/* (non-Javadoc)
*
*
* @see org.alfresco.repo.search.impl.querymodel.QueryEngine#getQueryModelFactory() */
@Override
public QueryModelFactory getQueryModelFactory()
@@ -534,7 +559,7 @@ public class DBQueryEngine implements QueryEngine
/**
* Injection of nodes cache for clean-up and warm up when required
*
*
* @param cache
* The node cache to set
*/
@@ -588,4 +613,7 @@ public class DBQueryEngine implements QueryEngine
}
}
}
protected record QueryTemplate(String select, String count)
{}
}

View File

@@ -33,7 +33,6 @@ import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
/**
*

View File

@@ -69,13 +69,6 @@ public class IdentityServiceConfig
private boolean clientIdValidationDisabled;
private String adminConsoleRedirectPath;
private String signatureAlgorithms;
private String adminConsoleScopes;
private String passwordGrantScopes;
private String issuerAttribute;
private String firstNameAttribute;
private String lastNameAttribute;
private String emailAttribute;
private long jwtClockSkewMs;
/**
*
@@ -336,78 +329,4 @@ public class IdentityServiceConfig
{
this.signatureAlgorithms = signatureAlgorithms;
}
public String getIssuerAttribute()
{
return issuerAttribute;
}
public void setIssuerAttribute(String issuerAttribute)
{
this.issuerAttribute = issuerAttribute;
}
public Set<String> getAdminConsoleScopes()
{
return Stream.of(adminConsoleScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setAdminConsoleScopes(String adminConsoleScopes)
{
this.adminConsoleScopes = adminConsoleScopes;
}
public Set<String> getPasswordGrantScopes()
{
return Stream.of(passwordGrantScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setPasswordGrantScopes(String passwordGrantScopes)
{
this.passwordGrantScopes = passwordGrantScopes;
}
public void setFirstNameAttribute(String firstNameAttribute)
{
this.firstNameAttribute = firstNameAttribute;
}
public void setLastNameAttribute(String lastNameAttribute)
{
this.lastNameAttribute = lastNameAttribute;
}
public void setEmailAttribute(String emailAttribute)
{
this.emailAttribute = emailAttribute;
}
public void setJwtClockSkewMs(long jwtClockSkewMs)
{
this.jwtClockSkewMs = jwtClockSkewMs;
}
public String getFirstNameAttribute()
{
return firstNameAttribute;
}
public String getLastNameAttribute()
{
return lastNameAttribute;
}
public String getEmailAttribute()
{
return emailAttribute;
}
public long getJwtClockSkewMs()
{
return jwtClockSkewMs;
}
}

View File

@@ -34,9 +34,6 @@ import java.util.Optional;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
/**
* Allows to interact with the Identity Service
*/
@@ -69,11 +66,11 @@ public interface IdentityServiceFacade
*
* @param token
* {@link String} with encoded access token value.
* @param userInfoAttrMapping
* {@link UserInfoAttrMapping} containing the mapping of claims.
* @return {@link DecodedTokenUser} containing user claims or {@link Optional#empty()} if the token does not contain a username claim.
* @param principalAttribute
* {@link String} the attribute name used to access the user's name from the user info response.
* @return {@link OIDCUserInfo} containing user claims.
*/
Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping);
Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute);
/**
* Gets a client registration

View File

@@ -72,7 +72,6 @@ import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
@@ -97,7 +96,6 @@ import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
@@ -126,8 +124,6 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
/**
* Creates an instance of {@link IdentityServiceFacade}. <br>
@@ -138,7 +134,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
private static final String DEFAULT_ISSUER_ATTR = "issuer";
private boolean enabled;
private SpringBasedIdentityServiceFacadeFactory factory;
@@ -211,9 +206,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
@Override
public Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
public Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute)
{
return getTargetFacade().getUserInfo(token, userInfoAttrMapping);
return getTargetFacade().getUserInfo(token, principalAttribute);
}
@Override
@@ -282,7 +277,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
{
final RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter(), new MappingJackson2HttpMessageConverter()));
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
@@ -390,6 +385,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{
private final IdentityServiceConfig config;
private static final Set<String> SCOPES = Set.of("openid", "profile", "email");
ClientRegistrationProvider(IdentityServiceConfig config)
{
this.config = requireNonNull(config);
@@ -459,12 +456,11 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
.map(URI::toASCIIString)
.orElse(null);
var metadataIssuer = getMetadataIssuer(metadata, config);
final String issuerUri = metadataIssuer
final String issuerUri = Optional.of(metadata)
.map(OIDCProviderMetadata::getIssuer)
.map(Issuer::getValue)
.orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ? config.getAuthServerUrl() : config.getIssuerUrl());
final var usernameAttribute = StringUtils.isNotBlank(config.getPrincipalAttribute()) ? config.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
return ClientRegistration
.withRegistrationId("ids")
.authorizationUri(authUri)
@@ -472,7 +468,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.issuerUri(issuerUri)
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
.userNameAttributeName(usernameAttribute)
.scope(getSupportedScopes(metadata.getScopes()))
.providerConfigurationMetadata(createMetadata(metadata))
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
@@ -506,17 +501,11 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private Set<String> getSupportedScopes(Scope scopes)
{
return scopes.stream()
.filter(this::hasPasswordGrantScope)
return scopes.stream().filter(scope -> SCOPES.contains(scope.getValue()))
.map(Identifier::getValue)
.collect(Collectors.toSet());
}
private boolean hasPasswordGrantScope(Scope.Value scope)
{
return config.getPasswordGrantScopes().contains(scope.getValue());
}
private Optional<OIDCProviderMetadata> extractMetadata(RestOperations rest, URI metadataUri)
{
final String response;
@@ -563,18 +552,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
}
private static Optional<String> getMetadataIssuer(OIDCProviderMetadata metadata, IdentityServiceConfig config)
{
return DEFAULT_ISSUER_ATTR.equals(config.getIssuerAttribute()) ? Optional.of(metadata)
.map(OIDCProviderMetadata::getIssuer)
.map(Issuer::getValue)
: Optional.of(metadata)
.map(OIDCProviderMetadata::getCustomParameters)
.map(map -> map.get(config.getIssuerAttribute()))
.filter(String.class::isInstance)
.map(String.class::cast);
}
static class JwtDecoderProvider
{
private static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
@@ -674,7 +651,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
{
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator(Duration.of(config.getJwtClockSkewMs(), ChronoUnit.MILLIS)));
validators.add(new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)));
validators.add(new JwtIssuerValidator(providerDetails.getIssuerUri()));
if (!config.isClientIdValidationDisabled())
{

View File

@@ -30,33 +30,52 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapper;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapper;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
/**
* This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository.
* 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 IdentityServiceConfig identityServiceConfig;
private final IdentityServiceFacade identityServiceFacade;
private final PersonService personService;
private final TransactionService transactionService;
private final IdentityServiceConfig identityServiceConfig;
private UserInfoAttrMapping userInfoAttrMapping;
private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
private final BiFunction<DecodedAccessToken, String, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = (token, usernameMappingClaim) -> {
Optional<String> firstName = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> lastName = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> email = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
return Optional.ofNullable(token.getClaim(Optional.ofNullable(usernameMappingClaim)
.filter(StringUtils::isNotBlank)
.orElse(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,
@@ -69,95 +88,92 @@ public class IdentityServiceJITProvisioningHandler
this.identityServiceConfig = identityServiceConfig;
}
/**
* Extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository. Call to the UserInfo endpoint is made only if the token does not contain a username claim or if user needs to be created and some of the {@link OIDCUserInfo} fields are empty.
*/
public Optional<OIDCUserInfo> extractUserInfoAndCreateUserIfNeeded(String bearerToken)
{
if (userInfoAttrMapping == null)
{
initMappers(identityServiceConfig);
}
Optional<OIDCUserInfo> oidcUserInfo = Optional.ofNullable(bearerToken)
Optional<OIDCUserInfo> userInfoResponse = Optional.ofNullable(bearerToken)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> extractUserInfoResponseFromAccessToken(token).filter(decodedTokenUser -> StringUtils.isNotEmpty(decodedTokenUser.username()))
.or(() -> extractUserInfoResponseFromEndpoint(token, userInfoAttrMapping)))
.map(tokenUserToOIDCUserMapper::toOIDCUser);
.flatMap(token -> extractUserInfoResponseFromAccessToken(token)
.filter(userInfo -> StringUtils.isNotEmpty(userInfo.username()))
.or(() -> extractUserInfoResponseFromEndpoint(token)));
if (transactionService.isReadOnly() || oidcUserInfo.isEmpty())
if (transactionService.isReadOnly() || userInfoResponse.isEmpty())
{
return oidcUserInfo;
return userInfoResponse;
}
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<>() {
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Optional<OIDCUserInfo>>() {
@Override
public Optional<OIDCUserInfo> doWork() throws Exception
{
return oidcUserInfo.map(oidcUser -> {
if (userDoesNotExistsAndCanBeCreated(oidcUser))
return userInfoResponse.map(userInfo -> {
if (userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username()))
{
if (!oidcUser.allFieldsNotEmpty())
if (!userInfo.allFieldsNotEmpty())
{
oidcUser = extractUserInfoResponseFromEndpoint(bearerToken, userInfoAttrMapping)
.map(tokenUserToOIDCUserMapper::toOIDCUser)
.orElse(oidcUser);
userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo);
}
createPerson(oidcUser);
Map<QName, Serializable> 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 oidcUser;
return userInfo;
});
}
}, AuthenticationUtil.getSystemUserName());
}
private void initMappers(IdentityServiceConfig identityServiceConfig)
{
this.userInfoAttrMapping = initUserInfoAttrMapping(identityServiceConfig);
this.tokenUserToOIDCUserMapper = new TokenUserToOIDCUserMapper(personService);
this.tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
}
private boolean userDoesNotExistsAndCanBeCreated(OIDCUserInfo userInfo)
{
return userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username());
}
private Optional<DecodedTokenUser> extractUserInfoResponseFromAccessToken(String bearerToken)
private Optional<OIDCUserInfo> extractUserInfoResponseFromAccessToken(String bearerToken)
{
return Optional.ofNullable(bearerToken)
.map(identityServiceFacade::decodeToken)
.flatMap(tokenToDecodedTokenUserMapper::toDecodedTokenUser);
.flatMap(decodedToken -> mapTokenToUserInfoResponse.apply(decodedToken,
identityServiceConfig.getPrincipalAttribute()));
}
private Optional<DecodedTokenUser> extractUserInfoResponseFromEndpoint(String bearerToken, UserInfoAttrMapping userInfoAttrMapping)
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
{
return identityServiceFacade.getUserInfo(bearerToken, userInfoAttrMapping)
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty());
return identityServiceFacade.getUserInfo(bearerToken,
StringUtils.isNotBlank(identityServiceConfig.getPrincipalAttribute()) ? identityServiceConfig.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)
.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("")));
}
private void createPerson(OIDCUserInfo userInfo)
/**
* 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)
{
Map<QName, Serializable> 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
if (userId == null)
{
return null;
}
personService.createPerson(properties);
String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>() {
@Override
public String doWork() throws Exception
{
return personService.getUserIdentifier(userId);
}
}, AuthenticationUtil.getSystemUserName());
return normalized == null ? userId : normalized;
}
private UserInfoAttrMapping initUserInfoAttrMapping(IdentityServiceConfig identityServiceConfig)
{
return new UserInfoAttrMapping(identityServiceFacade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
identityServiceConfig.getFirstNameAttribute(),
identityServiceConfig.getLastNameAttribute(),
identityServiceConfig.getEmailAttribute());
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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
@@ -38,7 +38,6 @@ import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
/**
* A {@link RemoteUserMapper} implementation that detects and validates JWTs issued by the Alfresco Identity Service.

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
package org.alfresco.repo.security.authentication.identityservice;
import java.util.stream.Stream;

View File

@@ -30,12 +30,21 @@ import static java.util.Objects.requireNonNull;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
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.openid.connect.sdk.claims.PersonClaims;
import org.apache.commons.lang3.StringUtils;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
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;
import org.springframework.core.convert.converter.Converter;
@@ -50,35 +59,27 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRe
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
{
private static final Log LOGGER = LogFactory.getLog(SpringBasedIdentityServiceFacade.class);
private static final Instant SOME_INSIGNIFICANT_DATE_IN_THE_PAST = Instant.MIN.plusSeconds(12345);
private final Map<AuthorizationGrantType, OAuth2AccessTokenResponseClient> clients;
private final DefaultOAuth2UserService defaultOAuth2UserService;
private final ClientRegistration clientRegistration;
private final JwtDecoder jwtDecoder;
@@ -92,7 +93,6 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations, clientRegistration));
this.defaultOAuth2UserService = createOAuth2UserService(restOperations);
}
@Override
@@ -121,18 +121,51 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
}
@Override
public Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter, String principalAttribute)
{
try
{
return Optional.ofNullable(defaultOAuth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, getSpringAccessToken(token))))
.flatMap(oAuth2User -> mapOAuth2UserToDecodedTokenUser(oAuth2User, userInfoAttrMapping));
}
catch (OAuth2AuthenticationException exception)
{
LOGGER.warn("User Info Request failed: " + exception.getMessage());
return Optional.empty();
}
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
{
UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
if (userInfoResponse instanceof UserInfoErrorResponse userInfoErrorResponse)
{
String errorMessage = Optional.ofNullable(userInfoErrorResponse.getErrorObject())
.map(ErrorObject::getDescription)
.orElse("No error description found");
LOGGER.warn("User Info Request failed: " + errorMessage);
throw new UserInfoException(errorMessage);
}
return Optional.of(userInfoResponse);
}
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.getStringClaim(principalAttribute), userInfo.getGivenName(),
userInfo.getFamilyName(), userInfo.getEmailAddress()));
}
@Override
@@ -169,7 +202,11 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
if (grant.isRefreshToken())
{
final OAuth2AccessToken expiredAccessToken = getSpringAccessToken("JUST_FOR_FULFILLING_THE_SPRING_API");
final OAuth2AccessToken expiredAccessToken = new OAuth2AccessToken(
TokenType.BEARER,
"JUST_FOR_FULFILLING_THE_SPRING_API",
SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken,
@@ -221,26 +258,6 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
return client;
}
private static DefaultOAuth2UserService createOAuth2UserService(RestOperations rest)
{
final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
userService.setRestOperations(rest);
return userService;
}
private Optional<DecodedTokenUser> mapOAuth2UserToDecodedTokenUser(OAuth2User oAuth2User, UserInfoAttrMapping userInfoAttrMapping)
{
var preferredUsername = Optional.ofNullable(oAuth2User.getAttribute(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast)
.filter(StringUtils::isNotEmpty);
var userName = Optional.ofNullable(oAuth2User.getName()).filter(username -> !username.isEmpty()).or(() -> preferredUsername);
return userName.map(name -> DecodedTokenUser.validateAndCreate(name,
oAuth2User.getAttribute(userInfoAttrMapping.firstNameClaim()),
oAuth2User.getAttribute(userInfoAttrMapping.lastNameClaim()),
oAuth2User.getAttribute(userInfoAttrMapping.emailClaim())));
}
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest,
ClientRegistration clientRegistration)
{
@@ -271,16 +288,6 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
};
}
private static OAuth2AccessToken getSpringAccessToken(String token)
{
// Just for fulfilling the Spring API
return new OAuth2AccessToken(
TokenType.BEARER,
token,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
}
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
{
private final OAuth2AccessTokenResponse tokenResponse;

View File

@@ -70,6 +70,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
private static final Set<String> SCOPES = Set.of("openid", "profile", "email", "offline_access");
private IdentityServiceConfig identityServiceConfig;
private IdentityServiceFacade identityServiceFacade;
@@ -224,16 +225,11 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set<String> getSupportedScopes(Scope scopes)
{
return scopes.stream()
.filter(this::hasAdminConsoleScope)
.filter(scope -> SCOPES.contains(scope.getValue()))
.map(Identifier::getValue)
.collect(Collectors.toSet());
}
private boolean hasAdminConsoleScope(Scope.Value scope)
{
return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue());
}
private String getRedirectUri(String requestURL)
{
try

View File

@@ -1,66 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
public class AccessTokenToDecodedTokenUserMapper
{
private static final String DEFAULT_USERNAME_CLAIM = PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
private final UserInfoAttrMapping userInfoAttrMapping;
public AccessTokenToDecodedTokenUserMapper(UserInfoAttrMapping userInfoAttrMapping)
{
this.userInfoAttrMapping = userInfoAttrMapping;
}
/**
* Maps the given {@link IdentityServiceFacade.DecodedAccessToken} to a {@link DecodedTokenUser}.
*
* @param token
* the token to map
* @return the mapped {@link DecodedTokenUser} or {@link Optional#empty()} if the token does not contain a username claim
*/
public Optional<DecodedTokenUser> toDecodedTokenUser(IdentityServiceFacade.DecodedAccessToken token)
{
Object firstName = token.getClaim(userInfoAttrMapping.firstNameClaim());
Object lastName = token.getClaim(userInfoAttrMapping.lastNameClaim());
Object email = token.getClaim(userInfoAttrMapping.emailClaim());
return Optional.ofNullable(token.getClaim(Optional.ofNullable(userInfoAttrMapping.usernameClaim())
.filter(StringUtils::isNotBlank)
.orElse(DEFAULT_USERNAME_CLAIM)))
.filter(String.class::isInstance)
.map(String.class::cast)
.map(username -> DecodedTokenUser.validateAndCreate(username, firstName, lastName, email));
}
}

View File

@@ -1,44 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
import java.util.Optional;
public record DecodedTokenUser(String username, String firstName, String lastName, String email)
{
private static final String EMPTY_STRING = "";
public static DecodedTokenUser validateAndCreate(String username, Object firstName, Object lastName, Object email)
{
return new DecodedTokenUser(username, getStringVal(firstName), getStringVal(lastName), getStringVal(email));
}
private static String getStringVal(Object firstName)
{
return Optional.ofNullable(firstName).filter(String.class::isInstance).map(String.class::cast).orElse(EMPTY_STRING);
}
}

View File

@@ -1,76 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.cmr.security.PersonService;
public class TokenUserToOIDCUserMapper
{
private final PersonService personService;
public TokenUserToOIDCUserMapper(PersonService personService)
{
this.personService = personService;
}
/**
* Maps a decoded token user to an OIDC user where the user id (username) is normalized.
*
* @param decodedTokenUser
* the decoded token user
* @return the OIDC user
*/
public OIDCUserInfo toOIDCUser(DecodedTokenUser decodedTokenUser)
{
return new OIDCUserInfo(usernameToUserId(decodedTokenUser.username()), decodedTokenUser.firstName(), decodedTokenUser.lastName(), decodedTokenUser.email());
}
/**
* Normalizes a username, taking into account existing user accounts and case sensitivity settings.
*
* @param caseSensitiveUserName
* the case-sensitive username
* @return the string
*/
private String usernameToUserId(final String caseSensitiveUserName)
{
if (caseSensitiveUserName == null)
{
return null;
}
String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>() {
@Override
public String doWork() throws Exception
{
return personService.getUserIdentifier(caseSensitiveUserName);
}
}, AuthenticationUtil.getSystemUserName());
return normalized == null ? caseSensitiveUserName : normalized;
}
}

View File

@@ -1,41 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
/**
* The UserInfoAttrMapping record represents the mapping of claims fetched from the UserInfo endpoint to create an Alfresco user.
*
* @param usernameClaim
* the claim that represents the username
* @param firstNameClaim
* the claim that represents the first name
* @param lastNameClaim
* the claim that represents the last name
* @param emailClaim
* the claim that represents the email
*/
public record UserInfoAttrMapping(String usernameClaim, String firstNameClaim, String lastNameClaim, String emailClaim)
{}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -203,9 +203,8 @@ public class VirtualQueryImpl implements VirtualQuery
start = 0;
}
}
final int totlaSecond = !hasMore ? (int) result.getNumberFound() : (int) (start + result.getNumberFound() + 1);
final Pair<Integer, Integer> total = new Pair<Integer, Integer>(totalFirst,
totlaSecond);
final int totalSecond = !hasMore ? (int) result.getNumberFound() : (int) (start + result.getNumberFound());
final Pair<Integer, Integer> total = new Pair<>(totalFirst, totalSecond);
return new PagingResults<Reference>() {
@Override

View File

@@ -8,4 +8,10 @@
<include refid="sql_select_byDynamicQuery"/>
</select>
</mapper>
<select id="count_byDynamicQuery" parameterType="org.alfresco.repo.search.impl.querymodel.impl.db.DBQuery" resultType="int">
SELECT COUNT(DISTINCT nodes.id)
FROM (
<include refid="sql_select_byDynamicQuery"/>
) nodes
</select>
</mapper>

View File

@@ -8,4 +8,10 @@
<include refid="sql_select_byDynamicQuery"/>
</select>
</mapper>
<select id="count_byDynamicQuery" parameterType="org.alfresco.repo.search.impl.querymodel.impl.db.DBQuery" resultType="int">
SELECT COUNT(DISTINCT nodes.id)
FROM (
<include refid="sql_select_byDynamicQuery"/>
) nodes
</select>
</mapper>

View File

@@ -149,15 +149,6 @@
<property name="principalAttribute">
<value>${identity-service.principal-attribute:preferred_username}</value>
</property>
<property name="firstNameAttribute">
<value>${identity-service.first-name-attribute:given_name}</value>
</property>
<property name="lastNameAttribute">
<value>${identity-service.last-name-attribute:family_name}</value>
</property>
<property name="emailAttribute">
<value>${identity-service.email-attribute:email}</value>
</property>
<property name="clientIdValidationDisabled">
<value>${identity-service.client-id.validation.disabled:true}</value>
</property>
@@ -167,18 +158,6 @@
<property name="signatureAlgorithms">
<value>${identity-service.signature-algorithms:RS256,PS256}</value>
</property>
<property name="adminConsoleScopes">
<value>${identity-service.admin-console.scopes:openid,profile,email,offline_access}</value>
</property>
<property name="passwordGrantScopes">
<value>${identity-service.password-grant.scopes:openid,profile,email}</value>
</property>
<property name="issuerAttribute">
<value>${identity-service.issuer-attribute:issuer}</value>
</property>
<property name="jwtClockSkewMs">
<value>${identity-service.jwt-clock-skew-ms:0}</value>
</property>
</bean>
<!-- Enable control over mapping between request and user ID -->
@@ -240,4 +219,4 @@
<ref bean="transactionService" />
</property>
</bean>
</beans>
</beans>

View File

@@ -12,11 +12,4 @@ identity-service.resource=alfresco
identity-service.credentials.secret=
identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
identity-service.signature-algorithms=RS256,PS256
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.admin-console.scopes=openid,profile,email,offline_access
identity-service.password-grant.scopes=openid,profile,email
identity-service.issuer-attribute=issuer
identity-service.jwt-clock-skew-ms=0
identity-service.signature-algorithms=RS256,PS256

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -80,6 +80,7 @@ import org.alfresco.util.testing.category.NonBuildTests;
// ACS-1907
org.alfresco.repo.search.impl.querymodel.impl.db.ACS1907Test.class,
org.alfresco.repo.search.impl.querymodel.impl.db.ACS9167Test.class,
// REPO-2963 : Tests causing a cascade of failures in AllDBTestsTestSuite on PostgreSQL/MySQL
// Moved at the bottom of the suite because DbNodeServiceImplTest.testNodeCleanupRegistry() takes a long time on a clean DB.

View File

@@ -37,8 +37,6 @@ import org.alfresco.repo.security.authentication.identityservice.SpringBasedIden
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest;
import org.alfresco.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
@@ -151,8 +149,6 @@ import org.alfresco.util.testing.category.NonBuildTests;
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
SpringBasedIdentityServiceFacadeUnitTest.class,
IdentityServiceJITProvisioningHandlerUnitTest.class,
AccessTokenToDecodedTokenUserMapperUnitTest.class,
TokenUserToOIDCUserMapperUnitTest.class,
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,

View File

@@ -0,0 +1,216 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.search.impl.querymodel.impl.db;
import static org.junit.Assert.assertEquals;
import java.io.Serializable;
import java.time.Duration;
import java.util.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.springframework.context.ApplicationContext;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.TransactionalCache;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.QueryConsistency;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.testing.category.DBTests;
@Category({OwnJVMTestsCategory.class, DBTests.class})
@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"})
public class ACS9167Test
{
private NodeService nodeService;
private AuthenticationComponent authenticationComponent;
private SearchService pubSearchService;
private TransactionService transactionService;
private RetryingTransactionHelper txnHelper;
@Before
public void setUp() throws Exception
{
setupServices();
txnHelper = new RetryingTransactionHelper();
txnHelper.setTransactionService(transactionService);
txnHelper.setReadOnly(false);
txnHelper.setMaxRetries(1);
authenticationComponent.setSystemUserAsCurrentUser();
}
private void setupServices()
{
ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
nodeService = (NodeService) ctx.getBean("dbNodeService");
authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent");
pubSearchService = (SearchService) ctx.getBean("SearchService");
transactionService = (TransactionService) ctx.getBean("TransactionService");
List<TransactionalCache<?, ?>> cachesToClear = new ArrayList<>();
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("propertyValueCache"));
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("node.nodesCache"));
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("node.propertiesCache"));
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("aclCache"));
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("aclEntityCache"));
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("permissionEntityCache"));
cachesToClear.add((TransactionalCache<?, ?>) ctx.getBean("nodeOwnerCache"));
for (TransactionalCache<?, ?> transactionalCache : cachesToClear)
{
transactionalCache.clear();
}
}
@After
public void tearDown() throws Exception
{
authenticationComponent.clearCurrentSecurityContext();
}
@Test
public void testPagination()
{
String searchMarker = UUID.randomUUID().toString();
int contentFilesCount = 185;
createFolderWithContentNodes(searchMarker, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 0, 50, 50, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 50, 50, 50, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 150, 50, 35, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 200, 50, 0, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 0, 100, 100, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 100, 100, 85, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 200, 100, 0, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 0, 200, contentFilesCount, contentFilesCount);
}
@Test
public void testLargeFilesCount()
{
String searchMarker = UUID.randomUUID().toString();
int contentFilesCount = 10_000;
createFolderWithContentNodes(searchMarker, contentFilesCount);
prepareParametersQueryAndAssertResult(searchMarker, 0, Integer.MAX_VALUE, contentFilesCount, contentFilesCount);
}
private void createFolderWithContentNodes(String searchMarker, int contentFilesCount)
{
NodeRef testFolder = txnHelper.doInTransaction(this::createFolderNode, false, false);
int batchSize = 1000;
int fullBatches = contentFilesCount / batchSize;
int remainingItems = contentFilesCount % batchSize;
for (int i = 0; i < fullBatches; i++)
{
txnHelper.doInTransaction(() -> {
for (int j = 0; j < batchSize; j++)
{
createContentNode(searchMarker, testFolder);
}
return null;
}, false, false);
}
if (remainingItems > 0)
{
txnHelper.doInTransaction(() -> {
for (int j = 0; j < remainingItems; j++)
{
createContentNode(searchMarker, testFolder);
}
return null;
}, false, false);
}
}
private void prepareParametersQueryAndAssertResult(String searchMarker, int parameterSkipCount, int parameterMaxItems, int expectedLength, int expectedNumberFound)
{
txnHelper.doInTransaction(() -> {
// given
SearchParameters sp = new SearchParameters();
sp.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
sp.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO);
sp.setQueryConsistency(QueryConsistency.TRANSACTIONAL);
sp.setQuery("(+TYPE:'cm:content') and !ASPECT:'cm:checkedOut' and !TYPE:'fm:forum' and !TYPE:'fm:topic' and !TYPE:'cm:systemfolder' and !TYPE:'fm:post' and !TYPE:'fm:forums' and =cm:description:'" + searchMarker + "'");
sp.setSkipCount(parameterSkipCount);
sp.setMaxItems(parameterMaxItems);
sp.setMaxPermissionChecks(Integer.MAX_VALUE);
sp.setMaxPermissionCheckTimeMillis(Duration.ofMinutes(10).toMillis());
// when
ResultSet resultSet = pubSearchService.query(sp);
// then
assertEquals(expectedLength, resultSet.length());
assertEquals(expectedNumberFound, resultSet.getNumberFound());
return null;
}, false, false);
}
private NodeRef createFolderNode()
{
NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
Map<QName, Serializable> testFolderProps = new HashMap<>();
String folderName = "folder" + UUID.randomUUID();
testFolderProps.put(ContentModel.PROP_NAME, folderName);
return nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, folderName),
ContentModel.TYPE_FOLDER,
testFolderProps).getChildRef();
}
private void createContentNode(String searchMarker, NodeRef testFolder)
{
Map<QName, Serializable> testContentProps = new HashMap<>();
String fileName = "content" + UUID.randomUUID();
testContentProps.put(ContentModel.PROP_NAME, fileName);
testContentProps.put(ContentModel.PROP_DESCRIPTION, searchMarker);
nodeService.createNode(
testFolder,
ContentModel.ASSOC_CONTAINS,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, fileName),
ContentModel.TYPE_CONTENT,
testContentProps);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* Copyright (C) 2005 - 2025 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,7 +60,8 @@ import org.alfresco.util.Pair;
public class DBQueryEngineTest
{
private static final String SQL_TEMPLATE_PATH = "alfresco.metadata.query.select_byDynamicQuery";
private static final String SQL_SELECT_TEMPLATE_PATH = "alfresco.metadata.query.select_byDynamicQuery";
private static final String SQL_COUNT_TEMPLATE_PATH = "alfresco.metadata.query.count_byDynamicQuery";
private DBQueryEngine engine;
private SqlSessionTemplate template;
@@ -94,6 +95,7 @@ public class DBQueryEngineTest
public void shouldGetAFilteringResultSetFromAcceleratedNodeSelection()
{
withMaxItems(10);
when(template.selectOne(any(), eq(dbQuery))).thenReturn(10);
ResultSet result = engine.acceleratedNodeSelection(options, dbQuery, assessor);
@@ -226,7 +228,9 @@ public class DBQueryEngineTest
}
return null;
}).when(template).select(eq(SQL_TEMPLATE_PATH), eq(dbQuery), any());
}).when(template).select(eq(SQL_SELECT_TEMPLATE_PATH), eq(dbQuery), any());
when(template.selectOne(eq(SQL_COUNT_TEMPLATE_PATH), eq(dbQuery))).thenReturn(nodes.size());
}
private QueryOptions createQueryOptions()

View File

@@ -38,7 +38,6 @@ import java.util.Set;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import net.minidev.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -58,9 +57,6 @@ public class ClientRegistrationProviderUnitTest
private static final String OPENID_CONFIGURATION = "{\"token_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.serviceonline.alfresco/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.serviceonline.alfresco/alfresco/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.service.alfresco/oidc/userinfo\",\"authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.serviceonline.alfresco/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"serviceonline.alfresco\",\"cloud_graph_host_name\":\"graph.oidc.net\",\"msgraph_host\":\"graph.service.alfresco\",\"rbac_url\":\"https://pas.oidc.alfresco\"}";
private static final String DISCOVERY_PATH_SEGMENTS = "/.well-known/openid-configuration";
private static final String AUTH_SERVER = "https://login.serviceonline.alfresco";
private static final String ADMIN_CONSOLE_SCOPES = "openid,email,profile,offline_access";
private static final String PSSWD_GRANT_SCOPES = "openid,email,profile";
private static final String ISSUER_ATRR = "issuer";
private IdentityServiceConfig config;
private RestTemplate restTemplate;
@@ -74,9 +70,6 @@ public class ClientRegistrationProviderUnitTest
config = new IdentityServiceConfig();
config.setAuthServerUrl(AUTH_SERVER);
config.setResource(CLIENT_ID);
config.setAdminConsoleScopes(ADMIN_CONSOLE_SCOPES);
config.setPasswordGrantScopes(PSSWD_GRANT_SCOPES);
config.setIssuerAttribute(ISSUER_ATRR);
restTemplate = mock(RestTemplate.class);
ResponseEntity responseEntity = mock(ResponseEntity.class);
@@ -270,42 +263,4 @@ public class ClientRegistrationProviderUnitTest
"https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
}
}
@Test
public void shouldUseDefaultIssuerAttribute()
{
config.setIssuerUrl(null);
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0");
}
}
@Test
public void shouldUseCustomIssuerAttribute()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
config.setIssuerAttribute("access_token_issuer");
when(oidcResponse.getCustomParameters()).thenReturn(createJSONObject("access_token_issuer", "https://login.serviceonline.alfresco/alfresco/v2.0/at_trust"));
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0/at_trust");
}
}
private static JSONObject createJSONObject(String fieldName, String fieldValue)
{
JSONObject jsonObject = new JSONObject();
jsonObject.appendField(fieldName, fieldValue);
return jsonObject;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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
@@ -43,7 +43,6 @@ import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;

View File

@@ -25,7 +25,10 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.atLeast;
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;
@@ -34,14 +37,11 @@ import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
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.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
@@ -126,15 +126,11 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(principalAttribute, "given_name", "family_name", "email");
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
ClientRegistration clientRegistration = mock(ClientRegistration.class, RETURNS_DEEP_STUBS);
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(principalAttribute);
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
when(idsServiceFacadeMock.getUserInfo(accessToken, userInfoAttrMapping)).thenReturn(identityServiceFacade.getUserInfo(accessToken, userInfoAttrMapping));
when(idsServiceFacadeMock.getClientRegistration()).thenReturn(clientRegistration);
when(idsServiceFacadeMock.getUserInfo(accessToken, principalAttribute)).thenReturn(identityServiceFacade.getUserInfo(accessToken, principalAttribute));
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
Field declaredField = jitProvisioningHandler.getClass()
@@ -155,7 +151,7 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertEquals("johndoe123@alfresco.com", userInfoOptional.get().email());
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
verify(idsServiceFacadeMock).decodeToken(accessToken);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, userInfoAttrMapping);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, principalAttribute);
if (!isAuth0Enabled)
{
assertEquals("John", userInfoOptional.get().firstName());

View File

@@ -40,13 +40,8 @@ import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.Mock;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
@@ -56,9 +51,6 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
@Mock
private IdentityServiceFacade identityServiceFacade;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ClientRegistration clientRegistration;
@Mock
private PersonService personService;
@@ -72,22 +64,11 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
private IdentityServiceConfig identityServiceConfig;
@Mock
private DecodedTokenUser decodedTokenUser;
private OIDCUserInfo userInfo;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private UserInfoAttrMapping expectedMapping;
private static final String JWT_TOKEN = "myToken";
private static final String USERNAME = "johny123";
private static final String FIRST_NAME = "John";
private static final String LAST_NAME = "Doe";
private static final String EMAIL = "johny123@email.com";
public static final String USERNAME_CLAIM = "nickname";
public static final String EMAIL_CLAIM = "email";
public static final String FIRST_NAME_CLAIM = "given_name";
public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
@@ -97,147 +78,149 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
when(transactionService.isReadOnly()).thenReturn(false);
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
when(personService.createMissingPeople()).thenReturn(true);
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(USERNAME_CLAIM);
when(identityServiceConfig.getEmailAttribute()).thenReturn(EMAIL_CLAIM);
when(identityServiceConfig.getFirstNameAttribute()).thenReturn(FIRST_NAME_CLAIM);
when(identityServiceConfig.getLastNameAttribute()).thenReturn(LAST_NAME_CLAIM);
expectedMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
personService, transactionService, identityServiceConfig);
}
@Test
public void shouldExtractUserInfoForExistingUser()
{
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals(USERNAME, result.get().username());
assertEquals("johny123", result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldExtractUserInfoForExistingUserWithProviderPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(USERNAME);
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).thenReturn("johny123");
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals(USERNAME, result.get().username());
assertEquals("johny123", result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, "nickname");
}
@Test
public void shouldExtractUserInfoFromAccessTokenAndCreateUser()
{
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.personExists(USERNAME)).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn(FIRST_NAME);
when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn(LAST_NAME);
when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn(EMAIL);
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");
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals(USERNAME, result.get().username());
assertEquals(FIRST_NAME, result.get().firstName());
assertEquals(LAST_NAME, result.get().lastName());
assertEquals(EMAIL, result.get().email());
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, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldExtractUserInfoFromUserInfoEndpointAndCreateUser()
{
when(decodedTokenUser.username()).thenReturn(USERNAME);
when(decodedTokenUser.firstName()).thenReturn(FIRST_NAME);
when(decodedTokenUser.lastName()).thenReturn(LAST_NAME);
when(decodedTokenUser.email()).thenReturn(EMAIL);
when(personService.personExists(USERNAME)).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
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, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals(USERNAME, result.get().username());
assertEquals(FIRST_NAME, result.get().firstName());
assertEquals(LAST_NAME, result.get().lastName());
assertEquals(EMAIL, result.get().email());
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, expectedMapping);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
{
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertFalse(result.isPresent());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldCallUserInfoEndpointToGetUsername()
{
when(personService.personExists(USERNAME)).thenReturn(true);
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals(USERNAME, result.get().username());
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, expectedMapping);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldCallUserInfoEndpointToGetUsernameWithProvidedPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, "nickname")).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals(USERNAME, result.get().username());
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, expectedMapping);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, "nickname");
}
@Test
@@ -249,8 +232,8 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
verify(personService, never()).createPerson(any());
verify(identityServiceFacade, never()).decodeToken(null);
verify(identityServiceFacade, never()).decodeToken("");
verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(null, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo("", PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
}

View File

@@ -38,7 +38,6 @@ import jakarta.servlet.http.HttpServletRequest;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import junit.framework.TestCase;
import org.mockito.Mockito;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.alfresco.repo.security.authentication.AuthenticationException;
@@ -97,14 +96,12 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
{
final TransactionService transactionService = mock(TransactionService.class);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class, Mockito.RETURNS_DEEP_STUBS);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
final PersonService personService = mock(PersonService.class);
final IdentityServiceConfig identityServiceConfig = mock(IdentityServiceConfig.class);
when(transactionService.isReadOnly()).thenReturn(true);
when(facade.decodeToken(anyString()))
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
when(facade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
.thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));

View File

@@ -40,14 +40,12 @@ import org.springframework.web.client.RestOperations;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
public class SpringBasedIdentityServiceFacadeUnitTest
{
private static final String USER_NAME = "user";
private static final String PASSWORD = "password";
private static final String TOKEN = "tEsT-tOkEn";
private static final UserInfoAttrMapping USER_INFO_ATTR_MAPPING = new UserInfoAttrMapping("preferred_username", "given_name", "family_name", "email");
@Test
public void shouldThrowVerificationExceptionOnFailure()
@@ -84,7 +82,7 @@ public class SpringBasedIdentityServiceFacadeUnitTest
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThat(facade.getUserInfo(TOKEN, USER_INFO_ATTR_MAPPING).isEmpty()).isTrue();
assertThat(facade.getUserInfo(TOKEN, "preferred_username").isEmpty()).isTrue();
}
private ClientRegistration testRegistration()

View File

@@ -38,7 +38,6 @@ import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -156,7 +155,6 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
@@ -180,7 +178,6 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAudience()).thenReturn(audience);
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(redirectPath);
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
.formatted("http://localhost:8080", redirectPath);

View File

@@ -1,109 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
public class AccessTokenToDecodedTokenUserMapperUnitTest
{
@Mock
private IdentityServiceFacade.DecodedAccessToken decodedAccessToken;
private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
public static final String USERNAME_CLAIM = "nickname";
public static final String EMAIL_CLAIM = "email";
public static final String FIRST_NAME_CLAIM = "given_name";
public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
{
initMocks(this);
UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
}
@Test
public void shouldMapToDecodedTokenUserWithAllFieldsPopulated()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
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());
}
@Test
public void shouldMapToDecodedTokenUserWithSomeFieldsEmpty()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("", result.get().email());
}
@Test
public void shouldReturnEmptyOptionalForNullUsername()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(null);
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertFalse(result.isPresent());
}
}

View File

@@ -1,95 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.user;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.alfresco.service.cmr.security.PersonService;
public class TokenUserToOIDCUserMapperUnitTest
{
@Mock
private PersonService personService;
@InjectMocks
private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
@Before
public void setup()
{
initMocks(this);
}
@Test
public void shouldMapToOIDCUserWithAllFieldsPopulated()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser("JOHNY123", "John", "Doe", "johny123@email.com");
when(personService.getUserIdentifier("JOHNY123")).thenReturn("johny123");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertEquals("johny123", oidcUserInfo.username());
assertEquals("John", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("johny123@email.com", oidcUserInfo.email());
}
@Test
public void shouldMapToOIDCUserWithSomeFieldsEmpty()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser("johny123", "", "Doe", "");
when(personService.getUserIdentifier("johny123")).thenReturn("johny123");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertEquals("johny123", oidcUserInfo.username());
assertEquals("", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("", oidcUserInfo.email());
}
@Test
public void shouldReturnNullForNullUsername()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser(null, "John", "Doe", "johny123@email.com");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertNull(oidcUserInfo.username());
assertEquals("John", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("johny123@email.com", oidcUserInfo.email());
}
}

View File

@@ -28,9 +28,6 @@ identity-service.register-node-at-startup=true
identity-service.register-node-period=50
identity-service.token-store=SESSION
identity-service.principal-attribute=preferred_username
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.turn-off-change-session-id-on-login=true
identity-service.token-minimum-time-to-live=10
identity-service.min-time-between-jwks-requests=60