Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
0d402f6014 | |||
f000f1b134 | |||
e7b6bd644e | |||
25093cd822 | |||
70ff0b5c5c | |||
343e1b65b9 | |||
d10ff1103d | |||
f5eefdb544 | |||
28a6f4d101 | |||
07a5ed959a | |||
14487b62eb | |||
e87a6b68a7 | |||
19f21fdd5c | |||
5ecb627dbf | |||
df37818f09 | |||
f3b70c1574 | |||
ea487fee31 | |||
9f9ededab2 | |||
116e22bbd6 | |||
f76105b979 | |||
9ad7a9e560 | |||
a3cb17e402 | |||
c6d0977b2f | |||
a55d1c32d0 | |||
2405a8a313 | |||
e22fbc30a5 | |||
a0cc13dc02 |
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Keycloak Extension for Activiti
|
||||||
|
|
||||||
|
This library was created to expand the functionality of keycloak integration within the APS (Activiti App) application. It includes a similar implementation for core Activiti (Activiti Engine), but the core functional is not delivered with that OOTB application at this time.
|
||||||
|
|
||||||
|
The Activiti App delivers SSO capability and that is about it. The user must already exist and group synchronization may only happen outside of the context of authentication. Namely over another protocol (LDAP).
|
||||||
|
|
||||||
|
This module expands SSO to include user creation and group synchronization. Group synchronization uses the standard access token for Open ID Connect. These groups are termed "roles".
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The installation is simple. Just include the JAR in the classpath of your Activiti App application. This is best done by not chaning the `activiti-app.war` file, but instead including it within the classpath using your web container configuration. For Apache Tomcat, you would add or modify the following context file: `conf/Catalina/localhost/activiti-app.xml`. Its related contents would be:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Context>
|
||||||
|
<Resources>
|
||||||
|
<PostResources base="${catalina.base}/ext" className="org.apache.catalina.webresources.DirResourceSet" webAppMount="/WEB-INF/lib" readOnly="true" />
|
||||||
|
</Resources>
|
||||||
|
</Context>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Common
|
||||||
|
|
||||||
|
| Property | Default | Description |
|
||||||
|
| ---------------------------------------------- | --------- | ----------- |
|
||||||
|
| `keycloak-ext.ais.enabled` | `false` | Enable AIS integration, overriding and extending the OOTB AIS provider. |
|
||||||
|
| `keycloak-ext.ootbSecurityConfig.enabled` | `true` | Enable OOTB functionality as if this module were not installed. This adapter operates at priority `0`. This means it only works if other adapters are disabled (default). |
|
||||||
|
| `keycloak-ext.default.admins.users` | | A default set of administrators to add to the administration role on application startup. |
|
||||||
|
| `keycloak-ext.clearNewUserDefaultGroups` | `true` | When creating a new user, clear any default groups added to that user. This will not impact existing users. |
|
||||||
|
| `keycloak-ext.resource.include.regex.patterns` | | OIDC provides roles in the realm and all permitted clients/resources. By default all resources are included. You can limit it with regular expressions with this property. |
|
||||||
|
| `keycloak-ext.group.format.regex.patterns` | | Reformat roles that match the specified regular expressions. The replacements are specified in another property. Multiple expressions may be specified by using commas. Whitespace is not stripped. |
|
||||||
|
| `keycloak-ext.group.format.regex.replacements` | | Reformat roles with the specified replacement expressions. The regular expressions are specified in another property. Multiple expressions may be specified by using commas. Whitespace is not stripped. |
|
||||||
|
| `keycloak-ext.group.include.regex.patterns` | | If specified, only the roles that match the specified regular expressions will be considered; otherwise all roles are included. |
|
||||||
|
| `keycloak-ext.group.exclude.regex.patterns` | | If specified, the roles that match the specified regular expressions will be ignored. This overrides any role explicitly included. |
|
||||||
|
| `keycloak-ext.syncInternalGroup` | `false` | If an internal group with the same name already exists, use that group instead of creating a new one with the same name. Also register that internal group as external. |
|
||||||
|
|
||||||
|
### For Activiti App Only
|
||||||
|
|
||||||
|
| 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
|
||||||
|
|
||||||
|
| Property | Default | Description |
|
||||||
|
| ----------------------------------------- | --------------- | ----------- |
|
||||||
|
| `keycloak-ext.ais.priority` | `-10` | The order of configurable adapters to use with the application. Only the lowest priority enabled adapter will be used. Values of `1`+ will only load if the OOTB adapter is disabled. |
|
||||||
|
| `keycloak-ext.group.admins.validate` | `false` | Whether or not to validate the existence and capabilities of an administrators group on appliation startup. This is only applicable for when one is accidently removed and no one has the rights to create one. |
|
||||||
|
| `keycloak-ext.group.admins.name` | `admins` | The name of an administrators group to potentially add and default users on application startup. |
|
||||||
|
| `keycloak-ext.group.admins.externalId` | `admins` | The name of an administrators group to potentially add and default users on application startup. |
|
||||||
|
| `keycloak-ext.createMissingUser` | `true` | Before authentication, check to make sure the user exists as an APS user; if they don't, create the user. |
|
||||||
|
| `keycloak-ext.createMissingGroup` | `true` | Before authorization, check to make sure groups exist for the roles the user claims; if they don't, create the groups. |
|
||||||
|
| `keycloak-ext.syncGroupAdd` | `true` | If the user belongs to a role but not its corresponding group, add the user to the group. |
|
||||||
|
| `keycloak-ext.syncGroupRemove` | `true` | If the user belongs to a group but does not have the corresponding role, remove the user from the group. |
|
||||||
|
|
||||||
|
### Untested
|
||||||
|
|
||||||
|
| Property | Default | Description |
|
||||||
|
| ----------------------------------------- | --------------- | ----------- |
|
||||||
|
| `keycloak-ext.keycloak.enabled` | `false` | Enable Keycloak integration, overriding and extending the OOTB Keycloak provider (*untested*). |
|
||||||
|
| `keycloak-ext.keycloak.priority` | `-5` | The order of configurable adapters to use with the application. Only the lowest priority enabled adapter will be used. Values of `1`+ will only load if the OOTB adapter is disabled. |
|
46
pom.xml
46
pom.xml
@@ -4,7 +4,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.inteligr8.activiti</groupId>
|
<groupId>com.inteligr8.activiti</groupId>
|
||||||
<artifactId>keycloak-activiti-app-ext</artifactId>
|
<artifactId>keycloak-activiti-app-ext</artifactId>
|
||||||
<version>1.1.1</version>
|
<version>1.3.0</version>
|
||||||
<name>Keycloak Authentication & Authorization for APS</name>
|
<name>Keycloak Authentication & Authorization for APS</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<maven.compiler.release>11</maven.compiler.release>
|
<maven.compiler.release>11</maven.compiler.release>
|
||||||
|
|
||||||
<aps.version>1.11.1.1</aps.version>
|
<aps.version>2.0.1</aps.version>
|
||||||
<keycloak.version>6.0.1</keycloak.version>
|
<keycloak.version>10.0.2</keycloak.version>
|
||||||
<spring-security-oauth2.version>2.0.17.RELEASE</spring-security-oauth2.version>
|
<spring-security-oauth2.version>2.5.2.RELEASE</spring-security-oauth2.version>
|
||||||
<slf4j.version>1.7.26</slf4j.version>
|
<slf4j.version>1.7.26</slf4j.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
<version>${keycloak.version}</version>
|
<version>${keycloak.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Needed for Activiti App Identity Service inheritance/override -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.activiti</groupId>
|
<groupId>com.activiti</groupId>
|
||||||
<artifactId>activiti-app</artifactId>
|
<artifactId>activiti-app</artifactId>
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
<classifier>classes</classifier>
|
<classifier>classes</classifier>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Needed for the Activiti App Public API -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.activiti</groupId>
|
<groupId>com.activiti</groupId>
|
||||||
<artifactId>activiti-app-logic</artifactId>
|
<artifactId>activiti-app-logic</artifactId>
|
||||||
@@ -52,27 +54,10 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>alfresco-public</id>
|
<id>alfresco-private</id>
|
||||||
<url>https://artifacts.alfresco.com/nexus/content/repositories/public</url>
|
<url>https://artifacts.alfresco.com/nexus/content/groups/private</url>
|
||||||
</repository>
|
</repository>
|
||||||
<repository>
|
<repository>
|
||||||
<id>activiti-releases</id>
|
<id>activiti-releases</id>
|
||||||
@@ -80,10 +65,15 @@
|
|||||||
</repository>
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<pluginRepositories>
|
<distributionManagement>
|
||||||
<pluginRepository>
|
<repository>
|
||||||
<id>inteligr8-releases</id>
|
<id>inteligr8-releases</id>
|
||||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||||
</pluginRepository>
|
</repository>
|
||||||
</pluginRepositories>
|
<snapshotRepository>
|
||||||
</project>
|
<id>inteligr8-snapshots</id>
|
||||||
|
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-snapshots</url>
|
||||||
|
</snapshotRepository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
7
src/main/java/com/inteligr8/activiti/DataFixer.java
Normal file
7
src/main/java/com/inteligr8/activiti/DataFixer.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package com.inteligr8.activiti;
|
||||||
|
|
||||||
|
public interface DataFixer {
|
||||||
|
|
||||||
|
void fix();
|
||||||
|
|
||||||
|
}
|
@@ -1,28 +1,16 @@
|
|||||||
package com.inteligr8.activiti;
|
package com.inteligr8.activiti;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.activiti.api.security.AlfrescoSecurityConfigOverride;
|
import com.activiti.api.security.AlfrescoSecurityConfigOverride;
|
||||||
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 overrides the APS security configuration with a collection
|
* This class/bean overrides the APS security configuration with a collection
|
||||||
@@ -44,28 +32,7 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
|
|||||||
private List<ActivitiSecurityConfigAdapter> adapters;
|
private List<ActivitiSecurityConfigAdapter> adapters;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private LicenseService licenseService;
|
private List<DataFixer> fixers;
|
||||||
|
|
||||||
@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:aps-admin}")
|
|
||||||
private String adminGroupExternalId;
|
|
||||||
|
|
||||||
@Value("${keycloak-ext.group.admins.validate:false}")
|
|
||||||
private boolean validateAdministratorsGroup;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
|
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
|
||||||
@@ -73,12 +40,10 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
|
|||||||
|
|
||||||
Collections.sort(this.adapters);
|
Collections.sort(this.adapters);
|
||||||
|
|
||||||
if (this.logger.isTraceEnabled())
|
if (this.fixers != null) {
|
||||||
this.logGroups();
|
for (DataFixer fixer : this.fixers)
|
||||||
if (this.validateAdministratorsGroup)
|
fixer.fix();
|
||||||
this.validateAdmins();
|
}
|
||||||
if (this.adminUserStrs != null && this.adminUserStrs.length() > 0)
|
|
||||||
this.associateAdmins();
|
|
||||||
|
|
||||||
for (ActivitiSecurityConfigAdapter adapter : this.adapters) {
|
for (ActivitiSecurityConfigAdapter adapter : this.adapters) {
|
||||||
if (adapter.isEnabled()) {
|
if (adapter.isEnabled()) {
|
||||||
@@ -90,74 +55,5 @@ 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.getGroupByExternalId(this.adminGroupExternalId);
|
|
||||||
if (group == null) {
|
|
||||||
this.logger.info("Creating '{}' group ...", this.adminGroupName);
|
|
||||||
group = this.groupService.createGroupFromExternalStore(
|
|
||||||
this.adminGroupExternalId, tenantId, Group.TYPE_SYSTEM_GROUP, null, this.adminGroupName, new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info("Granting '{}' group all capabilities ...", group.getName());
|
|
||||||
this.groupService.addCapabilitiesToGroup(group.getId(), Arrays.asList("access-all-models-in-tenant", "access-editor", "access-reports", "publish-app-to-dashboard", "tenant-admin", "tenant-admin-api", "upload-license"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,6 +3,7 @@ package com.inteligr8.activiti.keycloak;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -11,6 +12,8 @@ import java.util.Set;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
@@ -34,8 +37,8 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
|
|||||||
@Value("${keycloak-ext.createMissingUser:true}")
|
@Value("${keycloak-ext.createMissingUser:true}")
|
||||||
protected boolean createMissingUser;
|
protected boolean createMissingUser;
|
||||||
|
|
||||||
@Value("${keycloak-ext.clearNewUserGroups:true}")
|
@Value("${keycloak-ext.clearNewUserDefaultGroups:true}")
|
||||||
protected boolean clearNewUserGroups;
|
protected boolean clearNewUserDefaultGroups;
|
||||||
|
|
||||||
@Value("${keycloak-ext.createMissingGroup:true}")
|
@Value("${keycloak-ext.createMissingGroup:true}")
|
||||||
protected boolean createMissingGroup;
|
protected boolean createMissingGroup;
|
||||||
@@ -45,6 +48,9 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
|
|||||||
|
|
||||||
@Value("${keycloak-ext.syncGroupRemove:true}")
|
@Value("${keycloak-ext.syncGroupRemove:true}")
|
||||||
protected boolean syncGroupRemove;
|
protected boolean syncGroupRemove;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.syncInternalGroups:false}")
|
||||||
|
protected boolean syncInternalGroups;
|
||||||
|
|
||||||
@Value("${keycloak-ext.resource.include.regex.patterns:#{null}}")
|
@Value("${keycloak-ext.resource.include.regex.patterns:#{null}}")
|
||||||
protected String resourceRegexIncludes;
|
protected String resourceRegexIncludes;
|
||||||
@@ -67,6 +73,7 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
|
|||||||
protected final Set<Pattern> groupExcludes = new HashSet<>();
|
protected final Set<Pattern> groupExcludes = new HashSet<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@OverridingMethodsMustInvokeSuper
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
if (this.regexPatterns != null) {
|
if (this.regexPatterns != null) {
|
||||||
String[] regexPatternStrs = StringUtils.split(this.regexPatterns, ',');
|
String[] regexPatternStrs = StringUtils.split(this.regexPatterns, ',');
|
||||||
@@ -99,7 +106,7 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected Map<String, String> getRoles(Authentication auth) {
|
protected Map<String, String> getKeycloakRoles(Authentication auth) {
|
||||||
Map<String, String> authorities = new HashMap<>();
|
Map<String, String> authorities = new HashMap<>();
|
||||||
|
|
||||||
AccessToken atoken = this.getKeycloakAccessToken(auth);
|
AccessToken atoken = this.getKeycloakAccessToken(auth);
|
||||||
@@ -234,6 +241,24 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected <K, V> boolean removeMapEntriesByValue(Map<K, V> map, V value) {
|
||||||
|
if (value == null)
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
|
||||||
|
Iterator<Entry<K, V>> i = map.entrySet().iterator();
|
||||||
|
while (i.hasNext()) {
|
||||||
|
Entry<K, V> entry = i.next();
|
||||||
|
if (entry.getValue() != null && value.equals(entry.getValue())) {
|
||||||
|
i.remove();
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found > 0;
|
||||||
|
}
|
||||||
|
|
||||||
protected Set<String> toSet(Collection<? extends GrantedAuthority> grantedAuthorities) {
|
protected Set<String> toSet(Collection<? extends GrantedAuthority> grantedAuthorities) {
|
||||||
Set<String> authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16));
|
Set<String> authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16));
|
||||||
for (GrantedAuthority grantedAuthority : grantedAuthorities) {
|
for (GrantedAuthority grantedAuthority : grantedAuthorities) {
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
package com.inteligr8.activiti.keycloak;
|
package com.inteligr8.activiti.keycloak;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedList;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
||||||
import javax.persistence.NonUniqueResultException;
|
import javax.persistence.NonUniqueResultException;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -15,18 +17,17 @@ import org.keycloak.representations.AccessToken;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.activiti.domain.idm.Group;
|
import com.activiti.domain.idm.Group;
|
||||||
import com.activiti.domain.idm.Tenant;
|
|
||||||
import com.activiti.domain.idm.User;
|
import com.activiti.domain.idm.User;
|
||||||
import com.activiti.service.api.GroupService;
|
import com.activiti.service.api.GroupService;
|
||||||
import com.activiti.service.api.UserService;
|
import com.activiti.service.api.UserService;
|
||||||
import com.activiti.service.idm.TenantService;
|
import com.inteligr8.activiti.TenantFinderService;
|
||||||
import com.activiti.service.license.LicenseService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class/bean implements an Open ID Connect authenticator for Alfresco
|
* This class/bean implements an Open ID Connect authenticator for Alfresco
|
||||||
@@ -48,13 +49,6 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
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 Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*");
|
||||||
private final String externalIdmSource = "ais";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private LicenseService licenseService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private TenantService tenantService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
@@ -62,13 +56,36 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
@Autowired
|
@Autowired
|
||||||
private GroupService groupService;
|
private GroupService groupService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TenantFinderService tenantFinderService;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.external.id:ais}")
|
||||||
|
protected String externalIdmSource;
|
||||||
|
|
||||||
|
@Value("${keycloak-ext.group.capability.regex.patterns:#{null}}")
|
||||||
|
protected String regexCapIncludes;
|
||||||
|
|
||||||
|
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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method validates that the user exists, if not, it creates the
|
* This method validates that the user exists, if not, it creates the
|
||||||
* missing user. Without this functionality, SSO straight up fails in APS.
|
* missing user. Without this functionality, SSO straight up fails in APS.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void preAuthenticate(Authentication auth) throws AuthenticationException {
|
public void preAuthenticate(Authentication auth) throws AuthenticationException {
|
||||||
Long tenantId = this.findDefaultTenantId();
|
Long tenantId = this.tenantFinderService.findTenantId();
|
||||||
this.logger.trace("Tenant ID: {}", tenantId);
|
this.logger.trace("Tenant ID: {}", tenantId);
|
||||||
|
|
||||||
User user = this.findUser(auth, tenantId);
|
User user = this.findUser(auth, tenantId);
|
||||||
@@ -79,16 +96,26 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
user = this.createUser(auth, tenantId);
|
user = this.createUser(auth, tenantId);
|
||||||
this.logger.debug("Created user: {} => {}", user.getId(), user.getExternalId());
|
this.logger.debug("Created user: {} => {}", user.getId(), user.getExternalId());
|
||||||
|
|
||||||
if (this.clearNewUserGroups) {
|
if (this.clearNewUserDefaultGroups) {
|
||||||
this.logger.debug("Clearing groups: {}", user.getId());
|
this.logger.debug("Clearing groups: {}", user.getId());
|
||||||
// fetch and remove default groups
|
// fetch and remove default groups
|
||||||
user = this.userService.findUserByEmailFetchGroups(user.getEmail());
|
user = this.userService.getUser(user.getId(), true);
|
||||||
for (Group group : user.getGroups())
|
for (Group group : user.getGroups())
|
||||||
this.groupService.deleteUserFromGroup(group, user);
|
this.groupService.deleteUserFromGroup(group, user);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
|
||||||
}
|
}
|
||||||
|
} else if (user.getExternalOriginalSrc() == null || user.getExternalOriginalSrc().length() == 0) {
|
||||||
|
this.logger.debug("User exists, but not created by an external source: {}", auth.getName());
|
||||||
|
this.logger.info("Linking user '{}' with external IDM '{}'", auth.getName(), this.externalIdmSource);
|
||||||
|
user.setExternalId(auth.getName());
|
||||||
|
user.setExternalOriginalSrc(this.externalIdmSource);
|
||||||
|
this.userService.save(user);
|
||||||
|
} else if (!this.externalIdmSource.equals(user.getExternalOriginalSrc())) {
|
||||||
|
this.logger.debug("User '{}' exists, but created by another source: {}", auth.getName(), user.getExternalOriginalSrc());
|
||||||
|
} else {
|
||||||
|
this.logger.trace("User already exists: {}", auth.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,27 +126,13 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void postAuthenticate(Authentication auth) throws AuthenticationException {
|
public void postAuthenticate(Authentication auth) throws AuthenticationException {
|
||||||
Long tenantId = this.findDefaultTenantId();
|
Long tenantId = this.tenantFinderService.findTenantId();
|
||||||
User user = this.findUser(auth, tenantId);
|
User user = this.findUser(auth, tenantId);
|
||||||
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
|
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
|
||||||
|
|
||||||
this.syncUserRoles(user, auth, tenantId);
|
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) {
|
private User findUser(Authentication auth, Long tenantId) {
|
||||||
String email = auth.getName();
|
String email = auth.getName();
|
||||||
|
|
||||||
@@ -141,7 +154,7 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName());
|
Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName());
|
||||||
if (!emailNamesMatcher.matches()) {
|
if (!emailNamesMatcher.matches()) {
|
||||||
this.logger.warn("The email address could not be parsed for names: {}", auth.getName());
|
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());
|
return this.userService.createNewUserFromExternalStore(auth.getName(), "Unknown", "Person", tenantId, auth.getName(), this.externalIdmSource, new Date());
|
||||||
} else {
|
} else {
|
||||||
String firstName = StringUtils.capitalize(emailNamesMatcher.group(1));
|
String firstName = StringUtils.capitalize(emailNamesMatcher.group(1));
|
||||||
String lastName = StringUtils.capitalize(emailNamesMatcher.group(2));
|
String lastName = StringUtils.capitalize(emailNamesMatcher.group(2));
|
||||||
@@ -153,22 +166,36 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void syncUserRoles(User user, Authentication auth, Long tenantId) {
|
private void syncUserRoles(User user, Authentication auth, Long tenantId) {
|
||||||
Map<String, String> roles = this.getRoles(auth);
|
Map<String, String> roles = this.getKeycloakRoles(auth);
|
||||||
if (roles == null) {
|
if (roles == null) {
|
||||||
this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
|
this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check Activiti groups
|
// check Activiti groups
|
||||||
User userWithGroups = this.userService.findUserByEmailFetchGroups(user.getEmail());
|
User userWithGroups = this.userService.getUser(user.getId(), true);
|
||||||
for (Group group : userWithGroups.getGroups()) {
|
for (Group group : userWithGroups.getGroups()) {
|
||||||
|
if (group.getExternalId() == null && !this.syncInternalGroups)
|
||||||
|
continue;
|
||||||
|
|
||||||
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId());
|
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId());
|
||||||
|
|
||||||
if (group.getExternalId() == null) {
|
if (group.getExternalId() != null && this.removeMapEntriesByValue(roles, this.apsGroupExternalIdToKeycloakRole(group.getExternalId()))) {
|
||||||
// skip APS system groups
|
if (group.getTenantId() == null) {
|
||||||
} else if (roles.remove(group.getExternalId()) != null) {
|
// fix stray groups
|
||||||
// all good
|
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 {
|
} else {
|
||||||
|
// at this point, we have a group that the user does not have a corresponding role for
|
||||||
if (this.syncGroupRemove) {
|
if (this.syncGroupRemove) {
|
||||||
this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getName());
|
this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getName());
|
||||||
this.groupService.deleteUserFromGroup(group, userWithGroups);
|
this.groupService.deleteUserFromGroup(group, userWithGroups);
|
||||||
@@ -184,20 +211,37 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
|
|
||||||
Group group;
|
Group group;
|
||||||
try {
|
try {
|
||||||
group = this.groupService.getGroupByExternalId(role.getKey());
|
group = this.groupService.getGroupByExternalIdAndTenantId(this.keycloakRoleToApsGroupExternalId(role.getKey()), tenantId);
|
||||||
} catch (NonUniqueResultException nure) {
|
} catch (NonUniqueResultException nure) {
|
||||||
if (this.logger.isDebugEnabled()) {
|
this.logger.warn("There are multiple groups with the external ID; not adding user to group: {}", role.getKey());
|
||||||
// FIXME only added to address a former bug
|
continue;
|
||||||
group = this.fixMultipleGroups(role.getKey(), tenantId);
|
}
|
||||||
} else {
|
|
||||||
throw nure;
|
if (group == null && this.syncInternalGroups) {
|
||||||
|
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.keycloakRoleToApsGroupName(role.getValue()), tenantId);
|
||||||
|
if (groups.size() > 1) {
|
||||||
|
this.logger.warn("There are multiple groups with the same name; not adding user to group: {}", role.getValue());
|
||||||
|
continue;
|
||||||
|
} else if (groups.size() == 1) {
|
||||||
|
group = groups.iterator().next();
|
||||||
|
this.logger.debug("Found an internal group; registering as external: {}", group.getName());
|
||||||
|
group.setExternalId(this.keycloakRoleToApsGroupExternalId(role.getKey()));
|
||||||
|
group.setLastSyncTimeStamp(new Date());
|
||||||
|
group.setLastUpdate(new Date());
|
||||||
|
this.groupService.save(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
if (this.createMissingGroup) {
|
if (this.createMissingGroup) {
|
||||||
this.logger.trace("Creating new group: {}", role);
|
this.logger.trace("Creating new group for role: {}", role);
|
||||||
group = this.groupService.createGroupFromExternalStore(role.getValue(), tenantId, Group.TYPE_SYSTEM_GROUP, null, role.getKey(), new Date());
|
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;
|
||||||
|
this.logger.trace("Creating new group: {} ({}) [type: {}]", name, externalId, type);
|
||||||
|
group = this.groupService.createGroupFromExternalStore(name, tenantId, type, null, externalId, new Date());
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug("Group does not exist; group creation is disabled: {}", role);
|
this.logger.debug("Group does not exist; group creation is disabled: {}", role);
|
||||||
}
|
}
|
||||||
@@ -212,28 +256,34 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Group fixMultipleGroups(String externalId, Long tenantId) {
|
private String keycloakRoleToApsGroupExternalId(String role) {
|
||||||
List<Group> groupsToDelete = new LinkedList<>();
|
return this.externalIdmSource + "_" + role;
|
||||||
Date earliestDate = new Date();
|
}
|
||||||
Group earliestGroup = null;
|
|
||||||
|
private String apsGroupExternalIdToKeycloakRole(String externalId) {
|
||||||
|
int underscorePos = externalId.indexOf('_');
|
||||||
|
return underscorePos < 0 ? externalId : externalId.substring(underscorePos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String keycloakRoleToApsGroupName(String role) {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String apsGroupNameToKeycloakRole(String externalId) {
|
||||||
|
return externalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRoleToBeOrganization(String role) {
|
||||||
|
if (this.capIncludes.isEmpty())
|
||||||
|
return true;
|
||||||
|
|
||||||
for (Group group : this.groupService.getSystemGroups(tenantId)) {
|
for (Pattern regex : this.capIncludes) {
|
||||||
if (externalId.equals(group.getExternalId())) {
|
Matcher matcher = regex.matcher(role);
|
||||||
if (group.getLastUpdate().before(earliestDate)) {
|
if (matcher.matches())
|
||||||
if (earliestGroup != null)
|
return false;
|
||||||
groupsToDelete.add(earliestGroup);
|
}
|
||||||
earliestDate = group.getLastUpdate();
|
|
||||||
earliestGroup = group;
|
return true;
|
||||||
} else {
|
|
||||||
groupsToDelete.add(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Group group : groupsToDelete)
|
|
||||||
this.groupService.deleteGroup(group.getId());
|
|
||||||
|
|
||||||
return earliestGroup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,154 +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.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.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.getRoles(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))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType());
|
|
||||||
if (roles.remove(group.getId().substring(this.groupPrefix.length())) != 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.groupPrefix + role.getKey())
|
|
||||||
.singleResult();
|
|
||||||
if (group == null) {
|
|
||||||
if (this.createMissingGroup) {
|
|
||||||
this.logger.trace("Group does not exist; creating one");
|
|
||||||
group = this.identityService.newGroup(this.groupPrefix + role.getKey());
|
|
||||||
group.setName(role.getValue());
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Reference in New Issue
Block a user