Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
880da07a84 | |||
faba551a2d | |||
84d1b4ea2f | |||
187e558177 | |||
c38c1d28df | |||
d631cc5f12 | |||
0a10b06cc8 | |||
76ce7e42d4 | |||
790836194e | |||
dde6dbcdb0 |
89
README.md
89
README.md
@@ -1,6 +1,6 @@
|
||||
# Auth Extension for APS (Activiti App)
|
||||
|
||||
This library was originally created to expand the functionality of Keycloak integration within the Alfresco Process Services (APS) application. It has expanded to support general OAuth, closing gaps that remain in the implementation provided by Alfresco. This is useless for the open source Activiti.
|
||||
This library was originally created to expand the functionality of Keycloak integration within the Alfresco Process Services (APS) application. It has expanded to support general OAuth, closing gaps that remain in the implementation provided by Alfresco. This is useless for the open source Activiti product.
|
||||
|
||||
APS delivers SSO capability and that is about it. It has a few shortcomings:
|
||||
|
||||
@@ -9,6 +9,8 @@ APS delivers SSO capability and that is about it. It has a few shortcomings:
|
||||
|
||||
This extension aims to resolve those issues.
|
||||
|
||||
The older version of this extension was specific to Keycloak and was named `keycloak-activiti-app-ext`. See the [`develop-v1.4.x` branch](https://git.inteligr8.com/inteligr8/auth-activiti-app-ext/src/branch/develop-v1.4.x/) for details specific to it.
|
||||
|
||||
## Installation
|
||||
|
||||
The installation is simple. Just include the JAR in the classpath of your APS 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:
|
||||
@@ -29,13 +31,17 @@ grant codeBase "file:${catalina.base}/ext/-" {
|
||||
}
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
This extension requires the [`multiext-activiti-app-ext`](https://git.inteligr8.com/inteligr8/multiext-activiti-app-ext). Without it, APS will fail to startup. It is very small and requires no additional configuration.
|
||||
|
||||
## Support Matrix
|
||||
|
||||
| Auth Activiti App Extension | Activiti App |
|
||||
| --------------------------- | --------------- |
|
||||
| v1.0 - v1.2 | v1.11.x |
|
||||
| v1.3 | v1.11.x - v2.x |
|
||||
| v2.0+ | v24.x+ |
|
||||
| Activiti App Extension | Activiti App |
|
||||
| --------------------------------------- | --------------- |
|
||||
| `keycloak-activiti-app-ext` v1.0 - v1.2 | v1.11.x |
|
||||
| `keycloak-activiti-app-ext` v1.3 - v1.4 | v1.11.x - v2.x |
|
||||
| `auth-activiti-app-ext` v2.0+ | v24.x+ |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -43,6 +49,13 @@ The library is highly configurable. You configure it with properties specified
|
||||
|
||||
This will only work if OAuth is being used. That would be the case if `activiti.identity-service.enabled` or `security.oauth2.authentication.enabled` is `true`.
|
||||
|
||||
The following properties are used across the functionalities of this extension.
|
||||
|
||||
| Property | Default | Description |
|
||||
| --------------------------------- | --------- | ----------- |
|
||||
| `auth-ext.externalId` | `oauth` | This will serve as the external ID for users and as the prefix for the external ID of groups created or searched by this extension. Anything without an external ID is considered internal. So mismatched external IDs are never considered for anything by this extension. |
|
||||
| `auth-ext.tenant` | | A preselected tenant for all operations in this extension. Only required if there are multiple tenants. |
|
||||
|
||||
### OAuth Authentication/Authorization
|
||||
|
||||
The following properties were added to increase the configurability of the built-in OAuth capabilities of APS. The default in this extension adds the `microprofile-jwt` scope, which is key to providing groups/roles/entitlements.
|
||||
@@ -55,48 +68,48 @@ The following properties were added to increase the configurability of the built
|
||||
|
||||
The following properties provide the core functionality of this extension. That is role synchronization.
|
||||
|
||||
| Property | Default | Description |
|
||||
| ---------------------------------------------- | --------- | ----------- |
|
||||
| `auth-ext.sync.externalId` | `oauth` | This will serve as the external ID for users and as the prefix for the external ID of groups created by this extension. |
|
||||
| `auth-ext.tenant` | | A preselected tenant for all operations in this extension. Only required if there are multiple tenants. |
|
||||
| `auth-ext.sync.user.createMissing` | `true` | If the user is authenticated, the user may be created in APS. |
|
||||
| `auth-ext.sync.user.requireGroup` | | This is only applicable when `createMissing` is `true`. If this is unset or the OAuth Authorization Server gives the user the specified group/role, then the user record will be created in APS. |
|
||||
| `auth-ext.sync.user.clearNewUserGroups` | `true` | This is only applicable when `createMissing` is `true`. All default APS groups will be deleted from the new user record. |
|
||||
| `auth-ext.sync.group.createMissing` | `true` | If a filtered and translated OIDC group has no corresponding APS group, a group will be created in APS. See `auth-ext.sync.group.capabilities.patterns` for whether that group will be an APS Organization or APS Capability. |
|
||||
| `auth-ext.sync.group.additions` | `true` | If the user isn't in an APS group but OAuth claims the OIDC group, then add them to it. |
|
||||
| `auth-ext.sync.group.removals` | `true` | If the user is in APS group but OAuth claims no OIDC group, then remove them from it. |
|
||||
| `auth-ext.sync.group.internal` | `false` | When considering groups for creation or user membership, include internal groups. Internal groups are ones without an `externalId`. |
|
||||
| `auth-ext.sync.group.internal.externalize` | `false` | This is only applicable when `internal` is `true`. If an internal group is encountered during the operation of this extension, make it external with the current `externalId`. |
|
||||
| `auth-ext.sync.group.tenantize` | `false` | If a group without a tenant is encountered during the operation of this extension, make it part of the selected tenant. |
|
||||
| `auth-ext.sync.group.translate.patterns` | | A comma delimited set of regular expression patterns for the translation (reformatting) of authorities. |
|
||||
| `auth-ext.sync.group.translate.replacements` | | A comma delimited set of regular expression replacement strings for the translation (reformatting) of authorities. |
|
||||
| `auth-ext.sync.group.include.patterns` | | A comma delimited set of regular expression patterns on what authorities to include. This is processed before `translate` processing. A blank value includes everything. If anything is specified, then only matches could possibly be included; but could still be excluded explicitly. |
|
||||
| `auth-ext.sync.group.exclude.patterns` | | A comma delimited set of regular expression patterns on what authorities to exclude. This is processed before `translate` processing. A blank value excludes nothing. If anything is specified and `include` is empty, then only matches will be excluded. If both are specified, `exclude` overrules `include` matches. |
|
||||
| `auth-ext.sync.group.capabilities.patterns` | `Superusers` | A comma delimited set of regular expression patterns on what authorities to associate with APS Capabilities instead of APS Organizations (default). |
|
||||
|
||||
| Property | Default | Description |
|
||||
| --------------------------------------------- | ------------ | ----------- |
|
||||
| `auth-ext.sync.user.createMissing` | `true` | If the user is authenticated, the user may be created in APS. |
|
||||
| `auth-ext.sync.user.requireOidcGroup` | | This is only applicable when `createMissing` is `true`. If this is unset or the OAuth Authorization Server gives the user the specified group/role, then the user record will be created in APS. |
|
||||
| `auth-ext.sync.user.clearNewUserGroups` | `true` | This is only applicable when `createMissing` is `true`. All default APS groups will be deleted from the new user record. |
|
||||
| `auth-ext.sync.group.createMissing` | `true` | If a filtered and translated OIDC group has no corresponding APS group, a group will be created in APS. See `auth-ext.sync.group.capabilities.patterns` for whether that group will be an APS Organization or APS Capability. |
|
||||
| `auth-ext.sync.group.additions` | `true` | If the user isn't in an APS group but OAuth claims the OIDC group, then add them to it. |
|
||||
| `auth-ext.sync.group.removals` | `true` | If the user is in APS group but OAuth claims no OIDC group, then remove them from it. |
|
||||
| `auth-ext.sync.group.internal` | `false` | When considering groups for creation or user membership, include internal groups. Internal groups are ones without an `externalId`. |
|
||||
| `auth-ext.sync.group.internal.externalize` | `false` | This is only applicable when `internal` is `true`. If an internal group is encountered during the operation of this extension, make it external with the current `externalId`. |
|
||||
| `auth-ext.sync.group.tenantize` | `false` | If a group without a tenant is encountered during the operation of this extension, make it part of the selected tenant. |
|
||||
| `auth-ext.sync.group.include.patterns` | | A comma delimited set of regular expression patterns on what OIDC groups to include. This is processed before `translate` processing. A blank value matches everything. If anything is specified, then only matches could possibly be included. Any matches of the `exclude` property patterns always override though. |
|
||||
| `auth-ext.sync.group.exclude.patterns` | | A comma delimited set of regular expression patterns on what OIDC groups to exclude. This is processed before `translate` processing. A blank value matches nothing (includes all that pass the `include` constraint). If anything is specified and `include` is empty, then all non-matches are included. If both are specified, `exclude` matches override `include` matches. |
|
||||
| `auth-ext.sync.group.translate.patterns` | | A comma delimited set of regular expression patterns for the translation (reformatting) of OIDC groups to APS groups. This list corresponds to the `replacements` property and must have the same number of commas. |
|
||||
| `auth-ext.sync.group.translate.replacements` | | A comma delimited set of regular expression replacement strings for the translation (reformatting) of OIDC groups to APS groups. This list corresponds to the `patterns` property and must have the same number of commas. |
|
||||
| `auth-ext.sync.group.capability.patterns` | `Superusers` | A comma delimited set of regular expression patterns on what translated OIDC groups to associate with APS Capability Groups instead of APS Organization Groups (default). This is processed after `translate` processing. |
|
||||
|
||||
### Authentication Data Fixers
|
||||
|
||||
#### Administrator Password Fixer
|
||||
|
||||
| Property | Default | Description |
|
||||
| ----------------------------------------- | ------------------------ | ----------- |
|
||||
| `auth-ext.reset.admin.username` | `admin@app.activiti.com` |
|
||||
| `auth-ext.reset.admin.password` | | If set, the user's password will be set to this value on startup; otherwise this fixer is skipped. |
|
||||
This fixer does not execute unless the `auth-ext.reset.admin.password` property is explicitly set. It is meant to be set for a single run to fix scenarios when a non-OAuth administrator cannot access the system. This is useful when your OAuth configuration isn't working and you need to reset your non-OAuth administrator's password.
|
||||
|
||||
| Property | Default |
|
||||
| --------------------------------- | ------------------------ |
|
||||
| `auth-ext.reset.admin.username` | `admin@app.activiti.com` |
|
||||
| `auth-ext.reset.admin.password` | |
|
||||
|
||||
#### Administrator Members Fixer
|
||||
|
||||
| Property | Default | Description |
|
||||
| ----------------------------------------- | ------------------------ | ----------- |
|
||||
| `auth-ext.default.admins.users` | | A comma delimited list of user emails; fixer is skipped if empty. |
|
||||
| `auth-ext.group.admins.name` | `Superusers` | The APS Group (Capability or Organization) to add the specified users to. |
|
||||
| `auth-ext.group.admins.externalId` | | If specified, this APS Group will be considered before the specified `name` field. |
|
||||
This fixer does not execute unless the `auth-ext.default.admin-users` property is explicitly set. It is meant to be set for a single run to add both non-OAuth and OAuth users to an administrator group. This is useful when OAuth is misconfigured and all administrators lose their administrator rights.
|
||||
|
||||
| Property | Default | Description |
|
||||
| ------------------------------------ | ------------ | ----------- |
|
||||
| `auth-ext.default.admins.users` | | A comma delimited list of user emails. |
|
||||
| `auth-ext.group.admins.name` | `Superusers` | The APS Group (Capability or Organization) for the specified users to be members. |
|
||||
|
||||
#### Administrator Group Fixer
|
||||
|
||||
This fixer does not execute unless the `auth-ext.group.admins.validate` property is set to `true`. It is meant to be set for a single run to correct the specified group for administration access. This is useful if you accidentally delete the administrative APS Capability Group, typically called `Administrator` or `Superusers`.
|
||||
|
||||
| Property | Default | Description |
|
||||
| ----------------------------------------- | ------------------------ | ----------- |
|
||||
| `auth-ext.group.admins.validate` | `false` | If `true`, the specified APS Group will be granted all capabilities. |
|
||||
| `auth-ext.group.admins.name` | `Superusers` | The APS Group (Capability or Organization) to add the specified users to. |
|
||||
| `auth-ext.group.admins.externalId` | | If specified, this APS Group will be considered before the specified `name` field. |
|
||||
|
||||
| `auth-ext.group.admins.validate` | `false` |
|
||||
| `auth-ext.group.admins.name` | `Superusers` | The APS Capability Group of which to grant all capabilities. |
|
||||
|
@@ -1,178 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.inteligr8.activiti</groupId>
|
||||
<artifactId>keycloak-activiti-app-ext</artifactId>
|
||||
<name>Keycloak Authentication & Authorization for APS</name>
|
||||
<version>1.4-SNAPSHOT</version>
|
||||
<description>An Alfresco Process Service App extension providing improved Keycloak/AIS support.</description>
|
||||
<url>https://bitbucket.org/inteligr8/keycloak-activiti-app-ext</url>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>brian.long</id>
|
||||
<name>Brian Long</name>
|
||||
<email>brian@inteligr8.com</email>
|
||||
<url>https://twitter.com/brianmlong</url>
|
||||
</developer>
|
||||
</developers>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007</name>
|
||||
<url>https://www.gnu.org/licenses/lgpl-3.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<scm>
|
||||
<connection>scm:git:https://bitbucket.org/inteligr8/keycloak-activiti-app-ext.git</connection>
|
||||
<developerConnection>scm:git:git@bitbucket.org:inteligr8/keycloak-activiti-app-ext.git</developerConnection>
|
||||
<url>https://bitbucket.org/inteligr8/keycloak-activiti-app-ext</url>
|
||||
</scm>
|
||||
<organization>
|
||||
<name>Inteligr8</name>
|
||||
<url>https://www.inteligr8.com</url>
|
||||
</organization>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>shade-jar</id>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern />
|
||||
<shadedPattern>shaded.keycloak.</shadedPattern>
|
||||
<excludes>
|
||||
<exclude>com.activiti.conf.*</exclude>
|
||||
<exclude>com.activiti.extension.conf.*</exclude>
|
||||
<exclude>com.inteligr8.activiti.**</exclude>
|
||||
<exclude>META-INF/**/*</exclude>
|
||||
</excludes>
|
||||
</relocation>
|
||||
</relocations>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>ossrh-release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>source</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>javadoc</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<show>public</show>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>ossrh-deploy</id>
|
||||
<phase>deploy</phase>
|
||||
<goals>
|
||||
<goal>deploy</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
<repositories>
|
||||
<repository>
|
||||
<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>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-client</artifactId>
|
||||
<version>6.3.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>activiti-app</artifactId>
|
||||
<version>24.3.0</version>
|
||||
<classifier>classes</classifier>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>aspose-transformation</artifactId>
|
||||
<groupId>com.activiti</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>aoservices</artifactId>
|
||||
<groupId>org.alfresco.officeservices</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<keycloak.version>23.0.7</keycloak.version>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<slf4j.version>1.7.36</slf4j.version>
|
||||
<spring-security-oauth2.version>6.3.2</spring-security-oauth2.version>
|
||||
<aps.version>24.3.0</aps.version>
|
||||
</properties>
|
||||
</project>
|
595
pom.xml
595
pom.xml
@@ -1,294 +1,301 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.inteligr8.activiti</groupId>
|
||||
<artifactId>auth-activiti-app-ext</artifactId>
|
||||
<version>2.0.0</version>
|
||||
|
||||
<name>Authentication & Authorization for APS</name>
|
||||
<description>An Alfresco Process Service App extension providing improved authentication and authorization support.</description>
|
||||
<url>https://bitbucket.org/inteligr8/auth-activiti-app-ext</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007</name>
|
||||
<url>https://www.gnu.org/licenses/lgpl-3.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:https://bitbucket.org/inteligr8/auth-activiti-app-ext.git</connection>
|
||||
<developerConnection>scm:git:git@bitbucket.org:inteligr8/auth-activiti-app-ext.git</developerConnection>
|
||||
<url>https://bitbucket.org/inteligr8/auth-activiti-app-ext</url>
|
||||
</scm>
|
||||
<organization>
|
||||
<name>Inteligr8</name>
|
||||
<url>https://www.inteligr8.com</url>
|
||||
</organization>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>brian.long</id>
|
||||
<name>Brian Long</name>
|
||||
<email>brian@inteligr8.com</email>
|
||||
<url>https://twitter.com/brianmlong</url>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
|
||||
<aps.version>25.1.1</aps.version>
|
||||
|
||||
<!-- for RAD -->
|
||||
<tomcat-rad.version>10-2.2</tomcat-rad.version>
|
||||
<aps.tomcat.opts.base>-Dspring.main.allow-circular-references=true \
|
||||
-Dhibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \
|
||||
-Dauth-ext.oauth.enabled=true \
|
||||
-Dauth-ext.external.id=keycloak \
|
||||
-Dauth-ext.sync.group.translate.patterns=aps-admin \
|
||||
-Dauth-ext.sync.group.translate.replacements=Superusers \
|
||||
-Dauth-ext.group.admins.validate=true</aps.tomcat.opts.base>
|
||||
<aps.timeout>120000</aps.timeout>
|
||||
<keycloak.realm>my-app</keycloak.realm>
|
||||
<oauth.client.id>aps-app-public</oauth.client.id>
|
||||
<oauth.client.secret></oauth.client.secret>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Needed for Activiti App Identity Service inheritance/override -->
|
||||
<!-- includes activiti-app-logic for API -->
|
||||
<dependency>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>activiti-app</artifactId>
|
||||
<version>${aps.version}</version>
|
||||
<classifier>classes</classifier>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<!-- not necessary to download for building -->
|
||||
<exclusion>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>aspose-transformation</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.alfresco.officeservices</groupId>
|
||||
<artifactId>aoservices</artifactId>
|
||||
</exclusion>
|
||||
<!-- very old and overrides real spring version -->
|
||||
<exclusion>
|
||||
<groupId>com.ryantenney.metrics</groupId>
|
||||
<artifactId>metrics-spring</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.security.oauth</groupId>
|
||||
<artifactId>spring-security-oauth2</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.repaint.maven</groupId>
|
||||
<artifactId>tiles-maven-plugin</artifactId>
|
||||
<version>2.40</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<tiles>
|
||||
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-aps-ext-rad-tile -->
|
||||
<!--
|
||||
<tile>com.inteligr8.ootbee:beedk-aps-ext-rad-tile:[1.1.0,2.0.0)</tile>
|
||||
-->
|
||||
<tile>com.inteligr8.ootbee:beedk-aps-ext-rad-tile:1.1-SNAPSHOT</tile>
|
||||
</tiles>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>activiti-oauth-confidential</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>secret</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<oauth.client.id>aps-app-confidential</oauth.client.id>
|
||||
<oauth.client.secret>a-secret</oauth.client.secret>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>activiti-oauth-legacy</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>rad</name>
|
||||
<value>!spring</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<aps.tomcat.opts>${aps.tomcat.opts.base} \
|
||||
-Dactiviti.identity-service.enabled=true \
|
||||
-Dactiviti.identity-service.realm=${keycloak.realm} \
|
||||
-Dactiviti.identity-service.auth-server-url=http://host.docker.internal:${keycloak.server.port} \
|
||||
-Dactiviti.identity-service.resource=${oauth.client.id} \
|
||||
-Dactiviti.identity-service.credentials.secret=${oauth.client.secret} \
|
||||
-Dactiviti.use-browser-based-logout=true \
|
||||
-Dalfresco.content.sso.redirect_uri=http://loalhost:8080/activiti-app/app/rest/integration/sso/confirm-auth-request</aps.tomcat.opts>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>activiti-oauth-spring</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>rad</name>
|
||||
<value>spring</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<aps.tomcat.opts>${aps.tomcat.opts.base} \
|
||||
-Dsecurity.oauth2.authentication.enabled=true \
|
||||
-Dsecurity.oauth2.client.registration.my-app.client-id=${oauth.client.id} \
|
||||
-Dsecurity.oauth2.client.registration.my-app.client-secret=${oauth.client.secret} \
|
||||
-Dsecurity.oauth2.client.registration.my-app.provider=aps-app \
|
||||
-Dsecurity.oauth2.client.provider.aps-app.issuer_uri=http://host.docker.internal:${keycloak.server.port}/realms/${keycloak.realm}</aps.tomcat.opts>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>rad-keycloak</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>rad</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<!-- Due to SSL restricitons in previous versions, testing against keyclaok is near impossible. -->
|
||||
<!-- This module should still work against nearly all versions of Keycloak that support the OIDC standards -->
|
||||
<keycloak.server.version>26.2</keycloak.server.version>
|
||||
<keycloak.server.port>8081</keycloak.server.port>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<version>0.46.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>run-keycloak</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals><goal>start</goal></goals>
|
||||
<configuration>
|
||||
<images>
|
||||
<image>
|
||||
<name>keycloak/keycloak:${keycloak.server.version}</name>
|
||||
<alias>keycloak</alias>
|
||||
<run>
|
||||
<cmd>start-dev --import-realm</cmd>
|
||||
<env>
|
||||
<KC_BOOTSTRAP_ADMIN_USERNAME>admin</KC_BOOTSTRAP_ADMIN_USERNAME>
|
||||
<KC_BOOTSTRAP_ADMIN_PASSWORD>admin</KC_BOOTSTRAP_ADMIN_PASSWORD>
|
||||
</env>
|
||||
<ports>
|
||||
<port>${keycloak.server.port}:8080</port>
|
||||
</ports>
|
||||
<network>
|
||||
<mode>custom</mode>
|
||||
<name>${project.artifactId}</name>
|
||||
</network>
|
||||
<extraHosts>
|
||||
<host>host.docker.internal:host-gateway</host>
|
||||
</extraHosts>
|
||||
<volumes>
|
||||
<bind>
|
||||
<volume>${project.basedir}/src/test/resources/keycloak-import:/opt/keycloak/data/import:ro</volume>
|
||||
</bind>
|
||||
</volumes>
|
||||
</run>
|
||||
</image>
|
||||
</images>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>ossrh-release</id>
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>source</id>
|
||||
<phase>package</phase>
|
||||
<goals><goal>jar-no-fork</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>javadoc</id>
|
||||
<phase>package</phase>
|
||||
<goals><goal>jar</goal></goals>
|
||||
<configuration>
|
||||
<show>public</show>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign</id>
|
||||
<phase>verify</phase>
|
||||
<goals><goal>sign</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>ossrh-deploy</id>
|
||||
<phase>deploy</phase>
|
||||
<goals><goal>deploy</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>activiti-releases</id>
|
||||
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-enterprise-releases</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.inteligr8.activiti</groupId>
|
||||
<artifactId>auth-activiti-app-ext</artifactId>
|
||||
<version>2.1-SNAPSHOT</version>
|
||||
|
||||
<name>Authentication & Authorization for APS</name>
|
||||
<description>An Alfresco Process Service App extension providing improved authentication and authorization support.</description>
|
||||
<url>https://git.inteligr8.com/inteligr8/auth-activiti-app-ext</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>GNU GENERAL PUBLIC LICENSE, Version 3, 29 June 2007</name>
|
||||
<url>https://www.gnu.org/licenses/lgpl-3.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:https://git.inteligr8.com/inteligr8/auth-activiti-app-ext.git</connection>
|
||||
<developerConnection>scm:git:git@git.inteligr8.com:inteligr8/auth-activiti-app-ext.git</developerConnection>
|
||||
<url>https://git.inteligr8.com/inteligr8/auth-activiti-app-ext</url>
|
||||
</scm>
|
||||
<organization>
|
||||
<name>Inteligr8</name>
|
||||
<url>https://www.inteligr8.com</url>
|
||||
</organization>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>brian.long</id>
|
||||
<name>Brian Long</name>
|
||||
<email>brian@inteligr8.com</email>
|
||||
<url>https://twitter.com/brianmlong</url>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
|
||||
<aps.version>25.1.1</aps.version>
|
||||
|
||||
<!-- for RAD -->
|
||||
<tomcat-rad.version>10-2.2</tomcat-rad.version>
|
||||
<aps.hotswap.enabled>false</aps.hotswap.enabled>
|
||||
<aps.tomcat.opts.base>-Dspring.main.allow-circular-references=true \
|
||||
-Dhibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \
|
||||
-Dauth-ext.external.id=keycloak \
|
||||
-Dauth-ext.sync.group.translate.patterns=aps-admin \
|
||||
-Dauth-ext.sync.group.translate.replacements=Superusers \
|
||||
-Dauth-ext.group.admins.validate=true</aps.tomcat.opts.base>
|
||||
<aps.timeout>120000</aps.timeout>
|
||||
<keycloak.realm>my-app</keycloak.realm>
|
||||
<oauth.client.id>aps-app-public</oauth.client.id>
|
||||
<oauth.client.secret></oauth.client.secret>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Needed for Activiti App Identity Service inheritance/override -->
|
||||
<!-- includes activiti-app-logic for API -->
|
||||
<dependency>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>activiti-app</artifactId>
|
||||
<version>${aps.version}</version>
|
||||
<classifier>classes</classifier>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<!-- not necessary to download for building -->
|
||||
<exclusion>
|
||||
<groupId>com.activiti</groupId>
|
||||
<artifactId>aspose-transformation</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.alfresco.officeservices</groupId>
|
||||
<artifactId>aoservices</artifactId>
|
||||
</exclusion>
|
||||
<!-- very old and overrides real spring version -->
|
||||
<exclusion>
|
||||
<groupId>com.ryantenney.metrics</groupId>
|
||||
<artifactId>metrics-spring</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.security.oauth</groupId>
|
||||
<artifactId>spring-security-oauth2</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.inteligr8.activiti</groupId>
|
||||
<artifactId>multiext-activiti-app-ext</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.repaint.maven</groupId>
|
||||
<artifactId>tiles-maven-plugin</artifactId>
|
||||
<version>2.40</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<tiles>
|
||||
<!-- Documentation: https://bitbucket.org/inteligr8/ootbee-beedk/src/stable/beedk-aps-ext-rad-tile -->
|
||||
<!--
|
||||
<tile>com.inteligr8.ootbee:beedk-aps-ext-rad-tile:[1.1.0,2.0.0)</tile>
|
||||
-->
|
||||
<tile>com.inteligr8.ootbee:beedk-aps-ext-rad-tile:1.1-SNAPSHOT</tile>
|
||||
</tiles>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>activiti-oauth-confidential</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>secret</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<oauth.client.id>aps-app-confidential</oauth.client.id>
|
||||
<oauth.client.secret>a-secret</oauth.client.secret>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>activiti-oauth-legacy</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>rad</name>
|
||||
<value>!spring</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<aps.tomcat.opts>${aps.tomcat.opts.base} \
|
||||
-Dactiviti.identity-service.enabled=true \
|
||||
-Dactiviti.identity-service.realm=${keycloak.realm} \
|
||||
-Dactiviti.identity-service.auth-server-url=http://host.docker.internal:${keycloak.server.port} \
|
||||
-Dactiviti.identity-service.resource=${oauth.client.id} \
|
||||
-Dactiviti.identity-service.credentials.secret=${oauth.client.secret} \
|
||||
-Dactiviti.use-browser-based-logout=true \
|
||||
-Dalfresco.content.sso.redirect_uri=http://loalhost:8080/activiti-app/app/rest/integration/sso/confirm-auth-request</aps.tomcat.opts>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>activiti-oauth-spring</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>rad</name>
|
||||
<value>spring</value>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<aps.tomcat.opts>${aps.tomcat.opts.base} \
|
||||
-Dsecurity.oauth2.authentication.enabled=true \
|
||||
-Dsecurity.oauth2.client.registration.my-app.client-id=${oauth.client.id} \
|
||||
-Dsecurity.oauth2.client.registration.my-app.client-secret=${oauth.client.secret} \
|
||||
-Dsecurity.oauth2.client.registration.my-app.provider=aps-app \
|
||||
-Dsecurity.oauth2.client.provider.aps-app.issuer_uri=http://host.docker.internal:${keycloak.server.port}/realms/${keycloak.realm}</aps.tomcat.opts>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>rad-keycloak</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>rad</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<!-- Due to SSL restricitons in previous versions, testing against keyclaok is near impossible. -->
|
||||
<!-- This module should still work against nearly all versions of Keycloak that support the OIDC standards -->
|
||||
<keycloak.server.version>26.2</keycloak.server.version>
|
||||
<keycloak.server.port>8081</keycloak.server.port>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<version>0.46.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>run-keycloak</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals><goal>start</goal></goals>
|
||||
<configuration>
|
||||
<images>
|
||||
<image>
|
||||
<name>keycloak/keycloak:${keycloak.server.version}</name>
|
||||
<alias>keycloak</alias>
|
||||
<run>
|
||||
<cmd>start-dev --import-realm</cmd>
|
||||
<env>
|
||||
<KC_BOOTSTRAP_ADMIN_USERNAME>admin</KC_BOOTSTRAP_ADMIN_USERNAME>
|
||||
<KC_BOOTSTRAP_ADMIN_PASSWORD>admin</KC_BOOTSTRAP_ADMIN_PASSWORD>
|
||||
</env>
|
||||
<ports>
|
||||
<port>${keycloak.server.port}:8080</port>
|
||||
</ports>
|
||||
<network>
|
||||
<mode>custom</mode>
|
||||
<name>${project.artifactId}</name>
|
||||
</network>
|
||||
<extraHosts>
|
||||
<host>host.docker.internal:host-gateway</host>
|
||||
</extraHosts>
|
||||
<volumes>
|
||||
<bind>
|
||||
<volume>${project.basedir}/src/test/resources/keycloak-import:/opt/keycloak/data/import:ro</volume>
|
||||
</bind>
|
||||
</volumes>
|
||||
</run>
|
||||
</image>
|
||||
</images>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>ossrh-release</id>
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>source</id>
|
||||
<phase>package</phase>
|
||||
<goals><goal>jar-no-fork</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>javadoc</id>
|
||||
<phase>package</phase>
|
||||
<goals><goal>jar</goal></goals>
|
||||
<configuration>
|
||||
<show>public</show>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign</id>
|
||||
<phase>verify</phase>
|
||||
<goals><goal>sign</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>ossrh-deploy</id>
|
||||
<phase>deploy</phase>
|
||||
<goals><goal>deploy</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>activiti-releases</id>
|
||||
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-enterprise-releases</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
|
@@ -1,35 +1,35 @@
|
||||
/*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.activiti.extension.conf;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator;
|
||||
|
||||
/**
|
||||
* A means for injecting packages to scan for the Spring context.
|
||||
*
|
||||
* @author brian@inteligr8.com
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan(
|
||||
basePackages = {
|
||||
"com.inteligr8.activiti.auth"
|
||||
},
|
||||
nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
|
||||
)
|
||||
public class AuthExtSpringComponentScanner {
|
||||
|
||||
}
|
||||
/*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.activiti.extension.conf;
|
||||
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator;
|
||||
|
||||
/**
|
||||
* A means for injecting packages to scan for the Spring context.
|
||||
*
|
||||
* @author brian@inteligr8.com
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan(
|
||||
basePackages = {
|
||||
"com.inteligr8.activiti.auth"
|
||||
},
|
||||
nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
|
||||
)
|
||||
public class AuthExtSpringComponentScanner {
|
||||
|
||||
}
|
||||
|
@@ -64,7 +64,7 @@ public class ActivitiAppAdministratorGroupFixer implements DataFixer {
|
||||
@Value("${auth-ext.group.admins.name:Superusers}")
|
||||
private String adminGroupName;
|
||||
|
||||
@Value("${auth-ext.sync.externalId:oauth}")
|
||||
@Value("${auth-ext.externalId:oauth}")
|
||||
protected String externalIdmSource;
|
||||
|
||||
@Value("${auth-ext.group.admins.validate:false}")
|
||||
|
@@ -58,7 +58,7 @@ public class ActivitiAppAdministratorMembersFixer implements DataFixer {
|
||||
@Value("${auth-ext.group.admins.name:Superusers}")
|
||||
private String adminGroupName;
|
||||
|
||||
@Value("${auth-ext.group.admins.externalId:#{null}}")
|
||||
@Value("${auth-ext.externalId:oauth}")
|
||||
private String adminGroupExternalId;
|
||||
|
||||
@Override
|
||||
|
@@ -1,32 +0,0 @@
|
||||
package com.inteligr8.activiti.auth;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.activiti.api.boot.BootstrapConfigurer;
|
||||
|
||||
@Component("bootstrap.proxy")
|
||||
@Primary
|
||||
public class Bootstrapper implements BootstrapConfigurer {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@Override
|
||||
public void applicationContextInitialized(ApplicationContext applicationContext) {
|
||||
Map<String, BootstrapConfigurer> bootstraps = applicationContext.getBeansOfType(BootstrapConfigurer.class);
|
||||
bootstraps.remove("bootstrap.proxy");
|
||||
|
||||
this.logger.debug("Executing {} bootstrap configurers", bootstraps.size());
|
||||
for (Entry<String, BootstrapConfigurer> bootstrap : bootstraps.entrySet()) {
|
||||
this.logger.trace("Executing bootstrap configurer: {}: {}", bootstrap.getKey(), bootstrap.getValue().getClass());
|
||||
bootstrap.getValue().applicationContextInitialized(applicationContext);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,8 @@
|
||||
package com.inteligr8.activiti.auth.oauth;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -10,11 +13,22 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||
|
||||
import com.activiti.domain.idm.Capabilities;
|
||||
import com.activiti.security.ActivitiAppRequestHeaderService;
|
||||
import com.activiti.security.ActivitiRestAuthorizationService;
|
||||
import com.activiti.security.ProtectedPaths;
|
||||
import com.activiti.security.identity.service.config.IdentityServiceEnabledCondition;
|
||||
import com.inteligr8.activiti.auth.service.JwtAuthenticationProvider;
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
|
||||
/**
|
||||
@@ -33,6 +47,15 @@ public class IdentityServiceConfigurationOverride {
|
||||
@Autowired
|
||||
private ApplicationContext appContext;
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationProvider jwtAuthenticationProvider;
|
||||
|
||||
@Autowired
|
||||
private ActivitiAppRequestHeaderService appRequestHeaderService;
|
||||
|
||||
@Autowired
|
||||
private ActivitiRestAuthorizationService restAuthorizationService;
|
||||
|
||||
@Bean("inteligr8.clientRegistrationRepository")
|
||||
@Primary
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
@@ -51,7 +74,7 @@ public class IdentityServiceConfigurationOverride {
|
||||
|
||||
@Bean(OVERRIDE_CLIENT_REGISTRATION_BEANNAME)
|
||||
@Primary
|
||||
public ClientRegistration clientRegistration1() throws ParseException, InterruptedException {
|
||||
public ClientRegistration clientRegistration() throws ParseException, InterruptedException {
|
||||
this.logger.trace("clientRegistration()");
|
||||
ClientRegistration clientRegistration = this.appContext.getBean(OOTB_CLIENT_REGISTRATION_BEANNAME, ClientRegistration.class);
|
||||
|
||||
@@ -62,4 +85,62 @@ public class IdentityServiceConfigurationOverride {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly higher priority than the one provided OOTB. This allows for
|
||||
* the bean injection of the `JwtAuthenticationConverter`.
|
||||
*
|
||||
* This is basically a copy of what is provided OOTB, but:
|
||||
*
|
||||
* - The ability to configure the `JwtAuthenticationConverter`.
|
||||
* - Allow non-UI access to `/app/rest/*`
|
||||
*
|
||||
* @see com.activiti.security.identity.service.config.IdentityServiceConfigurationApi#identityServiceApiWebSecurity
|
||||
*/
|
||||
@Bean("inteligr8.identityServiceApiWebSecurity")
|
||||
@Order(-5)
|
||||
public SecurityFilterChain identityServiceApiWebSecurity(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.securityMatchers(matchers -> {
|
||||
matchers.requestMatchers(
|
||||
// same as OOTB
|
||||
antMatcher(ProtectedPaths.API_URL_PATH + "/**"),
|
||||
|
||||
// want to also allow non-UI access to the the protected API
|
||||
// we do this for anything with an `Authorization` header, as the UI uses session-based authorization
|
||||
new AndRequestMatcher(new RequestHeaderRequestMatcher("Authorization"), antMatcher(ProtectedPaths.APP_URL_PATH + "/rest/**"))
|
||||
);
|
||||
})
|
||||
.csrf(csrf -> {
|
||||
csrf.disable();
|
||||
})
|
||||
.cors(withDefaults())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Stores no Session for API calls
|
||||
.oauth2ResourceServer(oauth2 ->
|
||||
oauth2.jwt(jwtConfigurer -> {
|
||||
// here is where we are injecting a Spring extendible `JwtAuthenticationConverter`.
|
||||
jwtConfigurer.jwtAuthenticationConverter(this.jwtAuthenticationProvider.create());
|
||||
})
|
||||
)
|
||||
.authorizeHttpRequests(request ->
|
||||
request
|
||||
// same as OOTB
|
||||
.requestMatchers(antMatcher(ProtectedPaths.API_URL_PATH + "/enterprise/**"))
|
||||
.access(this.appRequestHeaderService)
|
||||
.requestMatchers(antMatcher(ProtectedPaths.API_URL_PATH + "/**"))
|
||||
.access(this.restAuthorizationService)
|
||||
|
||||
// borrowed from OOTB /app/rest security
|
||||
.requestMatchers(antMatcher(ProtectedPaths.APP_URL_PATH + "/rest/reporting/**"))
|
||||
.hasAuthority(Capabilities.ACCESS_REPORTS)
|
||||
|
||||
.requestMatchers(
|
||||
antMatcher(ProtectedPaths.API_URL_PATH + "/**"),
|
||||
antMatcher(ProtectedPaths.APP_URL_PATH + "/rest/**")
|
||||
)
|
||||
.authenticated()
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -30,7 +30,10 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.activiti.domain.idm.Group;
|
||||
@@ -56,7 +59,7 @@ public class GroupSyncService {
|
||||
@Autowired
|
||||
private TenantFinderService tenantFinderService;
|
||||
|
||||
@Value("${auth-ext.sync.externalId:oauth}")
|
||||
@Value("${auth-ext.externalId:oauth}")
|
||||
protected String externalIdmSource;
|
||||
|
||||
@Value("${auth-ext.sync.group.createMissing:true}")
|
||||
@@ -129,28 +132,36 @@ public class GroupSyncService {
|
||||
}
|
||||
|
||||
public void sync(OidcUser oidcUser) {
|
||||
if (!oidcUser.hasClaim("groups")) {
|
||||
this.logger.warn("There is no 'groups' claim to synchronize: {}", oidcUser.getEmail());
|
||||
this.logger.debug("The claims available: {}", oidcUser.getClaims().keySet());
|
||||
this.sync(oidcUser.getEmail(), oidcUser);
|
||||
}
|
||||
|
||||
public void sync(Jwt jwt) {
|
||||
this.sync(jwt.getClaim(StandardClaimNames.EMAIL), jwt);
|
||||
}
|
||||
|
||||
public void sync(String email, ClaimAccessor claims) {
|
||||
if (!claims.hasClaim("groups")) {
|
||||
this.logger.warn("There is no 'groups' claim to synchronize: {}", email);
|
||||
this.logger.debug("The claims available: {}", claims.getClaims().keySet());
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> oidcGroups = new HashSet<>(oidcUser.getClaimAsStringList("groups"));
|
||||
this.logger.trace("Incoming OIDC groups: {}: {}", oidcUser.getEmail(), oidcGroups);
|
||||
Set<String> oidcGroups = new HashSet<>(claims.getClaimAsStringList("groups"));
|
||||
this.logger.trace("Incoming OIDC groups: {}: {}", email, oidcGroups);
|
||||
|
||||
oidcGroups = this.filterGroups(oidcGroups);
|
||||
oidcGroups = this.translateGroups(oidcGroups);
|
||||
Set<String> translatedGroups = this.translateGroups(oidcGroups);
|
||||
|
||||
this.logger.debug("Filtered/translated OIDC groups: {}: {}", oidcUser.getEmail(), oidcGroups);
|
||||
this.logger.debug("Filtered/translated OIDC groups: {}: {}", email, translatedGroups);
|
||||
|
||||
long tenantId = this.tenantFinderService.findTenantId();
|
||||
|
||||
// check Activiti groups
|
||||
User user = this.userService.findUserByEmailAndTenantId(oidcUser.getEmail(), tenantId);
|
||||
User user = this.userService.findUserByEmailAndTenantId(email, tenantId);
|
||||
if (user == null) {
|
||||
user = this.userService.findUserByEmail(oidcUser.getEmail());
|
||||
user = this.userService.findUserByEmail(email);
|
||||
if (user == null)
|
||||
throw new UsernameNotFoundException("The user could not be found: " + oidcUser.getEmail());
|
||||
throw new UsernameNotFoundException("The user could not be found: " + email);
|
||||
}
|
||||
User userWithGroups = this.userService.getUser(user.getId(), true);
|
||||
this.logger.debug("Discovered user belongs to {} APS groups: {}", userWithGroups.getGroups().size(), user.getExternalId());
|
||||
@@ -166,7 +177,7 @@ public class GroupSyncService {
|
||||
this.logger.trace("Inspecting APS group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId());
|
||||
|
||||
if (group.getExternalId() != null) {
|
||||
String oidcGroup = this.apsGroupExternalIdToOidcGroup(group.getExternalId());
|
||||
String translatedGroup = this.apsGroupExternalIdToTranslatedOidcGroup(group.getExternalId());
|
||||
|
||||
if (this.retenantUntenantedGroups && group.getTenantId() == null) {
|
||||
this.logger.warn("Moving tenant-less APS group to tenant: {} => {}", group.getName(), tenantId);
|
||||
@@ -175,24 +186,25 @@ public class GroupSyncService {
|
||||
this.groupService.save(group);
|
||||
}
|
||||
|
||||
if (oidcGroups.remove(oidcGroup)) {
|
||||
this.logger.trace("User already belongs to APS group mapped to by OIDC group: {}: {} => {}", user.getExternalId(), oidcGroup, group.getName());
|
||||
if (translatedGroups.remove(translatedGroup)) {
|
||||
this.logger.trace("User already belongs to APS group mapped to by (translated) OIDC group: {}: {} => {}", user.getExternalId(), translatedGroup, group.getName());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
String oidcGroup = this.apsGroupNameToOidcGroup(group.getName());
|
||||
|
||||
if (this.externalizeMatchingInternalGroups) {
|
||||
this.logger.warn("Classifying internal APS group as external: {} => {}", group.getName(), this.externalIdmSource);
|
||||
// register the group as external
|
||||
group.setExternalId(this.oidcGroupToApsGroupExternalId(oidcGroup));
|
||||
group.setLastUpdate(new Date());
|
||||
this.groupService.save(group);
|
||||
// internal role already existed and the user is already a member
|
||||
}
|
||||
String translatedGroup = this.apsGroupNameToTranslatedOidcGroup(group.getName());
|
||||
|
||||
if (oidcGroups.remove(oidcGroup)) {
|
||||
this.logger.trace("User already belongs to APS group mapped to by OIDC group: {}: {} => {}", user.getExternalId(), oidcGroup, group.getName());
|
||||
if (translatedGroups.remove(translatedGroup)) {
|
||||
this.logger.trace("User already belongs to APS group mapped to by (translated) OIDC group: {}: {} => {}", user.getExternalId(), translatedGroup, group.getName());
|
||||
|
||||
if (this.externalizeMatchingInternalGroups) {
|
||||
this.logger.warn("Classifying internal APS group as external: {} => {}", group.getName(), this.externalIdmSource);
|
||||
// register the group as external
|
||||
group.setExternalId(this.translatedOidcGroupToApsGroupExternalId(translatedGroup));
|
||||
group.setLastUpdate(new Date());
|
||||
this.groupService.save(group);
|
||||
// internal role already existed and the user is already a member
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (!this.syncInternalGroups) {
|
||||
this.logger.trace("Internal APS group membership sync disabled; not considering removal of user from APS group: {} => {}", user.getExternalId(), group.getName());
|
||||
@@ -211,21 +223,21 @@ public class GroupSyncService {
|
||||
}
|
||||
|
||||
// the user needs to be added to the remaining authorities
|
||||
for (String oidcGroup : oidcGroups) {
|
||||
this.logger.trace("Inspecting unaccounted for OIDC group: {}", oidcGroup);
|
||||
for (String translatedGroup : translatedGroups) {
|
||||
this.logger.trace("Inspecting unaccounted for (translated) OIDC group: {}", translatedGroup);
|
||||
|
||||
Group group;
|
||||
try {
|
||||
group = this.groupService.getGroupByExternalIdAndTenantId(this.oidcGroupToApsGroupExternalId(oidcGroup), tenantId);
|
||||
group = this.groupService.getGroupByExternalIdAndTenantId(this.translatedOidcGroupToApsGroupExternalId(translatedGroup), tenantId);
|
||||
} catch (NonUniqueResultException nure) {
|
||||
this.logger.warn("There are multiple groups matching the OIDC group for the external system: {} [{}]; skipping consideration of OIDC group", oidcGroup, this.externalIdmSource);
|
||||
this.logger.warn("There are multiple groups matching the (translated) OIDC group for the external system: {} [{}]; skipping consideration of OIDC group", translatedGroup, this.externalIdmSource);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (group == null && this.syncInternalGroups) {
|
||||
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.oidcGroupToApsGroupName(oidcGroup), tenantId);
|
||||
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.translatedOidcGroupToApsGroupName(translatedGroup), tenantId);
|
||||
if (groups.size() > 1) {
|
||||
this.logger.warn("There are multiple APS groups matching the OIDC group: {} [{}]; skipping consideration of OIDC group", oidcGroup, this.externalIdmSource);
|
||||
this.logger.warn("There are multiple APS groups matching the (translated) OIDC group: {} [{}]; skipping consideration of OIDC group", translatedGroup, this.externalIdmSource);
|
||||
continue;
|
||||
} else if (groups.size() == 1) {
|
||||
group = groups.iterator().next();
|
||||
@@ -233,7 +245,7 @@ public class GroupSyncService {
|
||||
if (this.externalizeMatchingInternalGroups) {
|
||||
this.logger.debug("Found an internal APS group; registering as external: {}", group.getName());
|
||||
|
||||
group.setExternalId(this.oidcGroupToApsGroupExternalId(oidcGroup));
|
||||
group.setExternalId(this.translatedOidcGroupToApsGroupExternalId(translatedGroup));
|
||||
group.setLastSyncTimeStamp(new Date());
|
||||
group.setLastUpdate(new Date());
|
||||
this.groupService.save(group);
|
||||
@@ -243,11 +255,11 @@ public class GroupSyncService {
|
||||
|
||||
if (group == null) {
|
||||
if (!this.createMissing) {
|
||||
this.logger.debug("APS Group does not exist for OIDC group; APS group creation is disabled; OIDC group will go unrecognized: {}", oidcGroup);
|
||||
this.logger.debug("APS Group does not exist for (translated) OIDC group; APS group creation is disabled; OIDC group will go unrecognized: {}", translatedGroup);
|
||||
continue;
|
||||
}
|
||||
|
||||
group = this.createApsGroup(oidcGroup, tenantId);
|
||||
group = this.createApsGroup(translatedGroup, tenantId);
|
||||
}
|
||||
|
||||
if (this.syncAdditions) {
|
||||
@@ -260,13 +272,13 @@ public class GroupSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
protected Group createApsGroup(String oidcGroup, long tenantId) {
|
||||
this.logger.debug("APS Group does not exist for OIDC group; will attempt to create: {}", oidcGroup);
|
||||
String name = this.oidcGroupToApsGroupName(oidcGroup);
|
||||
String externalId = this.oidcGroupToApsGroupExternalId(oidcGroup);
|
||||
protected Group createApsGroup(String translatedGroup, long tenantId) {
|
||||
this.logger.debug("APS Group does not exist for (translated) OIDC group; will attempt to create: {}", translatedGroup);
|
||||
String name = this.translatedOidcGroupToApsGroupName(translatedGroup);
|
||||
String externalId = this.translatedOidcGroupToApsGroupExternalId(translatedGroup);
|
||||
|
||||
boolean syncAsOrg = this.isOidcGroupToBeOrganization(oidcGroup);
|
||||
this.logger.trace("Creating new APS group as {}: {}", syncAsOrg ? "organization" : "capability", oidcGroup);
|
||||
boolean syncAsOrg = this.isTranslatedOidcGroupToBeOrganization(translatedGroup);
|
||||
this.logger.trace("Creating new APS group as {}: {}", syncAsOrg ? "organization" : "capability", translatedGroup);
|
||||
int type = syncAsOrg ? Group.TYPE_FUNCTIONAL_GROUP : Group.TYPE_SYSTEM_GROUP;
|
||||
|
||||
Group apsGroup = this.groupService.createGroupFromExternalStore(name, tenantId, type, null, externalId, new Date());
|
||||
@@ -330,29 +342,29 @@ public class GroupSyncService {
|
||||
return translatedGroups;
|
||||
}
|
||||
|
||||
private String oidcGroupToApsGroupExternalId(String group) {
|
||||
private String translatedOidcGroupToApsGroupExternalId(String group) {
|
||||
return this.externalIdmSource + "_" + group;
|
||||
}
|
||||
|
||||
private String apsGroupExternalIdToOidcGroup(String externalId) {
|
||||
private String apsGroupExternalIdToTranslatedOidcGroup(String externalId) {
|
||||
int underscorePos = externalId.indexOf('_');
|
||||
return underscorePos < 0 ? externalId : externalId.substring(underscorePos + 1);
|
||||
}
|
||||
|
||||
private String oidcGroupToApsGroupName(String group) {
|
||||
private String translatedOidcGroupToApsGroupName(String group) {
|
||||
return group;
|
||||
}
|
||||
|
||||
private String apsGroupNameToOidcGroup(String externalId) {
|
||||
private String apsGroupNameToTranslatedOidcGroup(String externalId) {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
private boolean isOidcGroupToBeOrganization(String role) {
|
||||
private boolean isTranslatedOidcGroupToBeOrganization(String translatedGroup) {
|
||||
if (this.capabilities.isEmpty())
|
||||
return true;
|
||||
|
||||
for (Pattern regex : this.capabilities) {
|
||||
Matcher matcher = regex.matcher(role);
|
||||
Matcher matcher = regex.matcher(translatedGroup);
|
||||
if (matcher.matches())
|
||||
return false;
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package com.inteligr8.activiti.auth.service;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
public interface JwtAuthenticationProvider {
|
||||
|
||||
Converter<Jwt, AbstractAuthenticationToken> create();
|
||||
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
package com.inteligr8.activiti.auth.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import com.activiti.security.identity.service.config.JwtAuthenticationToken;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
public class SyncingJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final UserSyncService userSyncService;
|
||||
private final GroupSyncService groupSyncService;
|
||||
|
||||
public SyncingJwtAuthenticationConverter(UserDetailsService userDetailsService, UserSyncService userSyncService, GroupSyncService groupSyncService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.userSyncService = userSyncService;
|
||||
this.groupSyncService = groupSyncService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAuthenticationToken convert(Jwt source) {
|
||||
this.logger.trace("convert({}, {})", source.getId(), source.getClaim("email"));
|
||||
|
||||
try {
|
||||
this.logger.debug("jwt: {}", new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(source));
|
||||
} catch (JsonProcessingException jpe) {
|
||||
this.logger.error("error", jpe);
|
||||
}
|
||||
|
||||
this.userSyncService.sync(source);
|
||||
this.groupSyncService.sync(source);
|
||||
|
||||
UserDetails springUser = this.userDetailsService.loadUserByUsername(source.getClaim("email"));
|
||||
return new JwtAuthenticationToken(
|
||||
springUser,
|
||||
new ArrayList<>(springUser.getAuthorities()));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.inteligr8.activiti.auth.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SyncingJwtAuthenticationProvider implements JwtAuthenticationProvider {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Autowired
|
||||
private UserSyncService userSyncService;
|
||||
|
||||
@Autowired
|
||||
private GroupSyncService groupSyncService;
|
||||
|
||||
@Override
|
||||
public Converter<Jwt, AbstractAuthenticationToken> create() {
|
||||
return new SyncingJwtAuthenticationConverter(this.userDetailsService, this.userSyncService, this.groupSyncService);
|
||||
}
|
||||
|
||||
}
|
@@ -20,12 +20,16 @@ import com.activiti.security.identity.service.config.IdentityServiceKeycloakProp
|
||||
* Activiti Identity Service configuration is enabled. When it isn't
|
||||
* enabled, it will still serve as the default OIDC user service for
|
||||
* Spring Security.
|
||||
*
|
||||
* This is only executed with non-API authentication and authorization use
|
||||
* cases. API authentication/authorization uses the
|
||||
* `SyncingJwtAuthenitcationConverter`.
|
||||
*/
|
||||
@Component
|
||||
@Primary
|
||||
public class OIDCUserService extends OidcUserService {
|
||||
public class SyncingUserService extends OidcUserService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(OIDCUserService.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(SyncingUserService.class);
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
@@ -9,7 +9,10 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.activiti.domain.idm.Group;
|
||||
@@ -36,20 +39,20 @@ public class UserSyncService {
|
||||
@Autowired
|
||||
private TenantFinderService tenantFinderService;
|
||||
|
||||
@Value("${auth-ext.sync.externalId:oauth}")
|
||||
@Value("${auth-ext.externalId:oauth}")
|
||||
protected String externalIdmSource;
|
||||
|
||||
@Value("${auth-ext.sync.user.createMissing:true}")
|
||||
protected boolean createMissingUser;
|
||||
|
||||
@Value("${auth-ext.sync.user.requireGroup:#{null}}")
|
||||
@Value("${auth-ext.sync.user.requireOidcGroup:#{null}}")
|
||||
protected String requiredGroup;
|
||||
|
||||
@Value("${auth-ext.sync.user.clearNewUserGroups:true}")
|
||||
protected boolean clearNewUserGroups;
|
||||
|
||||
public void sync(OidcUser oidcUser) {
|
||||
UserDetails springUser = this.loadSpringUser(oidcUser);
|
||||
UserDetails springUser = this.loadSpringUser(oidcUser.getEmail(), oidcUser.getGivenName(), oidcUser.getFamilyName(), oidcUser);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Loaded Spring Security user: {}: {}", springUser.getUsername(), springUser.getAuthorities());
|
||||
} else {
|
||||
@@ -57,42 +60,58 @@ public class UserSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private UserDetails loadSpringUser(OidcUser oidcUser) throws UsernameNotFoundException {
|
||||
public void sync(Jwt jwt) {
|
||||
String email = jwt.getClaim(StandardClaimNames.EMAIL);
|
||||
if (email == null)
|
||||
throw new IllegalArgumentException("An '" + StandardClaimNames.EMAIL + "' claim is required");
|
||||
|
||||
String givenName = jwt.getClaim(StandardClaimNames.GIVEN_NAME);
|
||||
String familyName = jwt.getClaim(StandardClaimNames.FAMILY_NAME);
|
||||
|
||||
UserDetails springUser = this.loadSpringUser(email, givenName, familyName, jwt);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Loaded Spring Security user: {}: {}", springUser.getUsername(), springUser.getAuthorities());
|
||||
} else {
|
||||
this.logger.debug("Loaded Spring Security user: {}", springUser.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
private UserDetails loadSpringUser(String email, String givenName, String familyName, ClaimAccessor claims) throws UsernameNotFoundException {
|
||||
try {
|
||||
UserDetails springUser = this.userDetailsService.loadUserByUsername(oidcUser.getEmail());
|
||||
this.logger.debug("Loaded APS user: {} => {}", oidcUser.getEmail(), springUser.getUsername());
|
||||
UserDetails springUser = this.userDetailsService.loadUserByUsername(email);
|
||||
this.logger.debug("Loaded APS user: {} => {}", email, springUser.getUsername());
|
||||
return springUser;
|
||||
} catch (UsernameNotFoundException unfe) {
|
||||
this.logger.debug("User does not exist: {}", unfe.getMessage());
|
||||
if (!this.createMissingUser)
|
||||
throw unfe;
|
||||
|
||||
if (this.requiredGroup != null && (!oidcUser.hasClaim("groups") || !oidcUser.getClaimAsStringList("groups").contains(this.requiredGroup))) {
|
||||
this.logger.info("User does not exist and does not have the required OIDC group to be created: {} ", oidcUser.getEmail(), this.requiredGroup);
|
||||
if (this.requiredGroup != null && (!claims.hasClaim("groups") || !claims.getClaimAsStringList("groups").contains(this.requiredGroup))) {
|
||||
this.logger.info("User does not exist and does not have the required OIDC group to be created: {} ", email, this.requiredGroup);
|
||||
throw unfe;
|
||||
}
|
||||
|
||||
this.logger.debug("User does not exist; will attempt to create: {}", oidcUser.getEmail());
|
||||
User apsUser = this.createApsUser(oidcUser);
|
||||
this.logger.debug("User does not exist; will attempt to create: {}", email);
|
||||
User apsUser = this.createApsUser(email, givenName, familyName);
|
||||
if (this.clearNewUserGroups) {
|
||||
apsUser = this.userService.getUser(apsUser.getId(), true);
|
||||
if (this.logger.isDebugEnabled())
|
||||
this.logger.debug("User is new; clearing default groups: {}: {}", oidcUser.getEmail(), apsUser.getGroups().stream().map(group -> group.getName()).toList());
|
||||
this.logger.debug("User is new; clearing default groups: {}: {}", email, apsUser.getGroups().stream().map(group -> group.getName()).toList());
|
||||
this.deleteApsUserGroups(apsUser);
|
||||
}
|
||||
return this.userDetailsService.loadByUserId(apsUser.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private User createApsUser(OidcUser oidcUser) {
|
||||
private User createApsUser(String email, String givenName, String familyName) {
|
||||
long tenantId = this.tenantFinderService.findTenantId();
|
||||
|
||||
User user = this.userService.createNewUserFromExternalStore(
|
||||
oidcUser.getEmail(),
|
||||
oidcUser.getGivenName(),
|
||||
oidcUser.getFamilyName(),
|
||||
email,
|
||||
givenName,
|
||||
familyName,
|
||||
tenantId,
|
||||
oidcUser.getEmail(),
|
||||
email,
|
||||
this.externalIdmSource,
|
||||
new Date());
|
||||
this.logger.info("Created user: {} => {}", user.getId(), user.getEmail());
|
||||
|
@@ -101,14 +101,14 @@
|
||||
"profile",
|
||||
"roles",
|
||||
"basic",
|
||||
"email"
|
||||
"email",
|
||||
"microprofile-jwt"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"address",
|
||||
"phone",
|
||||
"organization",
|
||||
"offline_access",
|
||||
"microprofile-jwt"
|
||||
"offline_access"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -156,15 +156,76 @@
|
||||
"profile",
|
||||
"roles",
|
||||
"basic",
|
||||
"email"
|
||||
"email",
|
||||
"microprofile-jwt"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"address",
|
||||
"phone",
|
||||
"organization",
|
||||
"offline_access",
|
||||
"microprofile-jwt"
|
||||
"offline_access"
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientId": "cli",
|
||||
"name": "Command Line Tools",
|
||||
"description": "",
|
||||
"rootUrl": "",
|
||||
"adminUrl": "",
|
||||
"baseUrl": "",
|
||||
"surrogateAuthRequired": false,
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "eJa5W7bv4ohFbr7QRtaCk0eccRFoYM5x",
|
||||
"redirectUris": [
|
||||
"/*"
|
||||
],
|
||||
"webOrigins": [
|
||||
"/*"
|
||||
],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
"standardFlowEnabled": false,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"serviceAccountsEnabled": true,
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": true,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "false",
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"client.secret.creation.time": "1747506410",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"standard.token.exchange.enabled": "true",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": true,
|
||||
"nodeReRegistrationTimeout": -1,
|
||||
"defaultClientScopes": [
|
||||
"web-origins",
|
||||
"acr",
|
||||
"profile",
|
||||
"roles",
|
||||
"basic",
|
||||
"email",
|
||||
"microprofile-jwt"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"address",
|
||||
"phone",
|
||||
"organization",
|
||||
"offline_access"
|
||||
],
|
||||
"access": {
|
||||
"view": true,
|
||||
"configure": true,
|
||||
"manage": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
|
35
src/test/vscode/simple.http
Normal file
35
src/test/vscode/simple.http
Normal file
@@ -0,0 +1,35 @@
|
||||
@keycloakRealm = my-app
|
||||
@keycloakBaseUrl = http://localhost:8081
|
||||
@oauthUrl = {{keycloakBaseUrl}}/realms/{{keycloakRealm}}
|
||||
@keycloakTokenUrl = {{oauthUrl}}/protocol/openid-connect/token
|
||||
@oauthClientId = cli
|
||||
@oauthClientSecret = eJa5W7bv4ohFbr7QRtaCk0eccRFoYM5x
|
||||
@apsBaseUrl = http://localhost:8080/activiti-app
|
||||
|
||||
### Token
|
||||
# @name token
|
||||
curl -LX POST {{keycloakTokenUrl}} \
|
||||
-H 'Content-type: application/x-www-form-urlencoded' \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id={{oauthClientId}}" \
|
||||
-d "client_secret={{oauthClientSecret}}"
|
||||
|
||||
@accessToken = {{token.response.body.access_token}}
|
||||
@auth = Bearer {{accessToken}}
|
||||
|
||||
### APS Version
|
||||
# @name version
|
||||
GET {{apsBaseUrl}}/api/enterprise/app-version
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### APS Tenants
|
||||
# @name tenants
|
||||
GET {{apsBaseUrl}}/api/enterprise/admin/tenants
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
@tenantId = {{tenants.response.body.0.id}}
|
||||
|
||||
### APS Templates
|
||||
# @name templates
|
||||
GET {{apsBaseUrl}}/app/rest/document-templates?tenantId={{tenantId}}&start=0&size=10&sort=sort_by_name_asc
|
||||
Authorization: Bearer {{accessToken}}
|
Reference in New Issue
Block a user