21 Commits

Author SHA1 Message Date
0d402f6014 Merge branch 'develop' into stable 2022-07-01 12:15:48 -04:00
f000f1b134 updated README 2022-07-01 12:15:30 -04:00
e7b6bd644e Merge branch 'develop' into stable 2022-07-01 12:14:32 -04:00
25093cd822 using first version for most compatibility 2022-07-01 12:07:00 -04:00
70ff0b5c5c added APS v2.x compatibility 2022-07-01 12:06:30 -04:00
343e1b65b9 added password resetter 2022-01-24 15:29:17 -05:00
d10ff1103d added password resetter 2022-01-24 15:27:59 -05:00
f5eefdb544 flipped org/cap inclusion because caps are fewer 2021-11-10 14:31:39 -05:00
28a6f4d101 allowing for more dynamic organization/capability sync control 2021-11-10 13:56:42 -05:00
07a5ed959a moved distman to repo; pruned repos 2021-09-01 14:50:13 -04:00
14487b62eb v1.2.1 pom 2021-08-31 19:55:19 -04:00
e87a6b68a7 Merge branch 'develop' into stable 2021-08-31 19:54:53 -04:00
19f21fdd5c fixed group multi-match snafu on startup 2021-08-31 19:54:39 -04:00
5ecb627dbf Merge branch 'develop' into stable 2021-08-27 00:23:17 -04:00
df37818f09 v1.2.x pom 2021-08-27 00:22:37 -04:00
f3b70c1574 refactored tenant handling 2021-08-27 00:22:06 -04:00
ea487fee31 v1.1.4 pom 2021-08-25 15:54:37 -04:00
9f9ededab2 Merge branch 'develop' into stable 2021-08-25 15:53:50 -04:00
116e22bbd6 refactored tenant handling; outputing all tenant groups 2021-08-25 15:53:13 -04:00
f76105b979 Merge branch 'develop' into stable 2021-08-24 21:22:46 -04:00
9ad7a9e560 deploy from private to public 2021-08-24 21:22:22 -04:00
11 changed files with 435 additions and 369 deletions

View File

@@ -20,6 +20,13 @@ The installation is simple. Just include the JAR in the classpath of your Activ
Notice the use of `PostResources` instead of `PreResources`. This library needs to be loaded after the web application. This is the best way to load any other extensions or customization to the Activiti App, including `JavaDelegate` implementations.
## Support Matrix
| Keycloak Activiti App Extension | Activiti App |
| ------------------------------- | --------------- |
| v1.0 - v1.2 | v1.11.x |
| v1.3+ | v1.11.x - v2.3+ |
## Configuration
The library is highly configurable. You configure it with properties specified in the `activiti-app.properties` file, which exists somewhere in the root of the classpath. That is typically in the `lib` folder. The properties to configure are enumerated in the table below.
@@ -42,8 +49,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

37
pom.xml
View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.inteligr8.activiti</groupId>
<artifactId>keycloak-activiti-app-ext</artifactId>
<version>1.1.3</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,42 +54,21 @@
</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>
<repository>
<id>inteligr8-releases</id>
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-private</url>
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
</repository>
<snapshotRepository>
<id>inteligr8-snapshots</id>

View File

