Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
44d0bf533d | |||
a856ab7d3d | |||
5563b055e7 | |||
807294881b | |||
a42c754a09 | |||
916f371297 | |||
bbef37a222 |
2
pom.xml
2
pom.xml
@@ -4,7 +4,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.inteligr8.activiti</groupId>
|
<groupId>com.inteligr8.activiti</groupId>
|
||||||
<artifactId>keycloak-activiti-app-ext</artifactId>
|
<artifactId>keycloak-activiti-app-ext</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.1.0</version>
|
||||||
<name>Keycloak Authentication & Authorization for APS</name>
|
<name>Keycloak Authentication & Authorization for APS</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
@@ -0,0 +1,57 @@
|
|||||||
|
package com.activiti.conf;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.inteligr8.activiti.ActivitiSecurityConfigAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class/bean executes the OOTB security configuration without the
|
||||||
|
* override, so you can still use its OOTB features. This will allow you to
|
||||||
|
* enable/disable features, chain them, and uset he OOTB features as a
|
||||||
|
* fallback or failsafe.
|
||||||
|
*
|
||||||
|
* This class must be in the com.activiti.conf package so it can use protected
|
||||||
|
* fields and methods of the OOTB class instance.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
* @see com.activiti.conf.SecurityConfiguration
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ActivitiOotbSecurityConfigurationAdapter implements ActivitiSecurityConfigAdapter {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.ootbSecurityConfig.enabled:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SecurityConfiguration ootbSecurityConfig;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPriority() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
|
||||||
|
this.logger.trace("configureGlobal()");
|
||||||
|
|
||||||
|
this.logger.info("Using OOTB authentication");
|
||||||
|
|
||||||
|
// unset override (which has already been called in order to get here)
|
||||||
|
this.ootbSecurityConfig.securityConfigOverride = null;
|
||||||
|
|
||||||
|
this.ootbSecurityConfig.configureGlobal(authmanBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -4,7 +4,7 @@ import org.springframework.context.annotation.ComponentScan;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ComponentScan(basePackages = {"com.inteligr8.activiti.oidc"})
|
@ComponentScan(basePackages = {"com.inteligr8.activiti"})
|
||||||
public class OidcExtSpringComponentScanner {
|
public class KeycloakExtSpringComponentScanner {
|
||||||
|
|
||||||
}
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package com.inteligr8.activiti;
|
||||||
|
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
*/
|
||||||
|
public interface ActivitiSecurityConfigAdapter extends Comparable<ActivitiSecurityConfigAdapter> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the adapter enabled? This allows for configurable enablement.
|
||||||
|
*
|
||||||
|
* @return true if enabled; false otherwise
|
||||||
|
*/
|
||||||
|
boolean isEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lower the value, the higher the priority. The OOTB security
|
||||||
|
* configuration uses priority 0. Use negative values to supersede it.
|
||||||
|
* Anything with equal priorities should be considered unordered and may
|
||||||
|
* execute in a random order.
|
||||||
|
*
|
||||||
|
* @return A priority; may be negative or positive
|
||||||
|
*/
|
||||||
|
int getPriority();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see com.activiti.api.security.AlfrescoSecurityConfigOverride
|
||||||
|
*/
|
||||||
|
void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default int compareTo(ActivitiSecurityConfigAdapter adapter) {
|
||||||
|
return Integer.compare(this.getPriority(), adapter.getPriority());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,100 @@
|
|||||||
|
package com.inteligr8.activiti;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.activiti.api.security.AlfrescoSecurityConfigOverride;
|
||||||
|
import com.activiti.domain.idm.Group;
|
||||||
|
import com.activiti.domain.idm.Tenant;
|
||||||
|
import com.activiti.service.api.GroupService;
|
||||||
|
import com.activiti.service.idm.TenantService;
|
||||||
|
import com.activiti.service.license.LicenseService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class/bean overrides the APS security configuration with a collection
|
||||||
|
* of implementations. The OOTB extension only provides one override. This
|
||||||
|
* uses that extension point, but delegates it out to multiple possible
|
||||||
|
* implementations.
|
||||||
|
*
|
||||||
|
* Order cannot be controlled, so it should not be assumed in any adapter
|
||||||
|
* implementation.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityConfigOverride {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private List<ActivitiSecurityConfigAdapter> adapters;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private LicenseService licenseService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private TenantService tenantService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private GroupService groupService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
|
||||||
|
this.logger.trace("configureGlobal()");
|
||||||
|
|
||||||
|
Collections.sort(this.adapters);
|
||||||
|
|
||||||
|
if (this.logger.isTraceEnabled())
|
||||||
|
this.logGroups();
|
||||||
|
|
||||||
|
for (ActivitiSecurityConfigAdapter adapter : this.adapters) {
|
||||||
|
if (adapter.isEnabled()) {
|
||||||
|
this.logger.info("Security adapter enabled: {}", adapter.getClass());
|
||||||
|
adapter.configureGlobal(authmanBuilder, userDetailsService);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
this.logger.info("Security adapter disabled: {}", adapter.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logGroups() {
|
||||||
|
Long tenantId = this.findDefaultTenantId();
|
||||||
|
if (tenantId != null) {
|
||||||
|
// not first boot
|
||||||
|
this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(tenantId)));
|
||||||
|
this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(tenantId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long findDefaultTenantId() {
|
||||||
|
String defaultTenantName = this.licenseService.getDefaultTenantName();
|
||||||
|
this.logger.trace("Default Tenant: {}", defaultTenantName);
|
||||||
|
|
||||||
|
List<Tenant> tenants = this.tenantService.findTenantsByName(defaultTenantName);
|
||||||
|
if (tenants == null || tenants.isEmpty()) {
|
||||||
|
this.logger.warn("Default tenant not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tenant tenant = tenants.iterator().next();
|
||||||
|
return tenant.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<String> toGroupNames(Collection<Group> groups) {
|
||||||
|
List<String> groupNames = new ArrayList<>(groups.size());
|
||||||
|
for (Group group : groups)
|
||||||
|
groupNames.add(group.getName() + " [" + group.getExternalId() + "]");
|
||||||
|
return groupNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,54 +0,0 @@
|
|||||||
package com.inteligr8.activiti.ais;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.keycloak.KeycloakPrincipal;
|
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
|
||||||
import org.keycloak.adapters.OidcKeycloakAccount;
|
|
||||||
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
|
|
||||||
import com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationToken;
|
|
||||||
|
|
||||||
public abstract class AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected AccessToken getOidcAccessToken(Authentication auth) {
|
|
||||||
KeycloakSecurityContext ksc = this.getKeycloakSecurityContext(auth);
|
|
||||||
return ksc.getToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected KeycloakSecurityContext getKeycloakSecurityContext(Authentication auth) {
|
|
||||||
if (auth instanceof KeycloakAuthenticationToken) {
|
|
||||||
this.logger.debug("Fetching KeycloakSecurityContext from KeycloakAuthenticationToken");
|
|
||||||
if (auth.getPrincipal() instanceof KeycloakPrincipal) {
|
|
||||||
return ((KeycloakPrincipal<? extends KeycloakSecurityContext>)auth.getPrincipal()).getKeycloakSecurityContext();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else if (auth instanceof IdentityServiceAuthenticationToken) {
|
|
||||||
this.logger.debug("Fetching KeycloakSecurityContext from IdentityServiceAuthenticationToken");
|
|
||||||
OidcKeycloakAccount account = ((IdentityServiceAuthenticationToken)auth).getAccount();
|
|
||||||
return account == null ? null : account.getKeycloakSecurityContext();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Set<String> toSet(Collection<? extends GrantedAuthority> grantedAuthorities) {
|
|
||||||
Set<String> authorities = new HashSet<>(grantedAuthorities.size());
|
|
||||||
for (GrantedAuthority grantedAuthority : grantedAuthorities)
|
|
||||||
authorities.add(grantedAuthority.getAuthority());
|
|
||||||
return authorities;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,38 +0,0 @@
|
|||||||
package com.inteligr8.activiti.ais;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
|
||||||
|
|
||||||
import com.activiti.api.security.AlfrescoAuthenticationProviderOverride;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FIXME This would be nice, but with AIS enabled, it is never called. The use
|
|
||||||
* of this requires a fix from the Alfresco/Activiti team. Their AIS
|
|
||||||
* authentication logic appears to have been hastily added, breaking this
|
|
||||||
* override possibility. We are instead using the heavier weight
|
|
||||||
* `OidcSecurityConfigurationAdapter` and re-implementing the authentication
|
|
||||||
* logic discovered in the `activiti-app` project
|
|
||||||
* `com.activiti.conf.SecurityConfiguration` class.
|
|
||||||
*
|
|
||||||
* @author brian.long@yudrio.com
|
|
||||||
* @see IdentityServiceSecurityConfigurationAdapter
|
|
||||||
*/
|
|
||||||
//@Component
|
|
||||||
public class IdentityServiceAuthenticationProviderAdapter implements AlfrescoAuthenticationProviderOverride {
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("activiti-app.authenticator")
|
|
||||||
private Authenticator authenticator;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AuthenticationProvider createAuthenticationProvider() {
|
|
||||||
this.logger.trace("createAuthenticationProvider()");
|
|
||||||
return new InterceptingIdentityServiceAuthenticationProvider(this.authenticator);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package com.inteligr8.activiti.ais;
|
package com.inteligr8.activiti.ais;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -11,27 +12,31 @@ import org.springframework.security.core.userdetails.UserDetailsService;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.activiti.api.msmt.MsmtTenantResolver;
|
import com.activiti.api.msmt.MsmtTenantResolver;
|
||||||
import com.activiti.api.security.AlfrescoSecurityConfigOverride;
|
|
||||||
import com.activiti.conf.MsmtProperties;
|
import com.activiti.conf.MsmtProperties;
|
||||||
|
import com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationProvider;
|
||||||
|
import com.inteligr8.activiti.ActivitiSecurityConfigAdapter;
|
||||||
|
import com.inteligr8.activiti.auth.Authenticator;
|
||||||
|
import com.inteligr8.activiti.auth.InterceptingAuthenticationProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class/bean overrides the AIS authentication provider, enabling a more
|
* This class/bean injects a custom AIS authentication provider into the
|
||||||
* complete integration with AIS.
|
* security configuration.
|
||||||
*
|
*
|
||||||
* FIXME This is not optimal, but with AIS enabled, we cannot use the proper
|
* @author brian@inteligr8.com
|
||||||
* override.
|
* @see com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationProvider
|
||||||
*
|
|
||||||
* @author brian.long@yudrio.com
|
|
||||||
* @see IdentityServiceAuthenticationProviderAdapter
|
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class IdentityServiceSecurityConfigurationAdapter implements AlfrescoSecurityConfigOverride {
|
public class IdentityServiceSecurityConfigurationAdapter implements ActivitiSecurityConfigAdapter {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
@Value("${keycloak.ext.odic.enabled:true}")
|
@Value("${keycloak-ext.ais.enabled:false}")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
|
// this assures execution before the OOTB impl (-10 < 0)
|
||||||
|
@Value("${keycloak-ext.ais.priority:-10}")
|
||||||
|
private int priority;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected MsmtProperties msmtProperties;
|
protected MsmtProperties msmtProperties;
|
||||||
|
|
||||||
@@ -39,28 +44,36 @@ public class IdentityServiceSecurityConfigurationAdapter implements AlfrescoSecu
|
|||||||
protected MsmtTenantResolver tenantResolver;
|
protected MsmtTenantResolver tenantResolver;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("activiti-app.authenticator")
|
@Qualifier("keycloak-ext.activiti-app.authenticator")
|
||||||
private Authenticator authenticator;
|
private Authenticator authenticator;
|
||||||
|
|
||||||
protected Authenticator getAuthenticator() {
|
protected Authenticator getAuthenticator() {
|
||||||
return this.authenticator;
|
return this.authenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return this.priority;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
|
public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) {
|
||||||
this.logger.trace("configureGlobal()");
|
this.logger.trace("configureGlobal()");
|
||||||
|
|
||||||
if (this.enabled) {
|
this.logger.info("Using AIS authentication extension, featuring creation of missing users and authority synchronization");
|
||||||
this.logger.info("Using Keycloak authentication extension, featuring creation of missing users and authority synchronization");
|
|
||||||
|
IdentityServiceAuthenticationProvider provider = new IdentityServiceAuthenticationProvider();
|
||||||
InterceptingIdentityServiceAuthenticationProvider provider = new InterceptingIdentityServiceAuthenticationProvider(this.getAuthenticator());
|
if (this.msmtProperties.isMultiSchemaMultiTenantEnabled())
|
||||||
if (this.msmtProperties.isMultiSchemaMultiTenantEnabled())
|
provider.setTenantResolver(this.tenantResolver);
|
||||||
provider.setTenantResolver(this.tenantResolver);
|
provider.setUserDetailsService(userDetailsService);
|
||||||
provider.setUserDetailsService(userDetailsService);
|
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
|
||||||
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
|
|
||||||
|
auth.authenticationProvider(new InterceptingAuthenticationProvider(provider, this.getAuthenticator()));
|
||||||
authmanBuilder.authenticationProvider(provider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package com.inteligr8.activiti.ais;
|
package com.inteligr8.activiti.auth;
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
@@ -1,29 +1,35 @@
|
|||||||
package com.inteligr8.activiti.ais;
|
package com.inteligr8.activiti.auth;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
||||||
import com.activiti.security.identity.service.authentication.provider.IdentityServiceAuthenticationProvider;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class/bean extends the APS AIS OOTB authentication provider. It uses
|
* This class/bean provides a pre/post authentication capability to the
|
||||||
* an `Authenticator` to pre/post authenticate. The pre-authentication allows
|
* Spring AuthenticationProvider. The pre-authentication hook allows us to
|
||||||
* us to circumvent the problem with AIS and missing users. The
|
* circumvent the problem with authenticating missing users. The
|
||||||
* post-authentication allow us to synchronize groups/authorities.
|
* post-authentication hook allow us to synchronize groups/authorities.
|
||||||
*
|
*
|
||||||
* @author brian.long@yudrio.com
|
* @author brian@inteligr8.com
|
||||||
*/
|
*/
|
||||||
public class InterceptingIdentityServiceAuthenticationProvider extends IdentityServiceAuthenticationProvider {
|
public class InterceptingAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
private final AuthenticationProvider provider;
|
||||||
private final Authenticator authenticator;
|
private final Authenticator authenticator;
|
||||||
|
|
||||||
public InterceptingIdentityServiceAuthenticationProvider(Authenticator authenticator) {
|
public InterceptingAuthenticationProvider(AuthenticationProvider provider, Authenticator authenticator) {
|
||||||
|
this.provider = provider;
|
||||||
this.authenticator = authenticator;
|
this.authenticator = authenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authClass) {
|
||||||
|
return this.provider.supports(authClass);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Authentication authenticate(Authentication auth) throws AuthenticationException {
|
public Authentication authenticate(Authentication auth) throws AuthenticationException {
|
||||||
this.logger.trace("authenticate({})", auth.getName());
|
this.logger.trace("authenticate({})", auth.getName());
|
||||||
@@ -31,13 +37,9 @@ public class InterceptingIdentityServiceAuthenticationProvider extends IdentityS
|
|||||||
this.authenticator.preAuthenticate(auth);
|
this.authenticator.preAuthenticate(auth);
|
||||||
this.logger.debug("Pre-authenticated user: {}", auth.getName());
|
this.logger.debug("Pre-authenticated user: {}", auth.getName());
|
||||||
|
|
||||||
auth = super.authenticate(auth);
|
auth = this.provider.authenticate(auth);
|
||||||
this.logger.debug("Authenticated user '{}' with authorities: {}", auth.getName(), auth.getAuthorities());
|
this.logger.debug("Authenticated user '{}' with authorities: {}", auth.getName(), auth.getAuthorities());
|
||||||
|
|
||||||
// FIXME temporary for debugging
|
|
||||||
if (auth.getName().equals("admin@app.activiti.com"))
|
|
||||||
return auth;
|
|
||||||
|
|
||||||
this.authenticator.postAuthenticate(auth);
|
this.authenticator.postAuthenticate(auth);
|
||||||
this.logger.debug("Post-authenticated user: {}", auth.getName());
|
this.logger.debug("Post-authenticated user: {}", auth.getName());
|
||||||
|
|
@@ -0,0 +1,247 @@
|
|||||||
|
package com.inteligr8.activiti.keycloak;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.AccessToken.Access;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.util.Pair;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
import com.inteligr8.activiti.auth.Authenticator;
|
||||||
|
|
||||||
|
public abstract class AbstractKeycloakActivitiAuthenticator implements Authenticator, InitializingBean {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.createMissingUser:true}")
|
||||||
|
protected boolean createMissingUser;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.clearNewUserGroups:true}")
|
||||||
|
protected boolean clearNewUserGroups;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.createMissingGroup:true}")
|
||||||
|
protected boolean createMissingGroup;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.syncGroupAdd:true}")
|
||||||
|
protected boolean syncGroupAdd;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.syncGroupRemove:true}")
|
||||||
|
protected boolean syncGroupRemove;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.resource.include.regex.patterns:#{null}}")
|
||||||
|
protected String resourceRegexIncludes;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.group.format.regex.patterns:#{null}}")
|
||||||
|
protected String regexPatterns;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.group.format.regex.replacements:#{null}}")
|
||||||
|
protected String regexReplacements;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.group.include.regex.patterns:#{null}}")
|
||||||
|
protected String regexIncludes;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.group.exclude.regex.patterns:#{null}}")
|
||||||
|
protected String regexExcludes;
|
||||||
|
|
||||||
|
protected final List<Pair<Pattern, String>> groupFormatters = new LinkedList<>();
|
||||||
|
protected final Set<Pattern> resourceIncludes = new HashSet<>();
|
||||||
|
protected final Set<Pattern> groupIncludes = new HashSet<>();
|
||||||
|
protected final Set<Pattern> groupExcludes = new HashSet<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet() {
|
||||||
|
if (this.regexPatterns != null) {
|
||||||
|
String[] regexPatternStrs = StringUtils.split(this.regexPatterns, ',');
|
||||||
|
String[] regexReplaceStrs = this.regexReplacements == null ? new String[0] : StringUtils.split(this.regexReplacements, ",");
|
||||||
|
for (int i = 0; i < regexPatternStrs.length; i++) {
|
||||||
|
Pattern regexPattern = Pattern.compile(regexPatternStrs[i]);
|
||||||
|
String regexReplace = (i < regexReplaceStrs.length) ? regexReplaceStrs[i] : "";
|
||||||
|
this.groupFormatters.add(Pair.of(regexPattern, regexReplace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.resourceRegexIncludes != null) {
|
||||||
|
String[] regexPatternStrs = StringUtils.split(this.resourceRegexIncludes, ',');
|
||||||
|
for (int i = 0; i < regexPatternStrs.length; i++)
|
||||||
|
this.resourceIncludes.add(Pattern.compile(regexPatternStrs[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.regexIncludes != null) {
|
||||||
|
String[] regexPatternStrs = StringUtils.split(this.regexIncludes, ',');
|
||||||
|
for (int i = 0; i < regexPatternStrs.length; i++)
|
||||||
|
this.groupIncludes.add(Pattern.compile(regexPatternStrs[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.regexExcludes != null) {
|
||||||
|
String[] regexPatternStrs = StringUtils.split(this.regexExcludes, ',');
|
||||||
|
for (int i = 0; i < regexPatternStrs.length; i++)
|
||||||
|
this.groupExcludes.add(Pattern.compile(regexPatternStrs[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected Map<String, String> getRoles(Authentication auth) {
|
||||||
|
Map<String, String> authorities = new HashMap<>();
|
||||||
|
|
||||||
|
AccessToken atoken = this.getKeycloakAccessToken(auth);
|
||||||
|
if (atoken == null) {
|
||||||
|
this.logger.debug("Access token not available");
|
||||||
|
return null;
|
||||||
|
} else if (atoken.getRealmAccess() == null && atoken.getResourceAccess().isEmpty()) {
|
||||||
|
this.logger.debug("Access token has no role information");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (atoken.getRealmAccess() != null) {
|
||||||
|
this.logger.debug("Access token realm roles: {}", atoken.getRealmAccess().getRoles());
|
||||||
|
Collection<String> roles = this.filterRoles(atoken.getRealmAccess().getRoles());
|
||||||
|
Map<String, String> mappedRoles = this.formatRoles(roles);
|
||||||
|
authorities.putAll(mappedRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Entry<String, Access> resourceAccess : atoken.getResourceAccess().entrySet()) {
|
||||||
|
if (this.includeResource(resourceAccess.getKey())) {
|
||||||
|
this.logger.debug("Access token resources '{}' roles: {}", resourceAccess.getKey(), resourceAccess.getValue().getRoles());
|
||||||
|
Collection<String> roles = this.filterRoles(resourceAccess.getValue().getRoles());
|
||||||
|
Map<String, String> mappedRoles = this.formatRoles(roles);
|
||||||
|
authorities.putAll(mappedRoles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug("Access token authorities: {}", authorities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<String> filterRoles(Collection<String> unfilteredRoles) {
|
||||||
|
if (this.groupIncludes.isEmpty() && this.groupExcludes.isEmpty())
|
||||||
|
return unfilteredRoles;
|
||||||
|
|
||||||
|
Set<String> filteredRoles = new HashSet<>(unfilteredRoles.size());
|
||||||
|
|
||||||
|
for (String role : unfilteredRoles) {
|
||||||
|
boolean doInclude = this.groupIncludes.isEmpty();
|
||||||
|
for (Pattern regex : this.groupIncludes) {
|
||||||
|
Matcher matcher = regex.matcher(role);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
this.logger.debug("Role matched inclusion filter: {}", role);
|
||||||
|
doInclude = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doInclude) {
|
||||||
|
for (Pattern regex : this.groupExcludes) {
|
||||||
|
Matcher matcher = regex.matcher(role);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
this.logger.debug("Role matched exclusion filter: {}", role);
|
||||||
|
doInclude = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doInclude)
|
||||||
|
filteredRoles.add(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> formatRoles(Collection<String> unformattedRoles) {
|
||||||
|
Map<String, String> formattedRoles = new HashMap<>(unformattedRoles.size());
|
||||||
|
|
||||||
|
for (String unformattedRole : unformattedRoles) {
|
||||||
|
String formattedRole = null;
|
||||||
|
|
||||||
|
for (Pair<Pattern, String> regex : this.groupFormatters) {
|
||||||
|
Matcher matcher = regex.getFirst().matcher(unformattedRole);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
this.logger.trace("Role matched formatter: {}", unformattedRole);
|
||||||
|
formattedRole = matcher.replaceFirst(regex.getSecond());
|
||||||
|
this.logger.debug("Role formatted: {}", formattedRole);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedRoles.put(unformattedRole, formattedRole == null ? unformattedRole : formattedRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean includeResource(String resource) {
|
||||||
|
if (this.resourceIncludes.isEmpty())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (Pattern resourceInclude : this.resourceIncludes) {
|
||||||
|
Matcher matcher = resourceInclude.matcher(resource);
|
||||||
|
if (matcher.matches())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AccessToken getKeycloakAccessToken(Authentication auth) {
|
||||||
|
KeycloakSecurityContext ksc = this.getKeycloakSecurityContext(auth);
|
||||||
|
return ksc == null ? null : ksc.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected KeycloakSecurityContext getKeycloakSecurityContext(Authentication auth) {
|
||||||
|
if (auth.getCredentials() instanceof KeycloakSecurityContext) {
|
||||||
|
this.logger.debug("Found keycloak context in credentials");
|
||||||
|
return (KeycloakSecurityContext)auth.getCredentials();
|
||||||
|
} else if (auth.getPrincipal() instanceof KeycloakPrincipal) {
|
||||||
|
this.logger.debug("Found keycloak context in principal: {}", auth.getPrincipal());
|
||||||
|
return ((KeycloakPrincipal<? extends KeycloakSecurityContext>)auth.getPrincipal()).getKeycloakSecurityContext();
|
||||||
|
} else if (!(auth instanceof KeycloakAuthenticationToken)) {
|
||||||
|
this.logger.warn("Unexpected token: {}", auth.getClass());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakAuthenticationToken ktoken = (KeycloakAuthenticationToken)auth;
|
||||||
|
if (ktoken.getAccount() != null) {
|
||||||
|
this.logger.debug("Found keycloak context in account: {}", ktoken.getAccount().getPrincipal() == null ? null : ktoken.getAccount().getPrincipal().getName());
|
||||||
|
return ktoken.getAccount().getKeycloakSecurityContext();
|
||||||
|
} else {
|
||||||
|
this.logger.warn("Unable to find keycloak security context");
|
||||||
|
this.logger.debug("Principal: {}", auth.getPrincipal());
|
||||||
|
this.logger.debug("Account: {}", ktoken.getAccount());
|
||||||
|
if (auth.getPrincipal() != null)
|
||||||
|
this.logger.debug("Principal type: {}", auth.getPrincipal().getClass());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<String> toSet(Collection<? extends GrantedAuthority> grantedAuthorities) {
|
||||||
|
Set<String> authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16));
|
||||||
|
for (GrantedAuthority grantedAuthority : grantedAuthorities) {
|
||||||
|
String authority = StringUtils.trimToNull(grantedAuthority.getAuthority());
|
||||||
|
if (authority == null)
|
||||||
|
this.logger.warn("The granted authorities include an empty authority!?: '{}'", grantedAuthority.getAuthority());
|
||||||
|
authorities.add(authority);
|
||||||
|
}
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,9 @@
|
|||||||
package com.inteligr8.activiti.ais;
|
package com.inteligr8.activiti.keycloak;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ import org.keycloak.representations.AccessToken;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
@@ -38,29 +38,14 @@ import com.activiti.service.license.LicenseService;
|
|||||||
*
|
*
|
||||||
* @author brian.long@yudrio.com
|
* @author brian.long@yudrio.com
|
||||||
*/
|
*/
|
||||||
@Component("activiti-app.authenticator")
|
@Component("keycloak-ext.activiti-app.authenticator")
|
||||||
@Lazy
|
@Lazy
|
||||||
public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
|
public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAuthenticator {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
private final Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*");
|
private final Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*");
|
||||||
private final String externalIdmSource = "ais";
|
private final String externalIdmSource = "ais";
|
||||||
|
|
||||||
@Value("${keycloak-ext.createMissingUser:true}")
|
|
||||||
private boolean createMissingUser;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.clearNewUserGroups:true}")
|
|
||||||
private boolean clearNewUserGroups;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.createMissingGroup:true}")
|
|
||||||
private boolean createMissingGroup;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.syncGroupAdd:true}")
|
|
||||||
private boolean syncGroupAdd;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.syncGroupRemove:true}")
|
|
||||||
private boolean syncGroupRemove;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private LicenseService licenseService;
|
private LicenseService licenseService;
|
||||||
@@ -115,7 +100,7 @@ public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentitySer
|
|||||||
User user = this.findUser(auth, tenantId);
|
User user = this.findUser(auth, tenantId);
|
||||||
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
|
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
|
||||||
|
|
||||||
this.syncUserAuthorities(user, auth, tenantId);
|
this.syncUserRoles(user, auth, tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long findDefaultTenantId() {
|
private Long findDefaultTenantId() {
|
||||||
@@ -147,9 +132,9 @@ public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentitySer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private User createUser(Authentication auth, Long tenantId) {
|
private User createUser(Authentication auth, Long tenantId) {
|
||||||
AccessToken atoken = this.getOidcAccessToken(auth);
|
AccessToken atoken = this.getKeycloakAccessToken(auth);
|
||||||
if (atoken == null) {
|
if (atoken == null) {
|
||||||
this.logger.debug("The OIDC access token could not be found; using email to determine names: {}", auth.getName());
|
this.logger.debug("The keycloak access token could not be found; using email to determine names: {}", auth.getName());
|
||||||
Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName());
|
Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName());
|
||||||
if (!emailNamesMatcher.matches()) {
|
if (!emailNamesMatcher.matches()) {
|
||||||
this.logger.warn("The email address could not be parsed for names: {}", auth.getName());
|
this.logger.warn("The email address could not be parsed for names: {}", auth.getName());
|
||||||
@@ -164,47 +149,51 @@ public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentitySer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncUserAuthorities(User user, Authentication auth, Long tenantId) {
|
private void syncUserRoles(User user, Authentication auth, Long tenantId) {
|
||||||
Set<String> authorities = this.toSet(auth.getAuthorities());
|
Map<String, String> roles = this.getRoles(auth);
|
||||||
this.logger.debug("OIDC authorities: {}", authorities);
|
if (roles == null) {
|
||||||
|
this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check Activiti groups
|
// check Activiti groups
|
||||||
User userWithGroups = this.userService.findUserByEmailFetchGroups(user.getEmail());
|
User userWithGroups = this.userService.findUserByEmailFetchGroups(user.getEmail());
|
||||||
for (Group group : userWithGroups.getGroups()) {
|
for (Group group : userWithGroups.getGroups()) {
|
||||||
this.logger.trace("Inspecting group: {} => ", group.getId(), group.getExternalId());
|
this.logger.trace("Inspecting group: {} => ", group.getId(), group.getName());
|
||||||
|
|
||||||
if (authorities.remove(group.getExternalId())) {
|
if (group.getExternalId() == null) {
|
||||||
|
// skip APS system groups
|
||||||
|
} else if (roles.remove(group.getExternalId()) != null) {
|
||||||
// all good
|
// all good
|
||||||
} else {
|
} else {
|
||||||
if (this.syncGroupRemove) {
|
if (this.syncGroupRemove) {
|
||||||
this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getExternalId());
|
this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getName());
|
||||||
this.groupService.deleteUserFromGroup(group, userWithGroups);
|
this.groupService.deleteUserFromGroup(group, userWithGroups);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug("User/group membership sync disabled; not removing user from group: {} => {}", user.getExternalId(), group.getExternalId());
|
this.logger.debug("User/group membership sync disabled; not removing user from group: {} => {}", user.getExternalId(), group.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add remaining authorities into Activiti
|
// add remaining authorities into Activiti
|
||||||
for (String authority : authorities) {
|
for (Entry<String, String> role : roles.entrySet()) {
|
||||||
this.logger.trace("Syncing group membership: {}", authority);
|
this.logger.trace("Syncing group membership: {}", role);
|
||||||
|
|
||||||
Group group = this.groupService.getGroupByExternalId(authority);
|
Group group = this.groupService.getGroupByExternalId(role.getKey());
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
if (this.createMissingGroup) {
|
if (this.createMissingGroup) {
|
||||||
this.logger.trace("Creating new group: {}", authority);
|
this.logger.trace("Creating new group: {}", role);
|
||||||
String shortAuthority = authority.replaceFirst("[A-Z]+_", "");
|
group = this.groupService.createGroupFromExternalStore(role.getValue(), tenantId, Group.TYPE_SYSTEM_GROUP, null, role.getKey(), new Date());
|
||||||
group = this.groupService.createGroupFromExternalStore(shortAuthority, tenantId, Group.TYPE_SYSTEM_GROUP, null, authority, new Date());
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug("Group does not exist; group creation is disabled: {}", authority);
|
this.logger.debug("Group does not exist; group creation is disabled: {}", role);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group != null && this.syncGroupAdd) {
|
if (group != null && this.syncGroupAdd) {
|
||||||
this.logger.trace("Adding user '{}' from group '{}'", user.getExternalId(), group.getExternalId());
|
this.logger.trace("Adding user '{}' to group '{}'", user.getExternalId(), group.getName());
|
||||||
this.groupService.addUserToGroup(group, userWithGroups);
|
this.groupService.addUserToGroup(group, userWithGroups);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug("User/group membership sync disabled; not adding user to group: {} => {}", user.getExternalId(), group.getExternalId());
|
this.logger.debug("User/group membership sync disabled; not adding user to group: {} => {}", user.getExternalId(), group.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,7 +1,8 @@
|
|||||||
package com.inteligr8.activiti.ais;
|
package com.inteligr8.activiti.keycloak;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import org.activiti.engine.IdentityService;
|
import org.activiti.engine.IdentityService;
|
||||||
import org.activiti.engine.identity.Group;
|
import org.activiti.engine.identity.Group;
|
||||||
@@ -21,29 +22,17 @@ import org.springframework.stereotype.Component;
|
|||||||
*
|
*
|
||||||
* @author brian.long@yudrio.com
|
* @author brian.long@yudrio.com
|
||||||
*/
|
*/
|
||||||
@Component("activiti.authenticator")
|
@Component("keycloak-ext.activiti-engine.authenticator")
|
||||||
@Lazy
|
@Lazy
|
||||||
public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
|
public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivitiAuthenticator {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
@Value("${keycloak-ext.createMissingUser:true}")
|
|
||||||
private boolean createMissingUser;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.clearNewUserGroups:true}")
|
|
||||||
private boolean clearNewUserGroups;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.createMissingGroup:true}")
|
|
||||||
private boolean createMissingGroup;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.syncGroupAdd:true}")
|
|
||||||
private boolean syncGroupAdd;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.syncGroupRemove:true}")
|
|
||||||
private boolean syncGroupRemove;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IdentityService identityService;
|
private IdentityService identityService;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.group.prefix:KEYCLOAK_}")
|
||||||
|
private String groupPrefix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method validates that the user exists, if not, it creates the
|
* This method validates that the user exists, if not, it creates the
|
||||||
@@ -83,7 +72,7 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
|
|||||||
User user = this.findUser(auth);
|
User user = this.findUser(auth);
|
||||||
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getEmail());
|
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getEmail());
|
||||||
|
|
||||||
this.syncUserAuthorities(user, auth);
|
this.syncUserRoles(user, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
private User findUser(Authentication auth) {
|
private User findUser(Authentication auth) {
|
||||||
@@ -103,9 +92,12 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncUserAuthorities(User user, Authentication auth) {
|
private void syncUserRoles(User user, Authentication auth) {
|
||||||
Set<String> authorities = this.toSet(auth.getAuthorities());
|
Map<String, String> roles = this.getRoles(auth);
|
||||||
this.logger.debug("OIDC authorities: {}", authorities);
|
if (roles == null) {
|
||||||
|
this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check Activiti groups
|
// check Activiti groups
|
||||||
List<Group> groups = this.identityService.createGroupQuery()
|
List<Group> groups = this.identityService.createGroupQuery()
|
||||||
@@ -113,8 +105,11 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
|
|||||||
.list();
|
.list();
|
||||||
this.logger.debug("User is currently a member of {} groups", groups.size());
|
this.logger.debug("User is currently a member of {} groups", groups.size());
|
||||||
for (Group group : groups) {
|
for (Group group : groups) {
|
||||||
|
if (!group.getId().startsWith(this.groupPrefix))
|
||||||
|
continue;
|
||||||
|
|
||||||
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType());
|
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType());
|
||||||
if (authorities.remove(group.getName())) {
|
if (roles.remove(group.getId().substring(this.groupPrefix.length())) != null) {
|
||||||
this.logger.trace("Group and membership already exist: {} => {}", user.getEmail(), group.getName());
|
this.logger.trace("Group and membership already exist: {} => {}", user.getEmail(), group.getName());
|
||||||
// already a member of the group
|
// already a member of the group
|
||||||
} else {
|
} else {
|
||||||
@@ -127,20 +122,20 @@ public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug("Unaddressed OIDC authorities: {}", authorities);
|
this.logger.debug("Unaddressed OIDC roles: {}", roles);
|
||||||
|
|
||||||
// check remainder/unaddressed authorities
|
// check remainder/unaddressed roles
|
||||||
for (String authority : authorities) {
|
for (Entry<String, String> role : roles.entrySet()) {
|
||||||
this.logger.trace("Inspecting authority: {}", authority);
|
this.logger.trace("Inspecting role: {}", role);
|
||||||
|
|
||||||
Group group = this.identityService.createGroupQuery()
|
Group group = this.identityService.createGroupQuery()
|
||||||
.groupName(authority)
|
.groupId(this.groupPrefix + role.getKey())
|
||||||
.singleResult();
|
.singleResult();
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
if (this.createMissingGroup) {
|
if (this.createMissingGroup) {
|
||||||
this.logger.trace("Group does not exist; creating one");
|
this.logger.trace("Group does not exist; creating one");
|
||||||
group = this.identityService.newGroup(authority);
|
group = this.identityService.newGroup(this.groupPrefix + role.getKey());
|
||||||
group.setName(authority);
|
group.setName(role.getValue());
|
||||||
this.identityService.saveGroup(group);
|
this.identityService.saveGroup(group);
|
||||||
} else {
|
} else {
|
||||||
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
@@ -0,0 +1,67 @@
|
|||||||
|
package com.inteligr8.activiti.keycloak;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.inteligr8.activiti.ActivitiSecurityConfigAdapter;
|
||||||
|
import com.inteligr8.activiti.auth.Authenticator;
|
||||||
|
import com.inteligr8.activiti.auth.InterceptingAuthenticationProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class/bean injects a custom keycloak authentication provider into the
|
||||||
|
* security configuration.
|
||||||
|
*
|
||||||
|
* @author brian@inteligr8.com
|
||||||
|
* @see org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class KeycloakSecurityConfigurationAdapter implements ActivitiSecurityConfigAdapter {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.keycloak.enabled:false}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
// this assures execution before the OOTB impl (-10 < 0)
|
||||||
|
@Value("${keycloak-ext.keycloak.priority:-5}")
|
||||||
|
private int priority;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("keycloak-ext.activiti-app.authenticator")
|
||||||
|
private Authenticator authenticator;
|
||||||
|
|
||||||
|
protected Authenticator getAuthenticator() {
|
||||||
|
return this.authenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return this.priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) {
|
||||||
|
this.logger.trace("configureGlobal()");
|
||||||
|
|
||||||
|
this.logger.info("Using Keycloak authentication extension, featuring creation of missing users and authority synchronization");
|
||||||
|
|
||||||
|
KeycloakAuthenticationProvider provider = new KeycloakAuthenticationProvider();
|
||||||
|
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
|
||||||
|
|
||||||
|
auth.authenticationProvider(new InterceptingAuthenticationProvider(provider, this.getAuthenticator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user