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;
- }
-
-}