mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-05-12 21:24:43 +00:00
Enhancements from PR review
- split authority mapper into granted authority and person processor - rename authority sync to group sync (since it only handles groups not generic authorities), with slight rename of config properties - add javadoc - add copyright headers - add test configuration - use direct component injection instead of auth component pass-along - add additional "global-and-subsystem-properties" to Spring context for subsystem to handle dynamic config specified in alfresco-global.properties for which there is no pre-defined default in subsystem defaults (found during test with acme-group role mapping in alfresco-global.addition.properties)
This commit is contained in:
parent
782e785b6d
commit
d5a9d521c9
2
pom.xml
2
pom.xml
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<groupId>de.acosix.alfresco.keycloak</groupId>
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
<version>1.1.0-rc7</version>
|
<version>1.1.0-rc8-SNAPSHOT</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<name>Acosix Alfresco Keycloak - Parent</name>
|
<name>Acosix Alfresco Keycloak - Parent</name>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>de.acosix.alfresco.keycloak</groupId>
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
<version>1.1.0-rc7</version>
|
<version>1.1.0-rc8-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>de.acosix.alfresco.keycloak.repo</artifactId>
|
<artifactId>de.acosix.alfresco.keycloak.repo</artifactId>
|
||||||
|
@ -23,6 +23,16 @@
|
|||||||
<property name="subsystemChildApplicationContextManager" ref="Authentication" />
|
<property name="subsystemChildApplicationContextManager" ref="Authentication" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<!-- needed for bean emitters with dynamic properties (cannot be pre-defined in subsystem defaults) -->
|
||||||
|
<bean id="global-and-subsystem-properties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
|
||||||
|
<property name="propertiesArray">
|
||||||
|
<list>
|
||||||
|
<ref bean="global-properties" />
|
||||||
|
<ref bean="subsystem-properties" />
|
||||||
|
</list>
|
||||||
|
</property>
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="keycloakAdapterConfig" class="${project.artifactId}.spring.KeycloakAdapterConfigBeanFactory">
|
<bean id="keycloakAdapterConfig" class="${project.artifactId}.spring.KeycloakAdapterConfigBeanFactory">
|
||||||
<property name="propertiesSource" ref="subsystem-properties" />
|
<property name="propertiesSource" ref="subsystem-properties" />
|
||||||
<property name="configPropertyPrefix" value="keycloak.adapter" />
|
<property name="configPropertyPrefix" value="keycloak.adapter" />
|
||||||
@ -187,9 +197,15 @@
|
|||||||
<property name="processResourceRoles" value="${keycloak.roles.mapResourceRoles}" />
|
<property name="processResourceRoles" value="${keycloak.roles.mapResourceRoles}" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="authorityMapper" class="${project.artifactId}.authentication.KeycloakTokenAuthorityMapper">
|
<bean id="tokenGrantedAuthorityProcessor" class="${project.artifactId}.authentication.KeycloakTokenGrantedAuthorityProcessor">
|
||||||
<property name="enabled" value="${keycloak.authentication.mapAuthorities}" />
|
<property name="enabled" value="${keycloak.authentication.mapAuthorities}" />
|
||||||
<property name="mapPersonPropertiesOnLogin" value="${keycloak.authentication.mapPersonPropertiesOnLogin}" />
|
</bean>
|
||||||
|
|
||||||
|
<bean id="tokenPersonProcessor" class="${project.artifactId}.authentication.KeycloakTokenPersonProcessor">
|
||||||
|
<property name="enabled" value="${keycloak.authentication.mapPersonPropertiesOnLogin}" />
|
||||||
|
<property name="transactionService" ref="TransactionService" />
|
||||||
|
<property name="nodeService" ref="NodeService" />
|
||||||
|
<property name="personService" ref="PersonService" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="userAuthority.default" class="${project.artifactId}.authentication.DefaultAuthorityExtractor">
|
<bean id="userAuthority.default" class="${project.artifactId}.authentication.DefaultAuthorityExtractor">
|
||||||
@ -201,10 +217,11 @@
|
|||||||
|
|
||||||
<bean id="userToken.default" class="${project.artifactId}.authentication.DefaultPersonProcessor" />
|
<bean id="userToken.default" class="${project.artifactId}.authentication.DefaultPersonProcessor" />
|
||||||
|
|
||||||
<bean id="authoritySync" class="${project.artifactId}.authentication.KeycloakTokenAuthoritySync">
|
<bean id="tokenGroupProcessor" class="${project.artifactId}.authentication.KeycloakTokenGroupSyncProcessor">
|
||||||
<property name="enabled" value="${keycloak.authentication.syncAuthoritiesOnLogin}" />
|
<property name="createMissingGroupsOnLogin" value="${keycloak.authentication.createMissingGroupsOnLogin}" />
|
||||||
<property name="syncAuthorityMembershipOnLogin" value="${keycloak.authentication.syncAuthorityMembershipOnLogin}" />
|
<property name="syncGroupMembershipOnLogin" value="${keycloak.authentication.syncGroupMembershipOnLogin}" />
|
||||||
<property name="authorityService" ref="authorityService" />
|
<property name="transactionService" ref="TransactionService" />
|
||||||
|
<property name="authorityService" ref="AuthorityService" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="userFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentUserFilter">
|
<bean id="userFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentUserFilter">
|
||||||
@ -261,7 +278,7 @@
|
|||||||
<value>userToken</value>
|
<value>userToken</value>
|
||||||
</list>
|
</list>
|
||||||
</property>
|
</property>
|
||||||
<property name="propertiesSource" ref="subsystem-properties" />
|
<property name="propertiesSource" ref="global-and-subsystem-properties" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="${moduleId}-dynamicSynchronisationComponentsEmitter"
|
<bean id="${moduleId}-dynamicSynchronisationComponentsEmitter"
|
||||||
@ -276,7 +293,7 @@
|
|||||||
<value>groupMapper</value>
|
<value>groupMapper</value>
|
||||||
</list>
|
</list>
|
||||||
</property>
|
</property>
|
||||||
<property name="propertiesSource" ref="subsystem-properties" />
|
<property name="propertiesSource" ref="global-and-subsystem-properties" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="${moduleId}-dynamicRolesComponentsEmitter"
|
<bean id="${moduleId}-dynamicRolesComponentsEmitter"
|
||||||
@ -292,6 +309,6 @@
|
|||||||
<value>resourceFilter</value>
|
<value>resourceFilter</value>
|
||||||
</list>
|
</list>
|
||||||
</property>
|
</property>
|
||||||
<property name="propertiesSource" ref="subsystem-properties" />
|
<property name="propertiesSource" ref="global-and-subsystem-properties" />
|
||||||
</bean>
|
</bean>
|
||||||
</beans>
|
</beans>
|
@ -13,9 +13,9 @@ keycloak.authentication.mapPersonPropertiesOnLogin=true
|
|||||||
keycloak.authentication.authenticateFTP=true
|
keycloak.authentication.authenticateFTP=true
|
||||||
keycloak.authentication.silentRemoteUserValidationFailure=true
|
keycloak.authentication.silentRemoteUserValidationFailure=true
|
||||||
|
|
||||||
# Authority Sync plugin
|
# Group Sync processor by Brian Long
|
||||||
keycloak.authentication.syncAuthoritiesOnLogin=false
|
keycloak.authentication.createMissingGroupsOnLogin=false
|
||||||
keycloak.authentication.syncAuthorityMembershipOnLogin=false
|
keycloak.authentication.syncGroupMembershipOnLogin=false
|
||||||
|
|
||||||
keycloak.authentication.bodyBufferLimit=10485760
|
keycloak.authentication.bodyBufferLimit=10485760
|
||||||
|
|
||||||
|
@ -80,9 +80,9 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
|
|
||||||
this.accessTokenClient = new AccessTokenClient(this.deployment);
|
this.accessTokenClient = new AccessTokenClient(this.deployment);
|
||||||
|
|
||||||
List<TokenProcessor> tokenProcessors = new ArrayList<>(this.applicationContext.getBeansOfType(TokenProcessor.class, false, true).values());
|
this.tokenProcessors = new ArrayList<>(this.applicationContext.getBeansOfType(TokenProcessor.class, false, true).values());
|
||||||
Collections.sort(tokenProcessors);
|
Collections.sort(this.tokenProcessors);
|
||||||
this.tokenProcessors = Collections.unmodifiableList(tokenProcessors);
|
this.tokenProcessors = Collections.unmodifiableList(this.tokenProcessors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,25 +267,23 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes tokens for authenticated users, mapping them to Alfresco person properties or granted authorities as configured for this
|
* Handles user tokens after authentication (initial or refresh) by delegating them to {@link TokenProcessor token processors} defined
|
||||||
* instance.
|
* in the application context.
|
||||||
*
|
*
|
||||||
* @param accessToken
|
* @param accessToken
|
||||||
* the access token
|
* the access token
|
||||||
* @param idToken
|
* @param idToken
|
||||||
* the ID token
|
* the ID token
|
||||||
* @param freshLogin
|
* @param freshLogin
|
||||||
* {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise -
|
* {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise
|
||||||
* Alfresco person node properties will only be mapped for fresh tokens, while granted authorities processors will always be
|
|
||||||
* handled if enabled
|
|
||||||
*/
|
*/
|
||||||
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
|
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
|
||||||
{
|
{
|
||||||
for (TokenProcessor processor : this.tokenProcessors)
|
for (final TokenProcessor processor : this.tokenProcessors)
|
||||||
{
|
{
|
||||||
LOGGER.debug("Processing token with {}", processor.getName());
|
LOGGER.debug("Processing token with {}", processor.getName());
|
||||||
processor.handleUserTokens(this, accessToken, idToken, freshLogin);
|
processor.handleUserTokens(accessToken, idToken, freshLogin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,171 +0,0 @@
|
|||||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
|
||||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
|
||||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
|
||||||
import org.alfresco.service.cmr.repository.NodeRef;
|
|
||||||
import org.alfresco.service.cmr.repository.NodeService;
|
|
||||||
import org.alfresco.service.namespace.QName;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.IDToken;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.BeansException;
|
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.context.ApplicationContextAware;
|
|
||||||
|
|
||||||
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
|
||||||
import net.sf.acegisecurity.Authentication;
|
|
||||||
import net.sf.acegisecurity.GrantedAuthority;
|
|
||||||
import net.sf.acegisecurity.GrantedAuthorityImpl;
|
|
||||||
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
|
||||||
|
|
||||||
public class KeycloakTokenAuthorityMapper implements TokenProcessor, InitializingBean, ApplicationContextAware {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTokenAuthorityMapper.class);
|
|
||||||
|
|
||||||
private static final String NAME = "AuthorityMapper";
|
|
||||||
|
|
||||||
protected ApplicationContext applicationContext;
|
|
||||||
|
|
||||||
protected boolean enabled;
|
|
||||||
|
|
||||||
protected boolean mapPersonPropertiesOnLogin;
|
|
||||||
|
|
||||||
protected Collection<AuthorityExtractor> authorityExtractors;
|
|
||||||
|
|
||||||
protected Collection<UserProcessor> userProcessors;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mapPersonPropertiesOnLogin
|
|
||||||
* the mapPersonPropertiesOnLogin to set
|
|
||||||
*/
|
|
||||||
public void setMapPersonPropertiesOnLogin(final boolean mapPersonPropertiesOnLogin) {
|
|
||||||
this.mapPersonPropertiesOnLogin = mapPersonPropertiesOnLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
|
||||||
this.applicationContext = applicationContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterPropertiesSet() throws BeansException {
|
|
||||||
this.authorityExtractors = Collections
|
|
||||||
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(AuthorityExtractor.class, false, true).values()));
|
|
||||||
this.userProcessors = Collections
|
|
||||||
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(UserProcessor.class, false, true).values()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleUserTokens(AbstractAuthenticationComponent authComponent, AccessToken accessToken,
|
|
||||||
IDToken idToken, boolean freshLogin) {
|
|
||||||
if (!this.enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOGGER.debug("Mapping Keycloak access token to user authorities");
|
|
||||||
|
|
||||||
final Set<String> mappedAuthorities = new HashSet<>();
|
|
||||||
this.authorityExtractors.stream().map(extractor -> extractor.extractAuthorities(accessToken))
|
|
||||||
.forEach(mappedAuthorities::addAll);
|
|
||||||
|
|
||||||
LOGGER.debug("Mapped user authorities from access token: {}", mappedAuthorities);
|
|
||||||
|
|
||||||
if (!mappedAuthorities.isEmpty())
|
|
||||||
{
|
|
||||||
final Authentication currentAuthentication = authComponent.getCurrentAuthentication();
|
|
||||||
if (currentAuthentication instanceof UsernamePasswordAuthenticationToken)
|
|
||||||
{
|
|
||||||
GrantedAuthority[] grantedAuthorities = currentAuthentication.getAuthorities();
|
|
||||||
|
|
||||||
final List<GrantedAuthority> grantedAuthoritiesL = mappedAuthorities.stream().map(GrantedAuthorityImpl::new)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
grantedAuthoritiesL.addAll(Arrays.asList(grantedAuthorities));
|
|
||||||
|
|
||||||
grantedAuthorities = grantedAuthoritiesL.toArray(new GrantedAuthority[0]);
|
|
||||||
((UsernamePasswordAuthenticationToken) currentAuthentication).setAuthorities(grantedAuthorities);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOGGER.warn(
|
|
||||||
"Authentication for user is not of the expected type {} - Keycloak access token cannot be mapped to granted authorities",
|
|
||||||
UsernamePasswordAuthenticationToken.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (freshLogin && this.mapPersonPropertiesOnLogin)
|
|
||||||
{
|
|
||||||
final boolean requiresNew = AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
|
|
||||||
authComponent.getTransactionService().getRetryingTransactionHelper().doInTransaction(() -> {
|
|
||||||
this.updatePerson(authComponent, accessToken, idToken);
|
|
||||||
return null;
|
|
||||||
}, false, requiresNew);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the person for the current user with data mapped from the Keycloak tokens.
|
|
||||||
*
|
|
||||||
* @param accessToken
|
|
||||||
* the access token
|
|
||||||
* @param idToken
|
|
||||||
* the ID token
|
|
||||||
*/
|
|
||||||
protected void updatePerson(AbstractAuthenticationComponent authComponent,
|
|
||||||
final AccessToken accessToken, final IDToken idToken)
|
|
||||||
{
|
|
||||||
final String userName = authComponent.getCurrentUserName();
|
|
||||||
|
|
||||||
LOGGER.debug("Mapping person property updates for user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
|
||||||
|
|
||||||
final NodeRef person = authComponent.getPersonService().getPerson(userName);
|
|
||||||
|
|
||||||
final Map<QName, Serializable> updates = new HashMap<>();
|
|
||||||
this.userProcessors.forEach(processor -> processor.mapUser(accessToken, idToken != null ? idToken : accessToken, updates));
|
|
||||||
|
|
||||||
LOGGER.debug("Determined property updates for person node of user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
|
||||||
|
|
||||||
final Set<QName> propertiesToRemove = updates.keySet().stream().filter(k -> updates.get(k) == null).collect(Collectors.toSet());
|
|
||||||
updates.keySet().removeAll(propertiesToRemove);
|
|
||||||
|
|
||||||
final NodeService nodeService = authComponent.getNodeService();
|
|
||||||
final Map<QName, Serializable> currentProperties = nodeService.getProperties(person);
|
|
||||||
|
|
||||||
propertiesToRemove.retainAll(currentProperties.keySet());
|
|
||||||
if (!propertiesToRemove.isEmpty())
|
|
||||||
{
|
|
||||||
// there is no bulk-remove, so we need to use setProperties to achieve a single update event
|
|
||||||
final Map<QName, Serializable> newProperties = new HashMap<>(currentProperties);
|
|
||||||
newProperties.putAll(updates);
|
|
||||||
newProperties.keySet().removeAll(propertiesToRemove);
|
|
||||||
nodeService.setProperties(person, newProperties);
|
|
||||||
}
|
|
||||||
else if (!updates.isEmpty())
|
|
||||||
{
|
|
||||||
nodeService.addProperties(person, updates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,249 +0,0 @@
|
|||||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
|
|
||||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
|
||||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
|
||||||
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
|
|
||||||
import org.alfresco.service.cmr.security.AuthorityService;
|
|
||||||
import org.alfresco.service.cmr.security.AuthorityType;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.IDToken;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
|
||||||
import net.sf.acegisecurity.GrantedAuthority;
|
|
||||||
|
|
||||||
public class KeycloakTokenAuthoritySync implements TokenProcessor {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTokenAuthoritySync.class);
|
|
||||||
|
|
||||||
private static final String NAME = "AuthoritySync";
|
|
||||||
|
|
||||||
protected boolean enabled;
|
|
||||||
|
|
||||||
protected boolean syncAuthorityMembershipOnLogin;
|
|
||||||
|
|
||||||
protected AuthorityService authorityService;
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param syncAuthorityMembershipOnLogin
|
|
||||||
* the syncAuthorityMembershipOnLogin to set
|
|
||||||
*/
|
|
||||||
public void setSyncAuthorityMembershipOnLogin(final boolean syncAuthorityMembershipOnLogin) {
|
|
||||||
this.syncAuthorityMembershipOnLogin = syncAuthorityMembershipOnLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthorityService(AuthorityService authorityService) {
|
|
||||||
this.authorityService = authorityService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPriority() {
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleUserTokens(AbstractAuthenticationComponent authComponent, AccessToken accessToken,
|
|
||||||
IDToken idToken, boolean freshLogin) {
|
|
||||||
if (!this.enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (freshLogin) {
|
|
||||||
boolean requiresNew = AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
|
|
||||||
authComponent.getTransactionService().getRetryingTransactionHelper().doInTransaction(() -> {
|
|
||||||
GrantedAuthority[] authorities = authComponent.getCurrentAuthentication().getAuthorities();
|
|
||||||
this.syncAuthorities(Arrays.asList(authorities), accessToken, idToken);
|
|
||||||
return null;
|
|
||||||
}, false, requiresNew);
|
|
||||||
|
|
||||||
if (this.syncAuthorityMembershipOnLogin)
|
|
||||||
{
|
|
||||||
authComponent.getTransactionService().getRetryingTransactionHelper().doInTransaction(() -> {
|
|
||||||
String userName = authComponent.getCurrentUserName();
|
|
||||||
GrantedAuthority[] authorities = authComponent.getCurrentAuthentication().getAuthorities();
|
|
||||||
this.syncAuthorityMemberships(userName, Arrays.asList(authorities), accessToken, idToken);
|
|
||||||
return null;
|
|
||||||
}, false, requiresNew);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronizes the groups of the current user, but not the membership, as Alfresco user groups.
|
|
||||||
* @param authorities
|
|
||||||
* the Alfresco authorities to persist as user groups
|
|
||||||
* @param accessToken
|
|
||||||
* the access token
|
|
||||||
* @param idToken
|
|
||||||
* the ID token
|
|
||||||
*/
|
|
||||||
protected void syncAuthorities(final Collection<GrantedAuthority> authorities, final AccessToken accessToken, final IDToken idToken)
|
|
||||||
{
|
|
||||||
LOGGER.debug("Synchronizing user groups {}", authorities);
|
|
||||||
|
|
||||||
AuthenticationUtil.runAsSystem(new RunAsLoggableWork<Void>() {
|
|
||||||
@Override
|
|
||||||
public Void doLoggedWork() {
|
|
||||||
for (GrantedAuthority authority : authorities)
|
|
||||||
{
|
|
||||||
String authorityId = authority.getAuthority();
|
|
||||||
|
|
||||||
if (AuthorityType.GROUP.equals(AuthorityType.getAuthorityType(authorityId)))
|
|
||||||
{
|
|
||||||
// we only persist groups (not roles) in Alfresco
|
|
||||||
|
|
||||||
// if it didn't exist, then we need to associate the user to the group
|
|
||||||
if (!authorityService.authorityExists(authorityId))
|
|
||||||
{
|
|
||||||
// group does not yet exist; create one
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Creating authority {}", authorityId);
|
|
||||||
|
|
||||||
String authorityShortName = authorityService.getShortName(authorityId);
|
|
||||||
authorityShortName = normalizeAuthority(authorityShortName);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Creating group {}", authorityShortName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
authorityService.createAuthority(AuthorityType.GROUP, authorityShortName);
|
|
||||||
} catch (DuplicateChildNodeNameException dcnne) {
|
|
||||||
LOGGER.debug("Group {} already created; race condition?", authorityShortName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronizes the membership of the current user with the authority, as an Alfresco user group.
|
|
||||||
* @param authorities
|
|
||||||
* the Alfresco authorities to persist as user groups
|
|
||||||
* @param accessToken
|
|
||||||
* the access token
|
|
||||||
* @param idToken
|
|
||||||
* the ID token
|
|
||||||
*/
|
|
||||||
protected void syncAuthorityMemberships(String userName, final Collection<GrantedAuthority> authorities, final AccessToken accessToken, final IDToken idToken)
|
|
||||||
{
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Synchronizing user group membership for user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
|
||||||
|
|
||||||
String userAuthorityId = this.authorityService.getName(AuthorityType.USER, userName);
|
|
||||||
Set<String> persistedAuthorityIds = new HashSet<>(this.authorityService.getAuthoritiesForUser(userName));
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Current authorities for user {}: {}", AlfrescoCompatibilityUtil.maskUsername(userName), persistedAuthorityIds);
|
|
||||||
|
|
||||||
AuthenticationUtil.runAsSystem(new RunAsLoggableWork<Void>() {
|
|
||||||
@Override
|
|
||||||
public Void doLoggedWork() throws Exception {
|
|
||||||
for (GrantedAuthority authority : authorities)
|
|
||||||
{
|
|
||||||
String authorityId = authority.getAuthority();
|
|
||||||
LOGGER.trace("Inspecting authority '{}' to grant membership", authorityId);
|
|
||||||
|
|
||||||
if (AuthorityType.GROUP.equals(AuthorityType.getAuthorityType(authorityId)))
|
|
||||||
{
|
|
||||||
// we only persist groups in Alfresco
|
|
||||||
|
|
||||||
// remove if it exists; any remaining GROUP authorities will be unlinked from user later in this method
|
|
||||||
persistedAuthorityIds.remove(authorityId);
|
|
||||||
// we cannot assume persistedAuthorityIds has only registered groups; it includes authorities from keycloak
|
|
||||||
|
|
||||||
authorityId = normalizeAuthority(authorityId);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Adding user {} to group {}", AlfrescoCompatibilityUtil.maskUsername(userName), authorityId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// add the user to the existing group
|
|
||||||
authorityService.addAuthority(authorityId, userAuthorityId);
|
|
||||||
} catch (DuplicateChildNodeNameException dcnne) {
|
|
||||||
if (LOGGER.isTraceEnabled())
|
|
||||||
LOGGER.trace("User {} is already a member of group {}", AlfrescoCompatibilityUtil.maskUsername(userName), authorityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Removing user {} from authorities: {}", AlfrescoCompatibilityUtil.maskUsername(userName), persistedAuthorityIds);
|
|
||||||
|
|
||||||
// revoke user from groups
|
|
||||||
for (String persistedAuthorityId : persistedAuthorityIds)
|
|
||||||
{
|
|
||||||
LOGGER.trace("Inspecting authority '{}' to revoke membership", persistedAuthorityId);
|
|
||||||
|
|
||||||
if (AuthorityType.GROUP.equals(AuthorityType.getAuthorityType(persistedAuthorityId)))
|
|
||||||
{
|
|
||||||
// disassociate persisted groups only
|
|
||||||
persistedAuthorityId = normalizeAuthority(persistedAuthorityId);
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
LOGGER.debug("Removing user {} from group {}", AlfrescoCompatibilityUtil.maskUsername(userName), persistedAuthorityId);
|
|
||||||
|
|
||||||
authorityService.removeAuthority(persistedAuthorityId, userAuthorityId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeAuthority(String authorityIdOrShortName) {
|
|
||||||
// in case we need to filter out special characters
|
|
||||||
return authorityIdOrShortName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private interface RunAsLoggableWork<T> extends RunAsWork<T> {
|
|
||||||
|
|
||||||
default T doWork() throws Exception {
|
|
||||||
long time = System.currentTimeMillis();
|
|
||||||
LOGGER.trace("doWork()");
|
|
||||||
|
|
||||||
T result;
|
|
||||||
try {
|
|
||||||
result = this.doLoggedWork();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error("An unhandled exception occurred", e);
|
|
||||||
throw e;
|
|
||||||
} catch (Throwable t) {
|
|
||||||
LOGGER.error("An unhandled error occurred", t);
|
|
||||||
throw t;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.trace("doWork(): completed in {} ms", System.currentTimeMillis() - time);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
T doLoggedWork() throws Exception;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2021 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityService;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.authority.GrantedAuthorityAwareAuthorityServiceImpl;
|
||||||
|
import net.sf.acegisecurity.Authentication;
|
||||||
|
import net.sf.acegisecurity.GrantedAuthority;
|
||||||
|
import net.sf.acegisecurity.GrantedAuthorityImpl;
|
||||||
|
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This token processor maps authorities found in a Keycloak access token to {@link GrantedAuthority granted authorities} of the
|
||||||
|
* authenticated user. Such mapping does not
|
||||||
|
* make the authenticated user a permanent / persisted member of any group represented by an authority, and as such operations like
|
||||||
|
* {@link AuthorityService#getContainingAuthorities(org.alfresco.service.cmr.security.AuthorityType, String, boolean)
|
||||||
|
* getContainingAuthorities} will not return any of the mapped authorities if the user is not a member based on Alfresco's internal
|
||||||
|
* authority data. A {@link GrantedAuthorityAwareAuthorityServiceImpl granted-authority-aware AuthorityService} provided by this module
|
||||||
|
* adapts the {@link AuthorityService#getAuthoritiesForUser(String) getAuthoritiesForUser} operation in such a way that the result includes
|
||||||
|
* any authorities for the authenticated users specified via granted authorities. This ensures authorities are properly included in any
|
||||||
|
* permission / access checking performed by Alfresco.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class KeycloakTokenGrantedAuthorityProcessor implements TokenProcessor, InitializingBean, ApplicationContextAware
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTokenGrantedAuthorityProcessor.class);
|
||||||
|
|
||||||
|
private static final String NAME = "GrantedAuthorityProcessor";
|
||||||
|
|
||||||
|
protected ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
protected Collection<AuthorityExtractor> authorityExtractors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(final ApplicationContext applicationContext)
|
||||||
|
{
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws BeansException
|
||||||
|
{
|
||||||
|
this.authorityExtractors = Collections
|
||||||
|
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(AuthorityExtractor.class, false, true).values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
|
||||||
|
{
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping Keycloak access token to user authorities");
|
||||||
|
|
||||||
|
final Set<String> mappedAuthorities = new HashSet<>();
|
||||||
|
this.authorityExtractors.stream().map(extractor -> extractor.extractAuthorities(accessToken))
|
||||||
|
.forEach(mappedAuthorities::addAll);
|
||||||
|
|
||||||
|
LOGGER.debug("Mapped user authorities from access token: {}", mappedAuthorities);
|
||||||
|
|
||||||
|
if (!mappedAuthorities.isEmpty())
|
||||||
|
{
|
||||||
|
final Authentication currentAuthentication = AuthenticationUtil.getFullAuthentication();
|
||||||
|
if (currentAuthentication instanceof UsernamePasswordAuthenticationToken)
|
||||||
|
{
|
||||||
|
GrantedAuthority[] grantedAuthorities = currentAuthentication.getAuthorities();
|
||||||
|
|
||||||
|
final List<GrantedAuthority> grantedAuthoritiesL = mappedAuthorities.stream().map(GrantedAuthorityImpl::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
grantedAuthoritiesL.addAll(Arrays.asList(grantedAuthorities));
|
||||||
|
|
||||||
|
grantedAuthorities = grantedAuthoritiesL.toArray(new GrantedAuthority[0]);
|
||||||
|
((UsernamePasswordAuthenticationToken) currentAuthentication).setAuthorities(grantedAuthorities);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.warn(
|
||||||
|
"Authentication for user is not of the expected type {} - Keycloak access token cannot be mapped to granted authorities",
|
||||||
|
UsernamePasswordAuthenticationToken.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,253 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2021 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
||||||
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
||||||
|
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityService;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityType;
|
||||||
|
import org.alfresco.service.cmr.security.PermissionService;
|
||||||
|
import org.alfresco.service.transaction.TransactionService;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This token processor handles group authorities found in a Keylcoak access token, optionally creating any groups not already existing in
|
||||||
|
* Alfresco and/or synchronising the group membership of the current user with the set of groups specified in the token.
|
||||||
|
*
|
||||||
|
* @author Brian Long
|
||||||
|
*/
|
||||||
|
public class KeycloakTokenGroupSyncProcessor implements TokenProcessor, InitializingBean, ApplicationContextAware
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTokenGroupSyncProcessor.class);
|
||||||
|
|
||||||
|
private static final String NAME = "GroupSyncProcessor";
|
||||||
|
|
||||||
|
protected ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
protected boolean createMissingGroupsOnLogin;
|
||||||
|
|
||||||
|
protected boolean syncGroupMembershipOnLogin;
|
||||||
|
|
||||||
|
protected TransactionService transactionService;
|
||||||
|
|
||||||
|
protected AuthorityService authorityService;
|
||||||
|
|
||||||
|
protected Collection<AuthorityExtractor> authorityExtractors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(final ApplicationContext applicationContext)
|
||||||
|
{
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param createMissingGroupsOnLogin
|
||||||
|
* the createMissingGroupsOnLogin to set
|
||||||
|
*/
|
||||||
|
public void setCreateMissingGroupsOnLogin(final boolean createMissingGroupsOnLogin)
|
||||||
|
{
|
||||||
|
this.createMissingGroupsOnLogin = createMissingGroupsOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param syncGroupMembershipOnLogin
|
||||||
|
* the syncAuthorityMembershipOnLogin to set
|
||||||
|
*/
|
||||||
|
public void setSyncGroupMembershipOnLogin(final boolean syncGroupMembershipOnLogin)
|
||||||
|
{
|
||||||
|
this.syncGroupMembershipOnLogin = syncGroupMembershipOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param transactionService
|
||||||
|
* the transactionService to set
|
||||||
|
*/
|
||||||
|
public void setTransactionService(final TransactionService transactionService)
|
||||||
|
{
|
||||||
|
this.transactionService = transactionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param authorityService
|
||||||
|
* the authorityService to set
|
||||||
|
*/
|
||||||
|
public void setAuthorityService(final AuthorityService authorityService)
|
||||||
|
{
|
||||||
|
this.authorityService = authorityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws BeansException
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "transactionService", this.transactionService);
|
||||||
|
PropertyCheck.mandatory(this, "authorityService", this.authorityService);
|
||||||
|
this.authorityExtractors = Collections
|
||||||
|
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(AuthorityExtractor.class, false, true).values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
|
||||||
|
{
|
||||||
|
if (freshLogin && (this.createMissingGroupsOnLogin || this.syncGroupMembershipOnLogin))
|
||||||
|
{
|
||||||
|
final Collection<String> groups = this.extractGroups(accessToken);
|
||||||
|
|
||||||
|
final boolean requiresNew = AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
|
||||||
|
|
||||||
|
if (this.createMissingGroupsOnLogin)
|
||||||
|
{
|
||||||
|
AuthenticationUtil.runAsSystem(() -> this.transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
|
||||||
|
this.syncGroups(groups);
|
||||||
|
return null;
|
||||||
|
}, false, requiresNew));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.syncGroupMembershipOnLogin)
|
||||||
|
{
|
||||||
|
AuthenticationUtil.runAsSystem(() -> this.transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
|
||||||
|
this.syncGroupMemberships(groups);
|
||||||
|
return null;
|
||||||
|
}, false, requiresNew));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts groups from the provided access token.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the Keycloak access token for the authenticated user
|
||||||
|
* @return the (mutable) collection of extracted group authority names
|
||||||
|
*/
|
||||||
|
protected Collection<String> extractGroups(final AccessToken accessToken)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping Keycloak access token to user group authorities");
|
||||||
|
|
||||||
|
final Set<String> groups = new HashSet<>();
|
||||||
|
this.authorityExtractors.stream().map(extractor -> extractor.extractAuthorities(accessToken)).forEach(groups::addAll);
|
||||||
|
groups.removeIf(a -> AuthorityType.getAuthorityType(a) != AuthorityType.GROUP);
|
||||||
|
// in case some extractor mapped this pseudo-group
|
||||||
|
groups.remove(PermissionService.ALL_AUTHORITIES);
|
||||||
|
|
||||||
|
LOGGER.debug("Mapped user group authorities from access token: {}", groups);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronises the groups of the current user, but not the membership, as Alfresco user groups.
|
||||||
|
*
|
||||||
|
* @param groups
|
||||||
|
* the names of the user's groups extracted from the Keycloak access token
|
||||||
|
*/
|
||||||
|
protected void syncGroups(final Collection<String> groups)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Synchronizing user groups {}", groups);
|
||||||
|
|
||||||
|
for (final String group : groups)
|
||||||
|
{
|
||||||
|
if (!this.authorityService.authorityExists(group))
|
||||||
|
{
|
||||||
|
LOGGER.debug("Creating group {}", group);
|
||||||
|
final String groupShortName = this.authorityService.getShortName(group);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.authorityService.createAuthority(AuthorityType.GROUP, groupShortName);
|
||||||
|
}
|
||||||
|
catch (final DuplicateChildNodeNameException dcnne)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Group {} already created; race condition?", groupShortName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronises the membership of the current user in Alfresco user groups.
|
||||||
|
*
|
||||||
|
* @param groups
|
||||||
|
* the Alfresco group authorities as determined from the Keycloak access token for the current user
|
||||||
|
*/
|
||||||
|
protected void syncGroupMemberships(final Collection<String> groups)
|
||||||
|
{
|
||||||
|
final String userName = AuthenticationUtil.getFullyAuthenticatedUser();
|
||||||
|
final String maskedUsername = AlfrescoCompatibilityUtil.maskUsername(userName);
|
||||||
|
|
||||||
|
LOGGER.debug("Synchronising group membership for user {} and token extracted groups {}", maskedUsername, groups);
|
||||||
|
|
||||||
|
final Set<String> existingUnprocessedGroups = this.authorityService.getContainingAuthorities(AuthorityType.GROUP, userName, true);
|
||||||
|
|
||||||
|
LOGGER.debug("User {} is currently in the groups {}", maskedUsername, existingUnprocessedGroups);
|
||||||
|
|
||||||
|
for (final String group : groups)
|
||||||
|
{
|
||||||
|
// !remove(group) ensures we only add if not already a member
|
||||||
|
if (!existingUnprocessedGroups.remove(group) && this.authorityService.authorityExists(group))
|
||||||
|
{
|
||||||
|
LOGGER.debug("Adding user {} to group {}", maskedUsername, group);
|
||||||
|
this.authorityService.addAuthority(group, userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final String group : existingUnprocessedGroups)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Removing user {} from group {}", maskedUsername, group);
|
||||||
|
this.authorityService.removeAuthority(group, userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2021 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
||||||
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
||||||
|
import org.alfresco.service.cmr.repository.NodeRef;
|
||||||
|
import org.alfresco.service.cmr.repository.NodeService;
|
||||||
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
import org.alfresco.service.transaction.TransactionService;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This token processor maps profile data from the token of an authenticated user into the {@link ContentModel#TYPE_PERSON person
|
||||||
|
* properties} for that user upon login.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class KeycloakTokenPersonProcessor implements TokenProcessor, InitializingBean, ApplicationContextAware
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakTokenPersonProcessor.class);
|
||||||
|
|
||||||
|
private static final String NAME = "PersonProcessor";
|
||||||
|
|
||||||
|
protected ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
protected TransactionService transactionService;
|
||||||
|
|
||||||
|
protected NodeService nodeService;
|
||||||
|
|
||||||
|
protected PersonService personService;
|
||||||
|
|
||||||
|
protected Collection<UserProcessor> userProcessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(final ApplicationContext applicationContext)
|
||||||
|
{
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param transactionService
|
||||||
|
* the transactionService to set
|
||||||
|
*/
|
||||||
|
public void setTransactionService(final TransactionService transactionService)
|
||||||
|
{
|
||||||
|
this.transactionService = transactionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param nodeService
|
||||||
|
* the nodeService to set
|
||||||
|
*/
|
||||||
|
public void setNodeService(final NodeService nodeService)
|
||||||
|
{
|
||||||
|
this.nodeService = nodeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param personService
|
||||||
|
* the personService to set
|
||||||
|
*/
|
||||||
|
public void setPersonService(final PersonService personService)
|
||||||
|
{
|
||||||
|
this.personService = personService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() throws BeansException
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "transactionService", this.transactionService);
|
||||||
|
PropertyCheck.mandatory(this, "nodeService", this.nodeService);
|
||||||
|
PropertyCheck.mandatory(this, "personService", this.personService);
|
||||||
|
|
||||||
|
this.userProcessors = Collections
|
||||||
|
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(UserProcessor.class, false, true).values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
|
||||||
|
{
|
||||||
|
if (freshLogin && this.enabled)
|
||||||
|
{
|
||||||
|
final boolean requiresNew = AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
|
||||||
|
this.transactionService.getRetryingTransactionHelper().doInTransaction(() -> {
|
||||||
|
this.updatePerson(accessToken, idToken);
|
||||||
|
return null;
|
||||||
|
}, false, requiresNew);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the person for the current user with data mapped from the Keycloak tokens.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token
|
||||||
|
* @param idToken
|
||||||
|
* the ID token
|
||||||
|
*/
|
||||||
|
protected void updatePerson(final AccessToken accessToken, final IDToken idToken)
|
||||||
|
{
|
||||||
|
final String userName = AuthenticationUtil.getFullyAuthenticatedUser();
|
||||||
|
|
||||||
|
LOGGER.debug("Mapping person property updates for user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
||||||
|
|
||||||
|
final NodeRef person = this.personService.getPerson(userName);
|
||||||
|
|
||||||
|
final Map<QName, Serializable> updates = new HashMap<>();
|
||||||
|
this.userProcessors.forEach(processor -> processor.mapUser(accessToken, idToken != null ? idToken : accessToken, updates));
|
||||||
|
|
||||||
|
LOGGER.debug("Determined property updates for person node of user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
||||||
|
|
||||||
|
final Set<QName> propertiesToRemove = updates.keySet().stream().filter(k -> updates.get(k) == null).collect(Collectors.toSet());
|
||||||
|
updates.keySet().removeAll(propertiesToRemove);
|
||||||
|
|
||||||
|
final Map<QName, Serializable> currentProperties = this.nodeService.getProperties(person);
|
||||||
|
|
||||||
|
propertiesToRemove.retainAll(currentProperties.keySet());
|
||||||
|
if (!propertiesToRemove.isEmpty())
|
||||||
|
{
|
||||||
|
// there is no bulk-remove, so we need to use setProperties to achieve a single update event
|
||||||
|
final Map<QName, Serializable> newProperties = new HashMap<>(currentProperties);
|
||||||
|
newProperties.putAll(updates);
|
||||||
|
newProperties.keySet().removeAll(propertiesToRemove);
|
||||||
|
this.nodeService.setProperties(person, newProperties);
|
||||||
|
}
|
||||||
|
else if (!updates.isEmpty())
|
||||||
|
{
|
||||||
|
this.nodeService.addProperties(person, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,51 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2021 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instances of this interface are used to process access tokens from Keycloak authenticated users. All instances of this
|
* Instances of this interface are used to process access tokens from Keycloak authenticated users. All instances of this
|
||||||
* interface in the Keycloak authentication subsystem will be consulted in the order of the priority field, followed by the
|
* interface in the Keycloak authentication subsystem will be consulted in the order defined by the {@link #getPriority() priority} or the
|
||||||
* order the beans are defined in the Spring application context.
|
* order the beans are defined in the Spring application context in case of identical priorities.
|
||||||
*
|
*
|
||||||
* @author Brian Long
|
* @author Brian Long
|
||||||
*/
|
*/
|
||||||
public interface TokenProcessor extends Comparable<TokenProcessor> {
|
public interface TokenProcessor extends Comparable<TokenProcessor>
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A name of the processor for logging and reference purposes.
|
* The default priority value of a token processor if {@link #getPriority() getPriority} is not overridden.
|
||||||
* @return A processor name.
|
*/
|
||||||
*/
|
int DEFAULT_PRIORITY = 0;
|
||||||
String getName();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A priority for sorting beans for execution order.
|
* Retrieves the name of this processor, typically for logging and reference purposes.
|
||||||
* @return
|
*
|
||||||
*/
|
* @return the name of this processor
|
||||||
default int getPriority() {
|
*/
|
||||||
return 0;
|
String getName();
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* A priority for sorting beans for execution order.
|
||||||
|
*
|
||||||
|
* @return the priority - the lower the earlier this bean is processed
|
||||||
|
*/
|
||||||
|
default int getPriority()
|
||||||
|
{
|
||||||
|
return DEFAULT_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles access tokens from Keycloak.
|
* Handles access tokens from Keycloak.
|
||||||
*
|
*
|
||||||
* @param accessToken
|
* @param accessToken
|
||||||
* the Keycloak access token for the authenticated user
|
* the Keycloak access token for the authenticated user
|
||||||
* @param idToken
|
* @param idToken
|
||||||
* the Keycloak ID token for the authenticated user - may be {@code null} if not contained in the authentication response
|
* the Keycloak ID token for the authenticated user - may be {@code null} if not contained in the authentication response
|
||||||
* @param freshLogin
|
* @param freshLogin
|
||||||
* {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise
|
* {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise
|
||||||
*/
|
*/
|
||||||
void handleUserTokens(
|
void handleUserTokens(AccessToken accessToken, IDToken idToken, boolean freshLogin);
|
||||||
final AbstractAuthenticationComponent authComponent,
|
|
||||||
final AccessToken accessToken,
|
|
||||||
final IDToken idToken,
|
|
||||||
final boolean freshLogin);
|
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
default int compareTo(TokenProcessor o) {
|
*
|
||||||
return Integer.compare(this.getPriority(), o.getPriority());
|
* {@inheritDoc}
|
||||||
}
|
*/
|
||||||
|
@Override
|
||||||
|
default int compareTo(final TokenProcessor o)
|
||||||
|
{
|
||||||
|
return Integer.compare(this.getPriority(), o.getPriority());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
# note: this file is not named alfresco-global.properties to not override the default file in the image
|
# note: this file is not named alfresco-global.properties to not override the default file in the image
|
||||||
# instead it relies on Dockerfile post-processing to merge with the default file
|
# instead it relies on Dockerfile post-processing to merge with the default file
|
||||||
|
|
||||||
authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm
|
# alfrescoNtlm first to have local users take priority over remote and avoid errors in Keycloak for i.e. "admin" user
|
||||||
|
authentication.chain=alfrescoNtlm1:alfrescoNtlm,keycloak1:keycloak
|
||||||
|
|
||||||
keycloak.adapter.auth-server-url=http://localhost:${docker.tests.keycloakPort}/auth
|
keycloak.adapter.auth-server-url=http://localhost:${docker.tests.keycloakPort}/auth
|
||||||
keycloak.adapter.realm=test
|
keycloak.adapter.realm=test
|
||||||
@ -29,6 +30,11 @@ keycloak.adapter.proxy-url=http://keycloak:8080
|
|||||||
|
|
||||||
keycloak.roles.requiredClientScopes=alfresco-role-service
|
keycloak.roles.requiredClientScopes=alfresco-role-service
|
||||||
|
|
||||||
|
keycloak.roles.resourceMapper.default.static.property.nameMappings.map.acme-group=GROUP_ACME_MEMBER
|
||||||
|
|
||||||
|
keycloak.authentication.createMissingGroupsOnLogin=true
|
||||||
|
keycloak.authentication.syncGroupMembershipOnLogin=true
|
||||||
|
|
||||||
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
|
||||||
|
@ -591,7 +591,11 @@
|
|||||||
{
|
{
|
||||||
"clientScope": "alfresco",
|
"clientScope": "alfresco",
|
||||||
"roles": [
|
"roles": [
|
||||||
"admin"
|
"admin",
|
||||||
|
"search-admin",
|
||||||
|
"model-admin",
|
||||||
|
"site-admin",
|
||||||
|
"acme-group"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1091,6 +1095,22 @@
|
|||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"clientRole": true
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model-admin",
|
||||||
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search-admin",
|
||||||
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "site-admin",
|
||||||
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "acme-group",
|
||||||
|
"clientRole": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1157,6 +1177,11 @@
|
|||||||
"realmRoles": [
|
"realmRoles": [
|
||||||
"default-roles-test"
|
"default-roles-test"
|
||||||
],
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"alfresco": [
|
||||||
|
"acme-group"
|
||||||
|
]
|
||||||
|
},
|
||||||
"groups": [
|
"groups": [
|
||||||
"/Test A/Test AB",
|
"/Test A/Test AB",
|
||||||
"/Test B/Test BA"
|
"/Test B/Test BA"
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>de.acosix.alfresco.keycloak</groupId>
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
<version>1.1.0-rc7</version>
|
<version>1.1.0-rc8-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>de.acosix.alfresco.keycloak.share</artifactId>
|
<artifactId>de.acosix.alfresco.keycloak.share</artifactId>
|
||||||
|
@ -29,6 +29,11 @@ keycloak.adapter.forced-route-url=http://keycloak:8080
|
|||||||
|
|
||||||
keycloak.roles.requiredClientScopes=alfresco-role-service
|
keycloak.roles.requiredClientScopes=alfresco-role-service
|
||||||
|
|
||||||
|
keycloak.roles.resourceMapper.default.static.property.nameMappings.map.acme-group=GROUP_ACME_MEMBER
|
||||||
|
|
||||||
|
keycloak.authentication.createMissingGroupsOnLogin=true
|
||||||
|
keycloak.authentication.syncGroupMembershipOnLogin=true
|
||||||
|
|
||||||
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
|
||||||
|
@ -626,7 +626,11 @@
|
|||||||
{
|
{
|
||||||
"clientScope": "alfresco",
|
"clientScope": "alfresco",
|
||||||
"roles": [
|
"roles": [
|
||||||
"admin"
|
"admin",
|
||||||
|
"search-admin",
|
||||||
|
"model-admin",
|
||||||
|
"site-admin",
|
||||||
|
"acme-group"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1161,6 +1165,22 @@
|
|||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"clientRole": true
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model-admin",
|
||||||
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search-admin",
|
||||||
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "site-admin",
|
||||||
|
"clientRole": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "acme-group",
|
||||||
|
"clientRole": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1227,6 +1247,11 @@
|
|||||||
"realmRoles": [
|
"realmRoles": [
|
||||||
"default-roles-test"
|
"default-roles-test"
|
||||||
],
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"alfresco": [
|
||||||
|
"acme-group"
|
||||||
|
]
|
||||||
|
},
|
||||||
"groups": [
|
"groups": [
|
||||||
"/Test A/Test AB",
|
"/Test A/Test AB",
|
||||||
"/Test B/Test BA"
|
"/Test B/Test BA"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user