@@ -0,0 +1,128 @@
package com.inteligr8.activiti;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.stereotype.Component;
import com.activiti.domain.idm.Group;
import com.activiti.domain.idm.GroupCapability;
import com.activiti.domain.idm.Tenant;
import com.activiti.service.api.GroupService;
/**
* 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 ActivitiAppAdminGroupFixer implements DataFixer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final List<String> adminCapabilities = Arrays.asList(
"access-all-models-in-tenant",
"access-editor",
"access-reports",
"publish-app-to-dashboard",
"tenant-admin",
"tenant-admin-api",
"upload-license");
@Autowired(required = false)
private GroupService groupService;
@Autowired
private TenantFinderService tenantFinderService;
@Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId;
@Value("${keycloak-ext.group.admins.validate:false}")
private boolean validateAdministratorsGroup;
@Override
public void fix() {
this.logger.trace("fix()");
if (this.logger.isTraceEnabled())
this.logGroups();
if (this.validateAdministratorsGroup)
this.validateAdmins();
}
private void logGroups() {
if (this.groupService == null)
return;
Collection<Tenant> tenants = this.tenantFinderService.getTenants();
for (Tenant tenant : tenants) {
this.logger.trace("Tenant: {} => {}", tenant.getId(), tenant.getName());
this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(tenant.getId())));
this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(tenant.getId())));
}
this.logger.trace("Tenant: null");
this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(null)));
this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(null)));
}
private void validateAdmins() {
if (this.groupService == null)
return;
Long tenantId = this.tenantFinderService.findTenantId();
Group group = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group == null) {
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
if (!groups.isEmpty())
group = groups.iterator().next();
}
if (group == null) {
this.logger.info("Creating group: {} ({})", this.adminGroupName, this.adminGroupExternalId);
if (this.adminGroupExternalId != null) {
group = this.groupService.createGroupFromExternalStore(
this.adminGroupExternalId, tenantId, Group.TYPE_SYSTEM_GROUP, null, this.adminGroupName, new Date());
} else {
group = this.groupService.createGroup(this.adminGroupName, tenantId, Group.TYPE_SYSTEM_GROUP, null);
}
}
this.logger.debug("Checking group capabilities: {}", group.getName());
Group groupWithCaps = this.groupService.getGroup(group.getId(), false, true, false, false);
Set<String> adminCaps = new HashSet<>(this.adminCapabilities);
for (GroupCapability cap : groupWithCaps.getCapabilities())
adminCaps.remove(cap.getName());
if (!adminCaps.isEmpty()) {
this.logger.info("Granting group '{}' capabilities: {}", group.getName(), adminCaps);
this.groupService.addCapabilitiesToGroup(group.getId(), new ArrayList<>(adminCaps));
}
}
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;
}
}

View File

@@ -0,0 +1,95 @@
package com.inteligr8.activiti;
import java.util.Arrays;
import java.util.List;
import javax.persistence.NonUniqueResultException;
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.stereotype.Component;
import com.activiti.domain.idm.Group;
import com.activiti.domain.idm.User;
import com.activiti.service.api.GroupService;
import com.activiti.service.api.UserService;
/**
* 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 ActivitiAppAdminMembersFixer implements DataFixer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired(required = false)
private UserService userService;
@Autowired(required = false)
private GroupService groupService;
@Autowired
private TenantFinderService tenantFinderService;
@Value("${keycloak-ext.default.admins.users:#{null}}")
private String adminUserStrs;
@Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId;
@Override
public void fix() {
this.logger.trace("fix()");
if (this.adminUserStrs != null && this.adminUserStrs.length() > 0)
this.associateAdmins();
}
private void associateAdmins() {
if (this.userService == null || this.groupService == null)
return;
List<String> adminUsers = Arrays.asList(this.adminUserStrs.split(","));
if (adminUsers.isEmpty())
return;
Long tenantId = this.tenantFinderService.findTenantId();
List<Group> groups = null;
try {
Group group1 = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group1 != null)
groups = Arrays.asList(group1);
} catch (NonUniqueResultException nure) {
// suppress
}
if (groups == null)
groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
this.logger.debug("Found {} admin group(s)", groups.size());
for (String email : adminUsers) {
User user = this.userService.findUserByEmailAndTenantId(email, tenantId);
if (user == null) {
this.logger.info("The user with email '{}' does not exist, so they cannot be added as an administrator", email);
} else {
this.logger.debug("Adding {} to admin group(s)", user.getEmail());
for (Group group : groups)
this.groupService.addUserToGroup(group, user);
}
}
}
}

View File

@@ -0,0 +1,45 @@
package com.inteligr8.activiti;
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.stereotype.Component;
import com.activiti.domain.idm.User;
import com.activiti.service.api.UserService;
/**
* @author brian@inteligr8.com
*/
@Component
public class ActivitiAppAdminPasswordFixer implements DataFixer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired(required = false)
private UserService userService;
@Autowired
private TenantFinderService tenantFinderService;
@Value("${keycloak-ext.reset.admin.username:admin@app.activiti.com}")
private String adminUsername;
@Value("${keycloak-ext.reset.admin.password:#{null}}")
private String adminPassword;
@Override
public void fix() {
this.logger.trace("fix()");
if (this.adminPassword != null) {
this.logger.info("Resetting the password for admin user '{}'", this.adminUsername);
Long tenantId = this.tenantFinderService.findTenantId();
User adminUser = this.userService.findUserByEmailAndTenantId(this.adminUsername, tenantId);
this.userService.changePassword(adminUser.getId(), this.adminPassword);
}
}
}

