diff --git a/README.md b/README.md index a358832..e387cb1 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ The library is highly configurable. You configure it with properties specified ### For Activiti App Only -| Property | Default | Description | -| ----------------------------------------- | -------------- | ----------- | -| `keycloak-ext.syncGroupAs` | `organization` | When creating a new group, should it be a functional (`organization`) group or a system (`capability`) group? | -| `keycloak-ext.external.id` | `ais` | When creating a new group or registering an internal group as external, use this ID as a prefix to the external group ID. | +| Property | Default | Description | +| ---------------------------------------------- | ------- | ----------- | +| `keycloak-ext.group.capability.regex.patterns` | | When creating a new group, sync as an APS Organization, except when the specified pattern matches the role. In those cases, sync as an APS Capability. | +| `keycloak-ext.external.id` | `ais` | When creating a new group or registering an internal group as external, use this ID as a prefix to the external group ID. | ### Rare diff --git a/pom.xml b/pom.xml index d51a571..1f4a23f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.inteligr8.activiti keycloak-activiti-app-ext - 1.2.1 + 1.3.0 Keycloak Authentication & Authorization for APS @@ -12,9 +12,9 @@ 11 11 - 1.11.1.1 - 6.0.1 - 2.0.17.RELEASE + 2.0.1 + 10.0.2 + 2.5.2.RELEASE 1.7.26 @@ -37,6 +37,7 @@ ${keycloak.version} provided + com.activiti activiti-app @@ -44,6 +45,7 @@ classes provided + com.activiti activiti-app-logic @@ -52,36 +54,15 @@ - - - - io.repaint.maven - tiles-maven-plugin - 2.21 - true - - true - - com.inteligr8:maven-public-deploy-tile:[1.0.0,2.0.0) - - - - - - - alfresco-public - https://artifacts.alfresco.com/nexus/content/repositories/public + alfresco-private + https://artifacts.alfresco.com/nexus/content/groups/private activiti-releases https://artifacts.alfresco.com/nexus/content/repositories/activiti-enterprise-releases - - inteligr8-releases - https://repos.inteligr8.com/nexus/repository/inteligr8-private - diff --git a/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java b/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java index 9610660..935cd2b 100644 --- a/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java +++ b/src/main/java/com/inteligr8/activiti/keycloak/AbstractKeycloakActivitiAuthenticator.java @@ -12,6 +12,8 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.OverridingMethodsMustInvokeSuper; + import org.apache.commons.lang3.StringUtils; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; @@ -71,6 +73,7 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic protected final Set groupExcludes = new HashSet<>(); @Override + @OverridingMethodsMustInvokeSuper public void afterPropertiesSet() { if (this.regexPatterns != null) { String[] regexPatternStrs = StringUtils.split(this.regexPatterns, ','); diff --git a/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java index 94705dc..80c5798 100644 --- a/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java +++ b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiAppAuthenticator.java @@ -1,12 +1,15 @@ package com.inteligr8.activiti.keycloak; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.OverridingMethodsMustInvokeSuper; import javax.persistence.NonUniqueResultException; import org.apache.commons.lang3.StringUtils; @@ -59,16 +62,22 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu @Value("${keycloak-ext.external.id:ais}") protected String externalIdmSource; - @Value("${keycloak-ext.syncGroupAs:organization}") - protected String syncGroupAs; + @Value("${keycloak-ext.group.capability.regex.patterns:#{null}}") + protected String regexCapIncludes; + + protected final Set capIncludes = new HashSet<>(); - protected boolean syncGroupAsOrganization() { - return !this.syncGroupAsCapability(); - } - - protected boolean syncGroupAsCapability() { - return this.syncGroupAs != null && this.syncGroupAs.toLowerCase().startsWith("cap"); - } + @Override + @OverridingMethodsMustInvokeSuper + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + if (this.regexCapIncludes != null) { + String[] regexPatternStrs = StringUtils.split(this.regexCapIncludes, ','); + for (int i = 0; i < regexPatternStrs.length; i++) + this.capIncludes.add(Pattern.compile(regexPatternStrs[i])); + } + } /** * This method validates that the user exists, if not, it creates the @@ -163,8 +172,6 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu return; } - boolean syncAsOrg = this.syncGroupAsOrganization(); - // check Activiti groups User userWithGroups = this.userService.getUser(user.getId(), true); for (Group group : userWithGroups.getGroups()) { @@ -228,6 +235,8 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu if (group == null) { if (this.createMissingGroup) { this.logger.trace("Creating new group for role: {}", role); + boolean syncAsOrg = this.isRoleToBeOrganization(role.getKey()); + this.logger.trace("Creating new group as {}: {}", syncAsOrg ? "organization" : "capability", role); String name = this.keycloakRoleToApsGroupName(role.getValue()); String externalId = this.keycloakRoleToApsGroupExternalId(role.getKey()); int type = syncAsOrg ? Group.TYPE_FUNCTIONAL_GROUP : Group.TYPE_SYSTEM_GROUP; @@ -264,4 +273,17 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu return externalId; } + private boolean isRoleToBeOrganization(String role) { + if (this.capIncludes.isEmpty()) + return true; + + for (Pattern regex : this.capIncludes) { + Matcher matcher = regex.matcher(role); + if (matcher.matches()) + return false; + } + + return true; + } + } diff --git a/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiEngineAuthenticator.java b/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiEngineAuthenticator.java deleted file mode 100644 index 576ee35..0000000 --- a/src/main/java/com/inteligr8/activiti/keycloak/KeycloakActivitiEngineAuthenticator.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.inteligr8.activiti.keycloak; - -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -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("keycloak-ext.activiti-engine.authenticator") -@Lazy -public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivitiAuthenticator { - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - @Autowired - private IdentityService identityService; - - @Value("${keycloak-ext.group.prefix:KEYCLOAK_}") - private String groupPrefix; - - /** - * 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.clearNewUserDefaultGroups) { - this.logger.debug("Clearing groups: {}", user.getId()); - List 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.syncUserRoles(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 syncUserRoles(User user, Authentication auth) { - Map roles = this.getKeycloakRoles(auth); - if (roles == null) { - this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail()); - return; - } - - // check Activiti groups - List groups = this.identityService.createGroupQuery() - .groupMember(user.getEmail()) - .list(); - this.logger.debug("User is currently a member of {} groups", groups.size()); - for (Group group : groups) { - if (!group.getId().startsWith(this.groupPrefix) && this.syncInternalGroups) - continue; - - this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType()); - if (roles.remove(this.activitiGroupIdToKeycloakRole(group.getId())) != null) { - 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 roles: {}", roles); - - // check remainder/unaddressed roles - for (Entry role : roles.entrySet()) { - this.logger.trace("Inspecting role: {}", role); - - Group group = this.identityService.createGroupQuery() - .groupId(this.keycloakRoleToActivitiGroupId(role.getKey())) - .singleResult(); - if (group == null) { - if (this.createMissingGroup) { - this.logger.trace("Group does not exist; creating one"); - group = this.identityService.newGroup(this.keycloakRoleToActivitiGroupId(role.getKey())); - group.setName(role.getValue()); - this.identityService.saveGroup(group); - } else { - this.logger.info("Group does not exist; group creation is disabled: {}", role.getKey()); - } - } - - 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()); - } - } - } - - private String keycloakRoleToActivitiGroupId(String role) { - return this.groupPrefix + role; - } - - private String activitiGroupIdToKeycloakRole(String groupId) { - return groupId.startsWith(this.groupPrefix) ? groupId.substring(this.groupPrefix.length()) : groupId; - } - -}