Commit e7b6bd64 authored by Brian Long's avatar Brian Long
Browse files

Merge branch 'develop' into stable

parents 343e1b65 25093cd8
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -42,8 +42,8 @@ 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.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
+8 −27
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.inteligr8.activiti</groupId>
	<artifactId>keycloak-activiti-app-ext</artifactId>
	<version>1.2.1</version>
	<version>1.3.0</version>
	<name>Keycloak Authentication &amp; Authorization for APS</name>

	<properties>
@@ -12,9 +12,9 @@
		<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>
		<aps.version>2.0.1</aps.version>
		<keycloak.version>10.0.2</keycloak.version>
		<spring-security-oauth2.version>2.5.2.RELEASE</spring-security-oauth2.version>
		<slf4j.version>1.7.26</slf4j.version>
	</properties>

@@ -37,6 +37,7 @@
			<version>${keycloak.version}</version>
			<scope>provided</scope>
		</dependency>
		<!-- Needed for Activiti App Identity Service inheritance/override -->
		<dependency>
			<groupId>com.activiti</groupId>
			<artifactId>activiti-app</artifactId>
@@ -44,6 +45,7 @@
			<classifier>classes</classifier>
			<scope>provided</scope>
		</dependency>
		<!-- Needed for the Activiti App Public API -->
		<dependency>
			<groupId>com.activiti</groupId>
			<artifactId>activiti-app-logic</artifactId>
@@ -52,36 +54,15 @@
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>io.repaint.maven</groupId>
				<artifactId>tiles-maven-plugin</artifactId>
				<version>2.21</version>
				<extensions>true</extensions>
				<configuration>
					<filtering>true</filtering>
					<tiles>
						<tile>com.inteligr8:maven-public-deploy-tile:[1.0.0,2.0.0)</tile>
					</tiles>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>alfresco-public</id>
			<url>https://artifacts.alfresco.com/nexus/content/repositories/public</url>
			<id>alfresco-private</id>
			<url>https://artifacts.alfresco.com/nexus/content/groups/private</url>
		</repository>
		<repository>
			<id>activiti-releases</id>
			<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-enterprise-releases</url>
		</repository>
		<repository>
			<id>inteligr8-releases</id>
			<url>https://repos.inteligr8.com/nexus/repository/inteligr8-private</url>
		</repository>
	</repositories>
	
	<distributionManagement>
+3 −0
Original line number Diff line number Diff line
@@ -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<Pattern> groupExcludes = new HashSet<>();
    
    @Override
    @OverridingMethodsMustInvokeSuper
    public void afterPropertiesSet() {
    	if (this.regexPatterns != null) {
    		String[] regexPatternStrs = StringUtils.split(this.regexPatterns, ',');
+33 −11
Original line number Diff line number Diff line
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,15 +62,21 @@ 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 boolean syncGroupAsOrganization() {
    	return !this.syncGroupAsCapability();
    }
    protected final Set<Pattern> capIncludes = new HashSet<>();
    
    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]));
    	}
	}
    
    /**
@@ -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;
    }
    
}
+0 −162
Original line number Diff line number Diff line
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<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.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<String, String> 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<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) {
    		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<String, String> 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;
    }
    
}