View File

@@ -0,0 +1,7 @@
package com.inteligr8.activiti;
public interface DataFixer {
void fix();
}

View File

@@ -1,31 +1,16 @@
package com.inteligr8.activiti;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.activiti.api.security.AlfrescoSecurityConfigOverride;
import com.activiti.domain.idm.Group;
import com.activiti.domain.idm.GroupCapability;
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 overrides the APS security configuration with a collection
@@ -43,41 +28,11 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final List<String> adminCapabilities = Arrays.asList(
"access-all-models-in-tenant",
"access-editor",
"access-reports",
"publish-app-to-dashboard",
"tenant-admin",
"tenant-admin-api",
"upload-license");
@Autowired
private List<ActivitiSecurityConfigAdapter> adapters;
@Autowired(required = false)
private LicenseService licenseService;
@Autowired(required = false)
private TenantService tenantService;
@Autowired(required = false)
private UserService userService;
@Autowired(required = false)
private GroupService groupService;
@Value("${keycloak-ext.default.admins.users:#{null}}")
private String adminUserStrs;
@Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId;
@Value("${keycloak-ext.group.admins.validate:false}")
private boolean validateAdministratorsGroup;
private List<DataFixer> fixers;
@Override
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
@@ -85,12 +40,10 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
Collections.sort(this.adapters);
if (this.logger.isTraceEnabled())
this.logGroups();
if (this.validateAdministratorsGroup)
this.validateAdmins();
if (this.adminUserStrs != null && this.adminUserStrs.length() > 0)
this.associateAdmins();
if (this.fixers != null) {
for (DataFixer fixer : this.fixers)
fixer.fix();
}
for (ActivitiSecurityConfigAdapter adapter : this.adapters) {
if (adapter.isEnabled()) {
@@ -103,90 +56,4 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
}
}
private void logGroups() {
if (this.groupService == null)
return;
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 void validateAdmins() {
if (this.groupService == null)
return;
Long tenantId = this.findDefaultTenantId();
Group group = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group == null) {
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
if (!groups.isEmpty())
group = groups.iterator().next();
}
if (group == null) {
this.logger.info("Creating group: {} ({})", this.adminGroupName, this.adminGroupExternalId);
if (this.adminGroupExternalId != null) {
group = this.groupService.createGroupFromExternalStore(
this.adminGroupExternalId, tenantId, Group.TYPE_SYSTEM_GROUP, null, this.adminGroupName, new Date());
} else {
group = this.groupService.createGroup(this.adminGroupName, tenantId, Group.TYPE_SYSTEM_GROUP, null);
}
}
this.logger.debug("Checking group capabilities: {}", group.getName());
Group groupWithCaps = this.groupService.getGroup(group.getId(), false, true, false, false);
Set<String> adminCaps = new HashSet<>(this.adminCapabilities);
for (GroupCapability cap : groupWithCaps.getCapabilities())
adminCaps.remove(cap.getName());
if (!adminCaps.isEmpty()) {
this.logger.info("Granting group '{}' capabilities: {}", group.getName(), adminCaps);
this.groupService.addCapabilitiesToGroup(group.getId(), new ArrayList<>(adminCaps));
}
}
private void associateAdmins() {
if (this.userService == null || this.groupService == null)
return;
List<String> adminUsers = Arrays.asList(this.adminUserStrs.split(","));
if (adminUsers.isEmpty())
return;
Long tenantId = this.findDefaultTenantId();
List<Group> groups = this.groupService.getSystemGroupWithName("Administrators", tenantId);
for (String email : adminUsers) {
User user = this.userService.findUserByEmail(email);
this.logger.debug("Adding {} to {}", user.getEmail(), "Administrators");
for (Group group : groups)
this.groupService.addUserToGroup(group, user);
}
}
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;
}
}

View File

