initial checkin
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Maven
|
||||
target
|
||||
pom.xml.versionsBackup
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
|
||||
# Visual Studio Code
|
||||
.factorypath
|
65
pom.xml
Normal file
65
pom.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.inteligr8.activiti</groupId>
|
||||
<artifactId>oidc-activiti-app-ext</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>Keycloak Authentication & Authorization for APS</name>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<maven.compiler.release>11</maven.compiler.release>
|
||||
|
||||
<aps.version>1.11.1.1</aps.version>
|
||||
<keycloak.version>6.0.1</keycloak.version>
|
||||
<spring-security-oauth2.version>2.0.17.RELEASE</spring-security-oauth2.version>
|
||||
<slf4j.version>1.7.26</slf4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth</groupId>
|
||||
<artifactId>spring-security-oauth2</artifactId>
|
||||
<version>${spring-security-oauth2.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>activiti-app</artifactId>
|
||||
<version>${aps.version}</version>
|
||||
<classifier>classes</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>activiti-app-logic</artifactId>
|
||||
<version>${aps.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>alfresco-public</id>
|
||||
<url>https://artifacts.alfresco.com/nexus/content/repositories/public</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>activiti-releases</id>
|
||||
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-enterprise-releases</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
@@ -0,0 +1,10 @@
|
||||
package com.activiti.extension.conf;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = {"com.inteligr8.activiti.oidc"})
|
||||
public class OidcExtSpringComponentScanner {
|
||||
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
}
|
14
src/main/java/com/inteligr8/activiti/ais/Authenticator.java
Normal file
14
src/main/java/com/inteligr8/activiti/ais/Authenticator.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.inteligr8.activiti.ais;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
public interface Authenticator {
|
||||
|
||||
default void preAuthenticate(Authentication authentication) throws AuthenticationException {
|
||||
}
|
||||
|
||||
default void postAuthenticate(Authentication authentication) throws AuthenticationException {
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,211 @@
|
||||
package com.inteligr8.activiti.ais;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
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.context.annotation.Lazy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.activiti.domain.idm.Group;
|
||||
import com.activiti.domain.idm.Tenant;
|
||||
import com.activiti.domain.idm.User;
|
||||
import com.activiti.service.api.GroupService;
|
||||
import com.activiti.service.api.UserService;
|
||||
import com.activiti.service.idm.TenantService;
|
||||
import com.activiti.service.license.LicenseService;
|
||||
|
||||
/**
|
||||
* This class/bean implements an Open ID Connect authenticator for Alfresco
|
||||
* Process Services that supports the creation of missing users and groups and
|
||||
* synchronizes user/group membership. This is configurable using several
|
||||
* Spring property values starting with the `keycloak-ext.` prefix.
|
||||
*
|
||||
* This implements an internal Authenticator so other authenticators could be
|
||||
* created in the future.
|
||||
*
|
||||
* FIXME This implements is not good for multi-tenancy.
|
||||
*
|
||||
* @author brian.long@yudrio.com
|
||||
*/
|
||||
@Component("activiti-app.authenticator")
|
||||
@Lazy
|
||||
public class IdentityServiceActivitiAppAuthenticator extends AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
|
||||
|
||||
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 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
|
||||
private LicenseService licenseService;
|
||||
|
||||
@Autowired
|
||||
private TenantService tenantService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private GroupService groupService;
|
||||
|
||||
/**
|
||||
* This method validates that the user exists, if not, it creates the
|
||||
* missing user. Without this functionality, SSO straight up fails in APS.
|
||||
*/
|
||||
@Override
|
||||
public void preAuthenticate(Authentication auth) throws AuthenticationException {
|
||||
Long tenantId = this.findDefaultTenantId();
|
||||
this.logger.trace("Tenant ID: {}", tenantId);
|
||||
|
||||
User user = this.findUser(auth, tenantId);
|
||||
if (user == null) {
|
||||
if (this.createMissingUser) {
|
||||
this.logger.debug("User does not yet exist; creating the user: {}", auth.getName());
|
||||
|
||||
user = this.createUser(auth, tenantId);
|
||||
this.logger.debug("Created user: {} => {}", user.getId(), user.getExternalId());
|
||||
|
||||
if (this.clearNewUserGroups) {
|
||||
this.logger.debug("Clearing groups: {}", user.getId());
|
||||
// fetch and remove default groups
|
||||
user = this.userService.findUserByEmailFetchGroups(user.getEmail());
|
||||
for (Group group : user.getGroups())
|
||||
this.groupService.deleteUserFromGroup(group, user);
|
||||
}
|
||||
} else {
|
||||
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method validates that the groups exist, if not, it creates the
|
||||
* missing ones. Without this functionality, SSO works, but the user's
|
||||
* authorities are not synchronized.
|
||||
*/
|
||||
@Override
|
||||
public void postAuthenticate(Authentication auth) throws AuthenticationException {
|
||||
Long tenantId = this.findDefaultTenantId();
|
||||
User user = this.findUser(auth, tenantId);
|
||||
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
|
||||
|
||||
this.syncUserAuthorities(user, auth, 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 User findUser(Authentication auth, Long tenantId) {
|
||||
String email = auth.getName();
|
||||
|
||||
User user = this.userService.findUserByEmailAndTenantId(email, tenantId);
|
||||
if (user == null) {
|
||||
this.logger.debug("User does not exist in tenant; trying tenant-less lookup: {}", email);
|
||||
user = this.userService.findUserByEmail(email);
|
||||
} else {
|
||||
this.logger.trace("Found user: {}", user.getId());
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private User createUser(Authentication auth, Long tenantId) {
|
||||
AccessToken atoken = this.getOidcAccessToken(auth);
|
||||
if (atoken == null) {
|
||||
this.logger.debug("The OIDC access token could not be found; using email to determine names: {}", auth.getName());
|
||||
Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName());
|
||||
if (!emailNamesMatcher.matches()) {
|
||||
this.logger.warn("The email address could not be parsed for names: {}", auth.getName());
|
||||
return this.userService.createNewUserFromExternalStore(auth.getName(), "Unknown", "User", tenantId, auth.getName(), this.externalIdmSource, new Date());
|
||||
} else {
|
||||
String firstName = StringUtils.capitalize(emailNamesMatcher.group(1));
|
||||
String lastName = StringUtils.capitalize(emailNamesMatcher.group(2));
|
||||
return this.userService.createNewUserFromExternalStore(auth.getName(), firstName, lastName, tenantId, auth.getName(), this.externalIdmSource, new Date());
|
||||
}
|
||||
} else {
|
||||
return this.userService.createNewUserFromExternalStore(auth.getName(), atoken.getGivenName(), atoken.getFamilyName(), tenantId, auth.getName(), this.externalIdmSource, new Date());
|
||||
}
|
||||
}
|
||||
|
||||
private void syncUserAuthorities(User user, Authentication auth, Long tenantId) {
|
||||
Set<String> authorities = this.toSet(auth.getAuthorities());
|
||||
this.logger.debug("OIDC authorities: {}", authorities);
|
||||
|
||||
// check Activiti groups
|
||||
User userWithGroups = this.userService.findUserByEmailFetchGroups(user.getEmail());
|
||||
for (Group group : userWithGroups.getGroups()) {
|
||||
this.logger.trace("Inspecting group: {} => ", group.getId(), group.getExternalId());
|
||||
|
||||
if (authorities.remove(group.getExternalId())) {
|
||||
// all good
|
||||
} else {
|
||||
if (this.syncGroupRemove) {
|
||||
this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getExternalId());
|
||||
this.groupService.deleteUserFromGroup(group, userWithGroups);
|
||||
} else {
|
||||
this.logger.debug("User/group membership sync disabled; not removing user from group: {} => {}", user.getExternalId(), group.getExternalId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add remaining authorities into Activiti
|
||||
for (String authority : authorities) {
|
||||
this.logger.trace("Syncing group membership: {}", authority);
|
||||
|
||||
Group group = this.groupService.getGroupByExternalId(authority);
|
||||
if (group == null) {
|
||||
if (this.createMissingGroup) {
|
||||
this.logger.trace("Creating new group: {}", authority);
|
||||
String shortAuthority = authority.replaceFirst("[A-Z]+_", "");
|
||||
group = this.groupService.createGroupFromExternalStore(shortAuthority, tenantId, Group.TYPE_SYSTEM_GROUP, null, authority, new Date());
|
||||
} else {
|
||||
this.logger.debug("Group does not exist; group creation is disabled: {}", authority);
|
||||
}
|
||||
}
|
||||
|
||||
if (group != null && this.syncGroupAdd) {
|
||||
this.logger.trace("Adding user '{}' from group '{}'", user.getExternalId(), group.getExternalId());
|
||||
this.groupService.addUserToGroup(group, userWithGroups);
|
||||
} else {
|
||||
this.logger.debug("User/group membership sync disabled; not adding user to group: {} => {}", user.getExternalId(), group.getExternalId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,159 @@
|
||||
package com.inteligr8.activiti.ais;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.activiti.engine.IdentityService;
|
||||
import org.activiti.engine.identity.Group;
|
||||
import org.activiti.engine.identity.User;
|
||||
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.context.annotation.Lazy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* This is an unused implementation for non-APS installation. It is not tested
|
||||
* and probably pointless.
|
||||
*
|
||||
* @author brian.long@yudrio.com
|
||||
*/
|
||||
@Component("activiti.authenticator")
|
||||
@Lazy
|
||||
public class IdentityServiceActivitiEngineAuthenticator extends AbstractIdentityServiceActivitiAuthenticator implements Authenticator {
|
||||
|
||||
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
|
||||
private IdentityService identityService;
|
||||
|
||||
/**
|
||||
* This method validates that the user exists, if not, it creates the
|
||||
* missing user. Without this functionality, SSO straight up fails.
|
||||
*/
|
||||
@Override
|
||||
public void preAuthenticate(Authentication auth) throws AuthenticationException {
|
||||
User user = this.findUser(auth);
|
||||
if (user == null) {
|
||||
if (this.createMissingUser) {
|
||||
this.logger.debug("User does not yet exist; creating the user: {}", auth.getName());
|
||||
|
||||
user = this.createUser(auth);
|
||||
this.logger.debug("Created user: {} => {}", user.getId(), user.getEmail());
|
||||
|
||||
if (this.clearNewUserGroups) {
|
||||
this.logger.debug("Clearing groups: {}", user.getId());
|
||||
List<Group> groups = this.identityService.createGroupQuery()
|
||||
.groupMember(user.getId())
|
||||
.list();
|
||||
for (Group group : groups)
|
||||
this.identityService.deleteMembership(user.getId(), group.getId());
|
||||
}
|
||||
} else {
|
||||
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method validates that the groups exist, if not, it creates the
|
||||
* missing ones. Without this functionality, SSO works, but the user's
|
||||
* authorities are not synchronized.
|
||||
*/
|
||||
@Override
|
||||
public void postAuthenticate(Authentication auth) throws AuthenticationException {
|
||||
User user = this.findUser(auth);
|
||||
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getEmail());
|
||||
|
||||
this.syncUserAuthorities(user, auth);
|
||||
}
|
||||
|
||||
private User findUser(Authentication auth) {
|
||||
String email = auth.getName();
|
||||
|
||||
User user = this.identityService.createUserQuery()
|
||||
.userEmail(email)
|
||||
.singleResult();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private User createUser(Authentication auth) {
|
||||
User user = this.identityService.newUser(auth.getName());
|
||||
user.setEmail(auth.getName());
|
||||
this.identityService.saveUser(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void syncUserAuthorities(User user, Authentication auth) {
|
||||
Set<String> authorities = this.toSet(auth.getAuthorities());
|
||||
this.logger.debug("OIDC authorities: {}", authorities);
|
||||
|
||||
// check Activiti groups
|
||||
List<Group> groups = this.identityService.createGroupQuery()
|
||||
.groupMember(user.getEmail())
|
||||
.list();
|
||||
this.logger.debug("User is currently a member of {} groups", groups.size());
|
||||
for (Group group : groups) {
|
||||
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType());
|
||||
if (authorities.remove(group.getName())) {
|
||||
this.logger.trace("Group and membership already exist: {} => {}", user.getEmail(), group.getName());
|
||||
// already a member of the group
|
||||
} else {
|
||||
if (this.syncGroupRemove) {
|
||||
this.logger.trace("Group membership not in OIDC token; removing from group: {} => {}", user.getEmail(), group.getName());
|
||||
this.identityService.deleteMembership(user.getId(), group.getId());
|
||||
} else {
|
||||
this.logger.debug("User/group membership sync disabled; not removing user from group: {} => {}", user.getId(), group.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug("Unaddressed OIDC authorities: {}", authorities);
|
||||
|
||||
// check remainder/unaddressed authorities
|
||||
for (String authority : authorities) {
|
||||
this.logger.trace("Inspecting authority: {}", authority);
|
||||
|
||||
Group group = this.identityService.createGroupQuery()
|
||||
.groupName(authority)
|
||||
.singleResult();
|
||||
if (group == null) {
|
||||
if (this.createMissingGroup) {
|
||||
this.logger.trace("Group does not exist; creating one");
|
||||
group = this.identityService.newGroup(authority);
|
||||
group.setName(authority);
|
||||
this.identityService.saveGroup(group);
|
||||
} else {
|
||||
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (group != null && this.syncGroupAdd) {
|
||||
this.logger.trace("Group membership not in Activiti; adding to group: {} => {}", user.getEmail(), group.getName());
|
||||
this.identityService.createMembership(user.getId(), group.getId());
|
||||
} else {
|
||||
this.logger.debug("User/group membership sync disabled; not adding user to group: {} => {}", user.getId(), group.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
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.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.activiti.api.msmt.MsmtTenantResolver;
|
||||
import com.activiti.api.security.AlfrescoSecurityConfigOverride;
|
||||
import com.activiti.conf.MsmtProperties;
|
||||
|
||||
/**
|
||||
* This class/bean overrides the AIS authentication provider, enabling a more
|
||||
* complete integration with AIS.
|
||||
*
|
||||
* FIXME This is not optimal, but with AIS enabled, we cannot use the proper
|
||||
* override.
|
||||
*
|
||||
* @author brian.long@yudrio.com
|
||||
* @see IdentityServiceAuthenticationProviderAdapter
|
||||
*/
|
||||
@Component
|
||||
public class IdentityServiceSecurityConfigurationAdapter implements AlfrescoSecurityConfigOverride {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@Value("${keycloak.ext.odic.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@Autowired
|
||||
protected MsmtProperties msmtProperties;
|
||||
|
||||
@Autowired(required = false) // Only when multi-schema multi-tenant is enabled
|
||||
protected MsmtTenantResolver tenantResolver;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("activiti-app.authenticator")
|
||||
private Authenticator authenticator;
|
||||
|
||||
protected Authenticator getAuthenticator() {
|
||||
return this.authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
|
||||
this.logger.trace("configureGlobal()");
|
||||
|
||||
if (this.enabled) {
|
||||
this.logger.info("Using Keycloak authentication extension, featuring creation of missing users and authority synchronization");
|
||||
|
||||
InterceptingIdentityServiceAuthenticationProvider provider = new InterceptingIdentityServiceAuthenticationProvider(this.getAuthenticator());
|
||||
if (this.msmtProperties.isMultiSchemaMultiTenantEnabled())
|
||||
provider.setTenantResolver(this.tenantResolver);
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
|
||||
|
||||
authmanBuilder.authenticationProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package com.inteligr8.activiti.ais;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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
|
||||
* an `Authenticator` to pre/post authenticate. The pre-authentication allows
|
||||
* us to circumvent the problem with AIS and missing users. The
|
||||
* post-authentication allow us to synchronize groups/authorities.
|
||||
*
|
||||
* @author brian.long@yudrio.com
|
||||
*/
|
||||
public class InterceptingIdentityServiceAuthenticationProvider extends IdentityServiceAuthenticationProvider {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private final Authenticator authenticator;
|
||||
|
||||
public InterceptingIdentityServiceAuthenticationProvider(Authenticator authenticator) {
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication auth) throws AuthenticationException {
|
||||
this.logger.trace("authenticate({})", auth.getName());
|
||||
|
||||
this.authenticator.preAuthenticate(auth);
|
||||
this.logger.debug("Pre-authenticated user: {}", auth.getName());
|
||||
|
||||
auth = super.authenticate(auth);
|
||||
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.logger.debug("Post-authenticated user: {}", auth.getName());
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user