@@ -0,0 +1,82 @@
package com.inteligr8.activiti;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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.stereotype.Component;
import com.activiti.domain.idm.Tenant;
import com.activiti.service.idm.TenantService;
import com.activiti.service.license.LicenseService;
@Component
public class TenantFinderService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired(required = false)
private LicenseService licenseService;
@Autowired(required = false)
private TenantService tenantService;
@Value("${keycloak-ext.tenant:#{null}}")
private String tenant;
public Long findTenantId() {
Tenant tenant = this.findTenant();
return tenant == null ? null : tenant.getId();
}
public Tenant findTenant() {
this.logger.debug("Checking for a single tenant ...");
String tenantName = null;
if (this.tenant != null) {
tenantName = this.tenant;
} else {
List<Object[]> tenants = this.tenantService.getAllTenants();
if (tenants == null || tenants.isEmpty()) {
this.logger.warn("No tenants found!");
return null;
} else if (tenants.size() == 1) {
Object[] tenant = tenants.iterator().next();
this.logger.debug("Only one tenant available; selecting it: {}", tenant[0]);
return this.tenantService.getTenant((Long)tenant[0]);
} else {
tenantName = this.licenseService.getDefaultTenantName();
}
}
this.logger.debug("Trying to find by tenant name: {}", tenantName);
List<Tenant> tenants = this.tenantService.findTenantsByName(tenantName);
if (tenants == null || tenants.isEmpty()) {
this.logger.warn("Named tenant not found");
return null;
}
this.logger.debug("Found {} tenants with name {}; selecting the first one", tenants.size(), tenantName);
return tenants.iterator().next();
}
public Collection<Tenant> getTenants() {
List<Object[]> tenantObjs = this.tenantService.getAllTenants();
List<Tenant> tenants = new ArrayList<>(tenantObjs.size());
for (Object[] tenantObj : tenantObjs) {
if (tenantObj != null && tenantObj[0] != null) {
Tenant tenant = this.tenantService.getTenant((Long)tenantObj[0]);
tenants.add(tenant);
}
}
return tenants;
}
}

View File

@@ -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, ',');

View File

@@ -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;
@@ -21,12 +24,10 @@ 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;
import com.inteligr8.activiti.TenantFinderService;
/**
* This class/bean implements an Open ID Connect authenticator for Alfresco
@@ -49,30 +50,33 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
private final Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*");
@Autowired
private LicenseService licenseService;
@Autowired
private TenantService tenantService;
@Autowired
private UserService userService;
@Autowired
private GroupService groupService;
@Autowired
private TenantFinderService tenantFinderService;
@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<>();
@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]));
}
protected boolean syncGroupAsCapability() {
return this.syncGroupAs != null && this.syncGroupAs.toLowerCase().startsWith("cap");
}
/**
@@ -81,7 +85,7 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
*/
@Override
public void preAuthenticate(Authentication auth) throws AuthenticationException {
Long tenantId = this.findDefaultTenantId();
Long tenantId = this.tenantFinderService.findTenantId();
this.logger.trace("Tenant ID: {}", tenantId);
User user = this.findUser(auth, tenantId);
@@ -122,27 +126,13 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
*/
@Override
public void postAuthenticate(Authentication auth) throws AuthenticationException {
Long tenantId = this.findDefaultTenantId();
Long tenantId = this.tenantFinderService.findTenantId();
User user = this.findUser(auth, tenantId);
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
this.syncUserRoles(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();
@@ -182,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()) {
@@ -193,8 +181,18 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId());
if (group.getExternalId() != null && this.removeMapEntriesByValue(roles, this.apsGroupExternalIdToKeycloakRole(group.getExternalId()))) {
if (group.getTenantId() == null) {
// fix stray groups
group.setTenantId(tenantId);
group.setLastUpdate(new Date());
this.groupService.save(group);
}
// role already existed and the user is already a member
} else if (group.getExternalId() == null && roles.remove(this.apsGroupNameToKeycloakRole(group.getName())) != null) {
// register the group as external
group.setExternalId(this.keycloakRoleToApsGroupExternalId(this.apsGroupNameToKeycloakRole(group.getName())));
group.setLastUpdate(new Date());
this.groupService.save(group);
// internal role already existed and the user is already a member
} else {
// at this point, we have a group that the user does not have a corresponding role for
@@ -237,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;
@@ -273,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;
}
}

View File

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