mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-09-17 14:21:10 +00:00
Basic working state of repo-tier subsystem
- supports synch of users / groups - supports configurable / extensible attribute mapping - supports configurable / extensible filtering - supports claim / role mapping - supports Keycloak auth redirect, Bearer and Basic authentication - bundles newer Keycloak libraries than Alfresco default via shaded dependency artifacts
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
49
pom.xml
49
pom.xml
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>de.acosix.alfresco.maven</groupId>
|
<groupId>de.acosix.alfresco.maven</groupId>
|
||||||
<artifactId>de.acosix.alfresco.maven.project.parent-6.1.2</artifactId>
|
<artifactId>de.acosix.alfresco.maven.project.parent-6.0.7</artifactId>
|
||||||
<version>1.2.1-SNAPSHOT</version>
|
<version>1.3.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>de.acosix.alfresco.keycloak</groupId>
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<name>Acosix Alfresco Keycloak - Parent</name>
|
<name>Acosix Alfresco Keycloak - Parent</name>
|
||||||
<description>Addon to provide Keycloak-related customisations / extensions to out-of-the-box Alfresco authentication and authorization functionality</description>
|
<description>Addon to provide Keycloak-related customisations / extensions to out-of-the-box Alfresco authentication and authorisation functionality</description>
|
||||||
<url>https://github.com/Acosix/alfresco-keycloak</url>
|
<url>https://github.com/Acosix/alfresco-keycloak</url>
|
||||||
|
|
||||||
<licenses>
|
<licenses>
|
||||||
@@ -68,12 +68,14 @@
|
|||||||
<messages.packageId>acosix.keycloak</messages.packageId>
|
<messages.packageId>acosix.keycloak</messages.packageId>
|
||||||
<moduleId>acosix-keycloak</moduleId>
|
<moduleId>acosix-keycloak</moduleId>
|
||||||
|
|
||||||
<!-- Java 7 is out of support -->
|
|
||||||
<maven.compiler.source>1.8</maven.compiler.source>
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
<maven.compiler.target>1.8</maven.compiler.target>
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
|
|
||||||
|
<maven.shade.version>3.2.1</maven.shade.version>
|
||||||
|
|
||||||
<keycloak.version>6.0.1</keycloak.version>
|
<keycloak.version>6.0.1</keycloak.version>
|
||||||
<!-- lowest common denominator of Repository / Share in 6.1 -->
|
<resteasy.version>3.6.3.Final</resteasy.version>
|
||||||
|
<!-- lowest common denominator of Repository / Share in 6.0 -->
|
||||||
<apache.httpclient.version>4.5.1</apache.httpclient.version>
|
<apache.httpclient.version>4.5.1</apache.httpclient.version>
|
||||||
<apache.httpcore.version>4.4.3</apache.httpcore.version>
|
<apache.httpcore.version>4.4.3</apache.httpcore.version>
|
||||||
|
|
||||||
@@ -125,6 +127,30 @@
|
|||||||
<artifactId>keycloak-authz-client</artifactId>
|
<artifactId>keycloak-authz-client</artifactId>
|
||||||
<version>${keycloak.version}</version>
|
<version>${keycloak.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-admin-client</artifactId>
|
||||||
|
<version>${keycloak.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-client</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-multipart-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- HttpClient already bundled by both Repository and Share web apps -->
|
<!-- HttpClient already bundled by both Repository and Share web apps -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -205,11 +231,20 @@
|
|||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>${maven.shade.version}</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
|
<module>repository-dependencies</module>
|
||||||
<module>repository</module>
|
<module>repository</module>
|
||||||
|
<module>share-dependencies</module>
|
||||||
<module>share</module>
|
<module>share</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
53
repository-dependencies/dependency-reduced-pom.xml
Normal file
53
repository-dependencies/dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?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">
|
||||||
|
<parent>
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.repo.deps</artifactId>
|
||||||
|
<name>Acosix Alfresco Keycloak - Repository Dependencies Module</name>
|
||||||
|
<description>Aggregate (Uber-)JAR of all dependencies for the Acosix Alfresco Keycloak Repository Module</description>
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.keycloak</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.repo.deps.keycloak</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.jboss.logging</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.repo.deps.jboss.logging</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<transformers>
|
||||||
|
<transformer />
|
||||||
|
<transformer />
|
||||||
|
<transformer>
|
||||||
|
<addHeader>false</addHeader>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
127
repository-dependencies/pom.xml
Normal file
127
repository-dependencies/pom.xml
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!--
|
||||||
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<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/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.repo.deps</artifactId>
|
||||||
|
<name>Acosix Alfresco Keycloak - Repository Dependencies Module</name>
|
||||||
|
<description>Aggregate (Uber-)JAR of all dependencies for the Acosix Alfresco Keycloak Repository Module</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-adapter-core</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-servlet-adapter-spi</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-servlet-filter-adapter</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.keycloak</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.repo.deps.keycloak</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.jboss.logging</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.repo.deps.jboss.logging</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<transformers>
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
|
||||||
|
<addHeader>false</addHeader>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -28,9 +28,6 @@
|
|||||||
<name>Acosix Alfresco Keycloak - Repository Module</name>
|
<name>Acosix Alfresco Keycloak - Repository Module</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- Alfresco 6.x bundles an old version of adapter libraries -->
|
|
||||||
<!-- adapt dependencies to match -->
|
|
||||||
<keycloak.version>4.6.0.Final</keycloak.version>
|
|
||||||
<docker.tests.keycloakPort>8380</docker.tests.keycloakPort>
|
<docker.tests.keycloakPort>8380</docker.tests.keycloakPort>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -39,6 +36,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.alfresco</groupId>
|
<groupId>org.alfresco</groupId>
|
||||||
<artifactId>alfresco-remote-api</artifactId>
|
<artifactId>alfresco-remote-api</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -46,17 +49,17 @@
|
|||||||
<artifactId>javax.servlet-api</artifactId>
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- not included / packaged by Alfresco 6.x - only SPI -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>${project.groupId}</groupId>
|
||||||
<artifactId>keycloak-servlet-filter-adapter</artifactId>
|
<artifactId>de.acosix.alfresco.keycloak.repo.deps</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>*</artifactId>
|
<artifactId>*</artifactId>
|
||||||
</exclusion>
|
</exclusion>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
<artifactId>*</artifactId>
|
<artifactId>*</artifactId>
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
@@ -115,7 +118,7 @@
|
|||||||
<!-- no change to Share image (we don't use it) -->
|
<!-- no change to Share image (we don't use it) -->
|
||||||
</image>
|
</image>
|
||||||
<image>
|
<image>
|
||||||
<!-- no change to Search image (we don't use it) -->
|
<!-- no change to Search image -->
|
||||||
</image>
|
</image>
|
||||||
<image>
|
<image>
|
||||||
<name>jboss/keycloak</name>
|
<name>jboss/keycloak</name>
|
||||||
|
52
repository/src/main/asembly/amp.xml
Normal file
52
repository/src/main/asembly/amp.xml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!--
|
||||||
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||||
|
<id>amp</id>
|
||||||
|
<formats>
|
||||||
|
<format>amp</format>
|
||||||
|
</formats>
|
||||||
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
|
<componentDescriptors>
|
||||||
|
<componentDescriptor>assemblies/amp-lib-component.xml</componentDescriptor>
|
||||||
|
<componentDescriptor>assemblies/amp-config-component.xml</componentDescriptor>
|
||||||
|
<componentDescriptor>assemblies/amp-messages-component.xml</componentDescriptor>
|
||||||
|
<componentDescriptor>assemblies/amp-repo-webscript-component.xml</componentDescriptor>
|
||||||
|
<componentDescriptor>assemblies/amp-surf-webscript-component.xml</componentDescriptor>
|
||||||
|
<componentDescriptor>assemblies/amp-templates-component.xml</componentDescriptor>
|
||||||
|
<componentDescriptor>assemblies/amp-webapp-component.xml</componentDescriptor>
|
||||||
|
</componentDescriptors>
|
||||||
|
<fileSets>
|
||||||
|
<fileSet>
|
||||||
|
<directory>${project.basedir}</directory>
|
||||||
|
<outputDirectory></outputDirectory>
|
||||||
|
<includes>
|
||||||
|
<include>*.properties</include>
|
||||||
|
</includes>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
<lineEnding>crlf</lineEnding>
|
||||||
|
</fileSet>
|
||||||
|
</fileSets>
|
||||||
|
<dependencySets>
|
||||||
|
<dependencySet>
|
||||||
|
<outputDirectory>lib</outputDirectory>
|
||||||
|
<includes>
|
||||||
|
<include>${project.groupId}:${project.artifactId}.deps:*</include>
|
||||||
|
</includes>
|
||||||
|
</dependencySet>
|
||||||
|
</dependencySets>
|
||||||
|
</assembly>
|
@@ -1,3 +1,5 @@
|
|||||||
|
${moduleId}.authorityServiceEnhancement.enabled=true
|
||||||
|
|
||||||
cache.${moduleId}.ssoToSessionCache.maxItems=10000
|
cache.${moduleId}.ssoToSessionCache.maxItems=10000
|
||||||
cache.${moduleId}.ssoToSessionCache.timeToLiveSeconds=0
|
cache.${moduleId}.ssoToSessionCache.timeToLiveSeconds=0
|
||||||
cache.${moduleId}.ssoToSessionCache.maxIdleSeconds=0
|
cache.${moduleId}.ssoToSessionCache.maxIdleSeconds=0
|
||||||
@@ -48,4 +50,18 @@ cache.${moduleId}.sessionToPrincipalCache.readBackupData=false
|
|||||||
# explicitly not clearable - should be cleared via Keycloak back-channel action
|
# explicitly not clearable - should be cleared via Keycloak back-channel action
|
||||||
cache.${moduleId}.sessionToPrincipalCache.clearable=false
|
cache.${moduleId}.sessionToPrincipalCache.clearable=false
|
||||||
# replicate, not distribute
|
# replicate, not distribute
|
||||||
cache.${moduleId}.sessionToPrincipalCache.ignite.cache.type=replicated
|
cache.${moduleId}.sessionToPrincipalCache.ignite.cache.type=replicated
|
||||||
|
|
||||||
|
cache.${moduleId}.ticketTokenCache.maxItems=10000
|
||||||
|
cache.${moduleId}.ticketTokenCache.timeToLiveSeconds=0
|
||||||
|
cache.${moduleId}.ticketTokenCache.maxIdleSeconds=0
|
||||||
|
cache.${moduleId}.ticketTokenCache.cluster.type=fully-distributed
|
||||||
|
cache.${moduleId}.ticketTokenCache.backup-count=1
|
||||||
|
cache.${moduleId}.ticketTokenCache.eviction-policy=LRU
|
||||||
|
cache.${moduleId}.ticketTokenCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
|
||||||
|
cache.${moduleId}.ticketTokenCache.readBackupData=false
|
||||||
|
# dangerous to be cleared, as roles / claims can no longer be mapped
|
||||||
|
# would always be better to just invalidate the tickets themselves
|
||||||
|
cache.${moduleId}.ticketTokenCache.clearable=false
|
||||||
|
# replicate, not distribute
|
||||||
|
cache.${moduleId}.ticketTokenCache.ignite.cache.type=replicated
|
@@ -1 +1,2 @@
|
|||||||
log4j.logger.${project.artifactId}=INFO
|
log4j.logger.${project.artifactId}=INFO
|
||||||
|
log4j.logger.${project.artifactId}.deps=ERROR
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -31,20 +31,34 @@
|
|||||||
value="de.acosix.alfresco.utility.repo.subsystems.SubsystemChildApplicationContextManager" />
|
value="de.acosix.alfresco.utility.repo.subsystems.SubsystemChildApplicationContextManager" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="${moduleId}-sessionIdMapper-ssoToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
<bean id="${moduleId}-enhanceAuthorityService"
|
||||||
|
class="de.acosix.alfresco.utility.common.spring.ImplementationClassReplacingBeanDefinitionRegistryPostProcessor">
|
||||||
|
<property name="enabledPropertyKey" value="${moduleId}.authorityServiceEnhancement.enabled" />
|
||||||
|
<property name="propertiesSource" ref="global-properties" />
|
||||||
|
|
||||||
|
<property name="targetBeanName" value="authorityService" />
|
||||||
|
<property name="originalClassName" value="org.alfresco.repo.security.authority.AuthorityServiceImpl" />
|
||||||
|
<property name="replacementClassName" value="${project.artifactId}.authority.GrantedAuthorityAwareAuthorityServiceImpl" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean name="${moduleId}.ssoToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||||
<constructor-arg value="cache.${moduleId}.ssoToSessionCache" />
|
<constructor-arg value="cache.${moduleId}.ssoToSessionCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="${moduleId}-sessionIdMapper-sessionToSsoCache" factory-bean="cacheFactory" factory-method="createCache">
|
<bean name="${moduleId}.sessionToSsoCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||||
<constructor-arg value="cache.${moduleId}.sessionToSsoCache" />
|
<constructor-arg value="cache.${moduleId}.sessionToSsoCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="${moduleId}-sessionIdMapper-principalToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
<bean name="${moduleId}.principalToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||||
<constructor-arg value="cache.${moduleId}.principalToSessionCache" />
|
<constructor-arg value="cache.${moduleId}.principalToSessionCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="${moduleId}-sessionIdMapper-sessionToPrincipalCache" factory-bean="cacheFactory" factory-method="createCache">
|
<bean name="${moduleId}.sessionToPrincipalCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||||
<constructor-arg value="cache.${moduleId}.sessionToPrincipalCache" />
|
<constructor-arg value="cache.${moduleId}.sessionToPrincipalCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean name="${moduleId}-ticketTokenCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||||
|
<constructor-arg value="cache.${moduleId}.ticketTokenCache" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
</beans>
|
</beans>
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="sessionIdMapper" class="${project.artifactId}.authentication.SimpleCacheBackedSessionIdMapper">
|
<bean id="sessionIdMapper" class="${project.artifactId}.authentication.SimpleCacheBackedSessionIdMapper">
|
||||||
<property name="ssoToSession" ref="${moduleId}-sessionIdMapper-ssoToSessionCache" />
|
<property name="ssoToSession" ref="${moduleId}.ssoToSessionCache" />
|
||||||
<property name="sessionToSso" ref="${moduleId}-sessionIdMapper-sessionToSsoCache" />
|
<property name="sessionToSso" ref="${moduleId}.sessionToSsoCache" />
|
||||||
<property name="principalToSession" ref="${moduleId}-sessionIdMapper-principalToSessionCache" />
|
<property name="principalToSession" ref="${moduleId}.principalToSessionCache" />
|
||||||
<property name="sessionToPrincipal" ref="${moduleId}-sessionIdMapper-sessionToPrincipalCache" />
|
<property name="sessionToPrincipal" ref="${moduleId}.sessionToPrincipalCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="authenticationComponent" class="${project.artifactId}.authentication.KeycloakAuthenticationComponent"
|
<bean id="authenticationComponent" class="${project.artifactId}.authentication.KeycloakAuthenticationComponent"
|
||||||
@@ -49,10 +49,11 @@
|
|||||||
<property name="active" value="${keycloak.authentication.enabled}" />
|
<property name="active" value="${keycloak.authentication.enabled}" />
|
||||||
<property name="defaultAdministratorUserNameList" value="${keycloak.authentication.defaultAdministratorUserNames}" />
|
<property name="defaultAdministratorUserNameList" value="${keycloak.authentication.defaultAdministratorUserNames}" />
|
||||||
<property name="allowUserNamePasswordLogin" value="${keycloak.authentication.allowUserNamePasswordLogin}" />
|
<property name="allowUserNamePasswordLogin" value="${keycloak.authentication.allowUserNamePasswordLogin}" />
|
||||||
<property name="allowGuestLogin" value="${keycloak.authentication.allowGuestLogin}" />
|
<property name="allowGuestLogin" value="${keycloak.authentication.failExpiredUserNamePasswordLoginTokens}" />
|
||||||
<property name="adapterConfig" ref="keycloakAdapterConfig" />
|
<property name="failExpiredTicketTokens" value="${keycloak.authentication.allowGuestLogin}" />
|
||||||
<property name="connectionTimeout" value="${keycloak.authentication.connectionTimeout}" />
|
<property name="mapRoles" value="${keycloak.authentication.mapRoles}" />
|
||||||
<property name="socketTimeout" value="${keycloak.authentication.socketTimeout}" />
|
<property name="mapPersonPropertiesOnLogin" value="${keycloak.authentication.mapPersonPropertiesOnLogin}" />
|
||||||
|
<property name="deployment" ref="keycloakDeployment" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<!-- Wrapped version to be used within subsystem -->
|
<!-- Wrapped version to be used within subsystem -->
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<!-- Authentication service for chaining -->
|
<!-- Authentication service for chaining -->
|
||||||
<bean id="localAuthenticationService" class="org.alfresco.repo.security.authentication.AuthenticationServiceImpl">
|
<bean id="localAuthenticationService" class="${project.artifactId}.authentication.KeycloakAuthenticationServiceImpl">
|
||||||
<property name="ticketComponent" ref="ticketComponent" />
|
<property name="ticketComponent" ref="ticketComponent" />
|
||||||
<property name="authenticationComponent" ref="authenticationComponent" />
|
<property name="authenticationComponent" ref="authenticationComponent" />
|
||||||
<property name="sysAdminParams" ref="sysAdminParams" />
|
<property name="sysAdminParams" ref="sysAdminParams" />
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
<property name="protectionLimit" value="${authentication.protection.limit}" />
|
<property name="protectionLimit" value="${authentication.protection.limit}" />
|
||||||
<property name="protectionPeriodSeconds" value="${authentication.protection.periodSeconds}" />
|
<property name="protectionPeriodSeconds" value="${authentication.protection.periodSeconds}" />
|
||||||
<property name="personService" ref="personService" />
|
<property name="personService" ref="personService" />
|
||||||
|
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean id="ftpAuthenticator" class="org.alfresco.filesys.auth.ftp.AlfrescoFtpAuthenticator" parent="ftpAuthenticatorBase">
|
<bean id="ftpAuthenticator" class="org.alfresco.filesys.auth.ftp.AlfrescoFtpAuthenticator" parent="ftpAuthenticatorBase">
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
|
|
||||||
<bean id="remoteUserMapper" class="${project.artifactId}.authentication.KeycloakRemoteUserMapper">
|
<bean id="remoteUserMapper" class="${project.artifactId}.authentication.KeycloakRemoteUserMapper">
|
||||||
<property name="active" value="${keycloak.authentication.enabled}" />
|
<property name="active" value="${keycloak.authentication.enabled}" />
|
||||||
<property name="validationFailureSilent" value="${keycloak.authentication.silentValidationFailure}" />
|
<property name="validationFailureSilent" value="${keycloak.authentication.silentRemoteUserValidationFailure}" />
|
||||||
<property name="keycloakDeployment" ref="keycloakDeployment" />
|
<property name="keycloakDeployment" ref="keycloakDeployment" />
|
||||||
<property name="personService" ref="PersonService" />
|
<property name="personService" ref="PersonService" />
|
||||||
</bean>
|
</bean>
|
||||||
@@ -133,5 +135,77 @@
|
|||||||
<property name="nodeService" ref="NodeService" />
|
<property name="nodeService" ref="NodeService" />
|
||||||
<property name="transactionService" ref="TransactionService" />
|
<property name="transactionService" ref="TransactionService" />
|
||||||
<property name="remoteUserMapper" ref="RemoteUserMapper" />
|
<property name="remoteUserMapper" ref="RemoteUserMapper" />
|
||||||
|
|
||||||
|
<property name="keycloakAuthenticationComponent" ref="authenticationComponent" />
|
||||||
|
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="idmClient" class="${project.artifactId}.client.IDMClientImpl">
|
||||||
|
<property name="deployment" ref="keycloakDeployment" />
|
||||||
|
<property name="userName" value="${keycloak.synchronization.user}" />
|
||||||
|
<property name="password" value="${keycloak.synchronization.password}" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="userRegistry" class="${project.artifactId}.sync.KeycloakUserRegistry">
|
||||||
|
<property name="active" value="${keycloak.synchronization.enabled}" />
|
||||||
|
<property name="idmClient" ref="idmClient" />
|
||||||
|
<property name="personLoadBatchSize" value="${keycloak.synchronization.personLoadBatchSize}" />
|
||||||
|
<property name="groupLoadBatchSize" value="${keycloak.synchronization.groupLoadBatchSize}" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="userAuthority.default" class="${project.artifactId}.authentication.DefaultAuthorityExtractor">
|
||||||
|
<property name="adapterConfig" ref="keycloakAdapterConfig" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="userToken.default" class="${project.artifactId}.authentication.DefaultPersonProcessor" />
|
||||||
|
|
||||||
|
<bean id="userFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentUserFilter">
|
||||||
|
<property name="idmClient" ref="idmClient" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="groupFilter.containedInGroup" class="${project.artifactId}.sync.GroupContainmentGroupFilter">
|
||||||
|
<property name="idmClient" ref="idmClient" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="authorityMapper.simpleAttributes" abstract="true">
|
||||||
|
<property name="namespaceService" ref="namespaceService" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="userMapper.default" class="${project.artifactId}.sync.DefaultPersonProcessor" />
|
||||||
|
|
||||||
|
<bean id="groupMapper.default" class="${project.artifactId}.sync.DefaultGroupProcessor" />
|
||||||
|
|
||||||
|
<bean id="userMapper.simpleAttributes" parent="authorityMapper.simpleAttributes"
|
||||||
|
class="${project.artifactId}.sync.SimpleUserAttributeProcessor" />
|
||||||
|
|
||||||
|
<bean id="groupMapper.simpleAttributes" parent="authorityMapper.simpleAttributes"
|
||||||
|
class="${project.artifactId}.sync.SimpleGroupAttributeProcessor" />
|
||||||
|
|
||||||
|
<bean id="${moduleId}-dynamicAuthenticationComponentsEmitter"
|
||||||
|
class="de.acosix.alfresco.utility.common.spring.BeanDefinitionFromPropertiesPostProcessor">
|
||||||
|
<property name="enabled" value="true" />
|
||||||
|
<property name="propertyPrefix" value="keycloak.authentication" />
|
||||||
|
<property name="beanTypes">
|
||||||
|
<list>
|
||||||
|
<value>userAuthority</value>
|
||||||
|
<value>userToken</value>
|
||||||
|
</list>
|
||||||
|
</property>
|
||||||
|
<property name="propertiesSource" ref="subsystem-properties" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
|
<bean id="${moduleId}-dynamicSynchronisationComponentsEmitter"
|
||||||
|
class="de.acosix.alfresco.utility.common.spring.BeanDefinitionFromPropertiesPostProcessor">
|
||||||
|
<property name="enabled" value="true" />
|
||||||
|
<property name="propertyPrefix" value="keycloak.synchronization" />
|
||||||
|
<property name="beanTypes">
|
||||||
|
<list>
|
||||||
|
<value>userFilter</value>
|
||||||
|
<value>userMapper</value>
|
||||||
|
<value>groupFilter</value>
|
||||||
|
<value>groupMapper</value>
|
||||||
|
</list>
|
||||||
|
</property>
|
||||||
|
<property name="propertiesSource" ref="subsystem-properties" />
|
||||||
</bean>
|
</bean>
|
||||||
</beans>
|
</beans>
|
@@ -4,9 +4,12 @@ keycloak.authentication.defaultAdministratorUserNames=
|
|||||||
keycloak.authentication.allowTicketLogons=true
|
keycloak.authentication.allowTicketLogons=true
|
||||||
keycloak.authentication.allowLocalBasicLogon=true
|
keycloak.authentication.allowLocalBasicLogon=true
|
||||||
keycloak.authentication.allowUserNamePasswordLogin=true
|
keycloak.authentication.allowUserNamePasswordLogin=true
|
||||||
|
keycloak.authentication.failExpiredUserNamePasswordLoginTokens=false
|
||||||
keycloak.authentication.allowGuestLogin=true
|
keycloak.authentication.allowGuestLogin=true
|
||||||
|
keycloak.authentication.mapRoles=true
|
||||||
|
keycloak.authentication.mapPersonPropertiesOnLogin=true
|
||||||
keycloak.authentication.authenticateFTP=true
|
keycloak.authentication.authenticateFTP=true
|
||||||
keycloak.authentication.silentValidationFailure=true
|
keycloak.authentication.silentRemoteUserValidationFailure=true
|
||||||
|
|
||||||
keycloak.authentication.connectionTimeout=-1
|
keycloak.authentication.connectionTimeout=-1
|
||||||
keycloak.authentication.socketTimeout=-1
|
keycloak.authentication.socketTimeout=-1
|
||||||
@@ -22,4 +25,75 @@ keycloak.adapter.public-client=false
|
|||||||
keycloak.adapter.credentials.provider=secret
|
keycloak.adapter.credentials.provider=secret
|
||||||
keycloak.adapter.credentials.secret=
|
keycloak.adapter.credentials.secret=
|
||||||
|
|
||||||
# TODO default settings (identical to AdapterConfig defaults) to better align with default Alfresco subsystem property handling
|
# TODO default settings (identical to AdapterConfig defaults) to better align with default Alfresco subsystem property handling
|
||||||
|
|
||||||
|
keycloak.authentication.userAuthority.default.property.enabled=true
|
||||||
|
keycloak.authentication.userAuthority.default.property.processRealmAccess=false
|
||||||
|
keycloak.authentication.userAuthority.default.property.processResourceAccess=true
|
||||||
|
keycloak.authentication.userAuthority.default.property.realmAccessAuthorityType=ROLE
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessAuthorityType=ROLE
|
||||||
|
keycloak.authentication.userAuthority.default.property.realmAccessAuthorityNamePrefix=KEYCLOAK
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessAuthorityNamePrefix=KEYCLOAK_${keycloak.adapter.resource}
|
||||||
|
keycloak.authentication.userAuthority.default.property.applyRealmAccessAuthorityCapitalisation=true
|
||||||
|
keycloak.authentication.userAuthority.default.property.applyResourceAccessAuthorityCapitalisation=true
|
||||||
|
keycloak.authentication.userAuthority.default.property.realmAccessAuthorityCapitalisationLocale=
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessAuthorityCapitalisationLocale=
|
||||||
|
|
||||||
|
# user is a default realm role
|
||||||
|
keycloak.authentication.userAuthority.default.property.realmAccessExplicitMappings.map.user=ROLE_KEYCLOAK_USER
|
||||||
|
|
||||||
|
# default role mappings for common roles that might be created for an Alfresco client in Keycloak
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessExplicitMappings.map.admin=ROLE_ADMINISTRATOR
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessExplicitMappings.map.guest=ROLE_GUEST
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessExplicitMappings.map.model-admin=GROUP_MODEL_ADMINISTRATORS
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessExplicitMappings.map.search-admin=GROUP_SEARCH_ADMINISTRATORS
|
||||||
|
keycloak.authentication.userAuthority.default.property.resourceAccessExplicitMappings.map.site-admin=GROUP_SITE_ADMINISTRATORS
|
||||||
|
|
||||||
|
keycloak.authentication.userToken.default.property.enabled=true
|
||||||
|
keycloak.authentication.userToken.default.property.mapNull=true
|
||||||
|
keycloak.authentication.userToken.default.property.mapGivenName=true
|
||||||
|
keycloak.authentication.userToken.default.property.mapFamilyName=true
|
||||||
|
keycloak.authentication.userToken.default.property.mapEmail=true
|
||||||
|
keycloak.authentication.userToken.default.property.mapPhoneNumber=true
|
||||||
|
keycloak.authentication.userToken.default.property.mapPhoneNumberAsMobile=false
|
||||||
|
|
||||||
|
keycloak.synchronization.enabled=true
|
||||||
|
keycloak.synchronization.user=
|
||||||
|
keycloak.synchronization.password=
|
||||||
|
keycloak.synchronization.personLoadBatchSize=50
|
||||||
|
keycloak.synchronization.groupLoadBatchSize=50
|
||||||
|
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.groupIds=
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.requireAll=false
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.allowTransitive=true
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.groupLoadBatchSize=${keycloak.synchronization.groupLoadBatchSize}
|
||||||
|
|
||||||
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=
|
||||||
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupIds=
|
||||||
|
keycloak.synchronization.groupFilter.containedInGroup.property.requireAll=false
|
||||||
|
keycloak.synchronization.groupFilter.containedInGroup.property.allowTransitive=true
|
||||||
|
|
||||||
|
keycloak.synchronization.userMapper.default.property.enabled=true
|
||||||
|
keycloak.synchronization.userMapper.default.property.mapNull=true
|
||||||
|
keycloak.synchronization.userMapper.default.property.mapFirstName=true
|
||||||
|
keycloak.synchronization.userMapper.default.property.mapLastName=true
|
||||||
|
keycloak.synchronization.userMapper.default.property.mapEmail=true
|
||||||
|
keycloak.synchronization.userMapper.default.property.mapEnabledState=true
|
||||||
|
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.enabled=true
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.middleName=cm:middleName
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.organization=cm:organization
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.jobTitle=cm:jobtitle
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.location=cm:location
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.telephone=cm:telephone
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.mobile=cm:mobile
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyAddress1=cm:companyaddress1
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyAddress2=cm:companyaddress2
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyAddress3=cm:companyaddress3
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyPostCode=cm:companypostcode
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyTelephone=cm:companytelephone
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyFax=cm:companyfax
|
||||||
|
keycloak.synchronization.userMapper.simpleAttributes.property.attributePropertyMappings.map.companyEmail=cm:companyemail
|
||||||
|
|
||||||
|
keycloak.synchronization.groupMapper.simpleAttributes.property.enabled=true
|
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityService;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityType;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface are used to map / extract authorities for an authenticated user from Keycloak authenticated users for use as
|
||||||
|
* {@link AuthorityService#getAuthorities() authorities of the current user}. Any mapped / extracted authority will be considered to be an
|
||||||
|
* authority of the user without the need of any explicit authority membership in the node structures of Alfresco - such authorities are
|
||||||
|
* typically used as global roles.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface AuthorityExtractor
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps / extracts authorities from a Keycloak access token.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the Keycloak access token for the authenticated user
|
||||||
|
* @return the mapped / extracted authorities - never {@code null} and authorities must already include the appropriate prefix for the
|
||||||
|
* {@link AuthorityType authority type} as which they should be treated
|
||||||
|
*/
|
||||||
|
Set<String> extractAuthorities(AccessToken accessToken);
|
||||||
|
}
|
@@ -0,0 +1,404 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityType;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken.Access;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class provide a generalised default authority mapping / extraction logic for Keycloak authenticated users. The mapping
|
||||||
|
* / extraction processes both realm and client- / resource-specific roles, and provides configurable authority name transformation (e.g.
|
||||||
|
* consistent casing, authority type prefixes, potential subsystem prefixes for differentiation).
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class DefaultAuthorityExtractor implements InitializingBean, AuthorityExtractor
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAuthorityExtractor.class);
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
protected boolean processRealmAccess;
|
||||||
|
|
||||||
|
protected boolean processResourceAccess;
|
||||||
|
|
||||||
|
protected Map<String, String> realmAccessExplicitMappings;
|
||||||
|
|
||||||
|
protected Map<String, String> resourceAccessExplicitMappings;
|
||||||
|
|
||||||
|
protected AuthorityType realmAccessAuthorityType;
|
||||||
|
|
||||||
|
protected AuthorityType resourceAccessAuthorityType;
|
||||||
|
|
||||||
|
protected String realmAccessAuthorityNamePrefix;
|
||||||
|
|
||||||
|
protected String resourceAccessAuthorityNamePrefix;
|
||||||
|
|
||||||
|
protected boolean applyRealmAccessAuthorityCapitalisation;
|
||||||
|
|
||||||
|
protected boolean applyResourceAccessAuthorityCapitalisation;
|
||||||
|
|
||||||
|
protected String realmAccessAuthorityCapitalisationLocale;
|
||||||
|
|
||||||
|
protected String resourceAccessAuthorityCapitalisationLocale;
|
||||||
|
|
||||||
|
protected Locale effectiveRealmAccessAuthorityCapitalisationLocale;
|
||||||
|
|
||||||
|
protected Locale effectiveResourceAccessAuthorityCapitalisationLocale;
|
||||||
|
|
||||||
|
protected AdapterConfig adapterConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "realmAccessAuthorityType", this.realmAccessAuthorityType);
|
||||||
|
PropertyCheck.mandatory(this, "resourceAccessAuthorityType", this.resourceAccessAuthorityType);
|
||||||
|
PropertyCheck.mandatory(this, "realmAccessAuthorityNamePrefix", this.realmAccessAuthorityNamePrefix);
|
||||||
|
PropertyCheck.mandatory(this, "resourceAccessAuthorityNamePrefix", this.resourceAccessAuthorityNamePrefix);
|
||||||
|
PropertyCheck.mandatory(this, "adapterConfig", this.adapterConfig);
|
||||||
|
|
||||||
|
final Set<AuthorityType> allowedTypes = EnumSet.of(AuthorityType.ROLE, AuthorityType.GROUP);
|
||||||
|
if (!allowedTypes.contains(this.realmAccessAuthorityType))
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Only ROLE and GROUP authority types are allowed for realmAccessAuthorityType");
|
||||||
|
}
|
||||||
|
if (!allowedTypes.contains(this.resourceAccessAuthorityType))
|
||||||
|
{
|
||||||
|
throw new IllegalStateException("Only ROLE and GROUP authority types are allowed for resourceAccessAuthorityType");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Function<String, Locale> localeConversion = capitalisationLocale -> {
|
||||||
|
final Locale locale;
|
||||||
|
if (capitalisationLocale != null)
|
||||||
|
{
|
||||||
|
final String[] localeFragments = capitalisationLocale.split("[_\\-]");
|
||||||
|
if (localeFragments.length >= 3)
|
||||||
|
{
|
||||||
|
locale = new Locale(localeFragments[0], localeFragments[1], localeFragments[2]);
|
||||||
|
}
|
||||||
|
else if (localeFragments.length >= 2)
|
||||||
|
{
|
||||||
|
locale = new Locale(localeFragments[0], localeFragments[1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
locale = new Locale(localeFragments[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
locale = Locale.getDefault();
|
||||||
|
}
|
||||||
|
return locale;
|
||||||
|
};
|
||||||
|
this.effectiveRealmAccessAuthorityCapitalisationLocale = localeConversion.apply(this.realmAccessAuthorityCapitalisationLocale);
|
||||||
|
this.effectiveResourceAccessAuthorityCapitalisationLocale = localeConversion
|
||||||
|
.apply(this.resourceAccessAuthorityCapitalisationLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param processRealmAccess
|
||||||
|
* the processRealmAccess to set
|
||||||
|
*/
|
||||||
|
public void setProcessRealmAccess(final boolean processRealmAccess)
|
||||||
|
{
|
||||||
|
this.processRealmAccess = processRealmAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param processResourceAccess
|
||||||
|
* the processResourceAccess to set
|
||||||
|
*/
|
||||||
|
public void setProcessResourceAccess(final boolean processResourceAccess)
|
||||||
|
{
|
||||||
|
this.processResourceAccess = processResourceAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realmAccessExplicitMappings
|
||||||
|
* the realmAccessExplicitMappings to set
|
||||||
|
*/
|
||||||
|
public void setRealmAccessExplicitMappings(final Map<String, String> realmAccessExplicitMappings)
|
||||||
|
{
|
||||||
|
this.realmAccessExplicitMappings = realmAccessExplicitMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resourceAccessExplicitMappings
|
||||||
|
* the resourceAccessExplicitMappings to set
|
||||||
|
*/
|
||||||
|
public void setResourceAccessExplicitMappings(final Map<String, String> resourceAccessExplicitMappings)
|
||||||
|
{
|
||||||
|
this.resourceAccessExplicitMappings = resourceAccessExplicitMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realmAccessAuthorityType
|
||||||
|
* the realmAccessAuthorityType to set
|
||||||
|
*/
|
||||||
|
public void setRealmAccessAuthorityType(final AuthorityType realmAccessAuthorityType)
|
||||||
|
{
|
||||||
|
this.realmAccessAuthorityType = realmAccessAuthorityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resourceAccessAuthorityType
|
||||||
|
* the resourceAccessAuthorityType to set
|
||||||
|
*/
|
||||||
|
public void setResourceAccessAuthorityType(final AuthorityType resourceAccessAuthorityType)
|
||||||
|
{
|
||||||
|
this.resourceAccessAuthorityType = resourceAccessAuthorityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realmAccessAuthorityNamePrefix
|
||||||
|
* the realmAccessAuthorityNamePrefix to set
|
||||||
|
*/
|
||||||
|
public void setRealmAccessAuthorityNamePrefix(final String realmAccessAuthorityNamePrefix)
|
||||||
|
{
|
||||||
|
this.realmAccessAuthorityNamePrefix = realmAccessAuthorityNamePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resourceAccessAuthorityNamePrefix
|
||||||
|
* the resourceAccessAuthorityNamePrefix to set
|
||||||
|
*/
|
||||||
|
public void setResourceAccessAuthorityNamePrefix(final String resourceAccessAuthorityNamePrefix)
|
||||||
|
{
|
||||||
|
this.resourceAccessAuthorityNamePrefix = resourceAccessAuthorityNamePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param applyRealmAccessAuthorityCapitalisation
|
||||||
|
* the applyRealmAccessAuthorityCapitalisation to set
|
||||||
|
*/
|
||||||
|
public void setApplyRealmAccessAuthorityCapitalisation(final boolean applyRealmAccessAuthorityCapitalisation)
|
||||||
|
{
|
||||||
|
this.applyRealmAccessAuthorityCapitalisation = applyRealmAccessAuthorityCapitalisation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param applyResourceAccessAuthorityCapitalisation
|
||||||
|
* the applyResourceAccessAuthorityCapitalisation to set
|
||||||
|
*/
|
||||||
|
public void setApplyResourceAccessAuthorityCapitalisation(final boolean applyResourceAccessAuthorityCapitalisation)
|
||||||
|
{
|
||||||
|
this.applyResourceAccessAuthorityCapitalisation = applyResourceAccessAuthorityCapitalisation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realmAccessAuthorityCapitalisationLocale
|
||||||
|
* the realmAccessAuthorityCapitalisationLocale to set
|
||||||
|
*/
|
||||||
|
public void setRealmAccessAuthorityCapitalisationLocale(final String realmAccessAuthorityCapitalisationLocale)
|
||||||
|
{
|
||||||
|
this.realmAccessAuthorityCapitalisationLocale = realmAccessAuthorityCapitalisationLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resourceAccessAuthorityCapitalisationLocale
|
||||||
|
* the resourceAccessAuthorityCapitalisationLocale to set
|
||||||
|
*/
|
||||||
|
public void setResourceAccessAuthorityCapitalisationLocale(final String resourceAccessAuthorityCapitalisationLocale)
|
||||||
|
{
|
||||||
|
this.resourceAccessAuthorityCapitalisationLocale = resourceAccessAuthorityCapitalisationLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param adapterConfig
|
||||||
|
* the adapterConfig to set
|
||||||
|
*/
|
||||||
|
public void setAdapterConfig(final AdapterConfig adapterConfig)
|
||||||
|
{
|
||||||
|
this.adapterConfig = adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Set<String> extractAuthorities(final AccessToken accessToken)
|
||||||
|
{
|
||||||
|
Set<String> authorities = Collections.emptySet();
|
||||||
|
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
if (this.processRealmAccess || this.processResourceAccess)
|
||||||
|
{
|
||||||
|
authorities = new HashSet<>();
|
||||||
|
|
||||||
|
if (this.processRealmAccess)
|
||||||
|
{
|
||||||
|
final Access realmAccess = accessToken.getRealmAccess();
|
||||||
|
if (realmAccess != null)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping authorities from realm access");
|
||||||
|
|
||||||
|
final Set<String> realmAuthorites = this.processAccess(realmAccess, this.realmAccessExplicitMappings,
|
||||||
|
this.realmAccessAuthorityType, this.realmAccessAuthorityNamePrefix,
|
||||||
|
this.applyRealmAccessAuthorityCapitalisation, this.effectiveRealmAccessAuthorityCapitalisationLocale);
|
||||||
|
|
||||||
|
LOGGER.debug("Mapped authorities from realm access: {}", realmAuthorites);
|
||||||
|
|
||||||
|
authorities.addAll(realmAuthorites);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("No realm access provided in access token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping authorities from realm access is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.processResourceAccess)
|
||||||
|
{
|
||||||
|
final String resource = this.adapterConfig.getResource();
|
||||||
|
final Access resourceAccess = accessToken.getResourceAccess(resource);
|
||||||
|
if (resourceAccess != null)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping authorities from resource access on {}", resource);
|
||||||
|
|
||||||
|
final Set<String> resourceAuthorites = this.processAccess(resourceAccess, this.resourceAccessExplicitMappings,
|
||||||
|
this.resourceAccessAuthorityType, this.resourceAccessAuthorityNamePrefix,
|
||||||
|
this.applyResourceAccessAuthorityCapitalisation, this.effectiveResourceAccessAuthorityCapitalisationLocale);
|
||||||
|
|
||||||
|
LOGGER.debug("Mapped authorities from resource access on {}: {}", resource, resourceAuthorites);
|
||||||
|
|
||||||
|
authorities.addAll(resourceAuthorites);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("No resource access for {} provided in access token", resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping authorities from resource access is not enabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping authorities is not enabled for either realm or resource access");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping authorities from access token is not enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps / extracts authorities from a Keycloak access representation.
|
||||||
|
*
|
||||||
|
* @param access
|
||||||
|
* the access representation component of an access token
|
||||||
|
* @param explicitMappings
|
||||||
|
* the explicit mappings of roles to authorities to consider - an explicit mapping for a role overrides the default mapping
|
||||||
|
* handling for that role only
|
||||||
|
* @param authorityType
|
||||||
|
* the authority type to use for mapped / extracted authorities
|
||||||
|
* @param prefix
|
||||||
|
* the static authority name prefix to use for mapped / extracted authorities
|
||||||
|
* @param capitalisation
|
||||||
|
* {@code true} if authorities should be standardised on fully capitalised names, {@code false} if names should be left as
|
||||||
|
* mapped from the access representation
|
||||||
|
* @param capitalisationLocale
|
||||||
|
* the locale to use when capitalising authority names
|
||||||
|
* @return the authorities mapped / extracted from the access representation
|
||||||
|
*/
|
||||||
|
protected Set<String> processAccess(final Access access, final Map<String, String> explicitMappings, final AuthorityType authorityType,
|
||||||
|
final String prefix, final boolean capitalisation, final Locale capitalisationLocale)
|
||||||
|
{
|
||||||
|
final Set<String> authorities;
|
||||||
|
|
||||||
|
final Set<String> roles = access.getRoles();
|
||||||
|
if (roles != null && !roles.isEmpty())
|
||||||
|
{
|
||||||
|
LOGGER.debug("Access representation contains roles {}", roles);
|
||||||
|
|
||||||
|
Stream<String> rolesStream = roles.stream();
|
||||||
|
if (explicitMappings != null && !explicitMappings.isEmpty())
|
||||||
|
{
|
||||||
|
LOGGER.debug("Explicit mappings for roles have been provided");
|
||||||
|
rolesStream = rolesStream.filter(r -> !explicitMappings.containsKey(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix != null && !prefix.isEmpty())
|
||||||
|
{
|
||||||
|
rolesStream = rolesStream.map(r -> prefix + "_" + r);
|
||||||
|
}
|
||||||
|
rolesStream = rolesStream.map(r -> authorityType.getPrefixString() + r);
|
||||||
|
if (capitalisation)
|
||||||
|
{
|
||||||
|
|
||||||
|
rolesStream = rolesStream.map(r -> r.toUpperCase(capitalisationLocale));
|
||||||
|
}
|
||||||
|
authorities = rolesStream.collect(Collectors.toSet());
|
||||||
|
LOGGER.debug("Generically mapped authorities: {}", authorities);
|
||||||
|
|
||||||
|
if (explicitMappings != null && !explicitMappings.isEmpty())
|
||||||
|
{
|
||||||
|
final Set<String> explicitlyMappedAuthorities = roles.stream().filter(explicitMappings::containsKey)
|
||||||
|
.map(explicitMappings::get).collect(Collectors.toSet());
|
||||||
|
LOGGER.debug("Explicitly mapped authorities: {}", explicitlyMappedAuthorities);
|
||||||
|
authorities.addAll(explicitlyMappedAuthorities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.debug("Access representation contains no roles");
|
||||||
|
|
||||||
|
authorities = Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorities;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.IDToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This user authentication mapping processor maps the default Alfresco person properties from an authenticated Keycloak user.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class DefaultPersonProcessor implements UserProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
protected boolean mapNull;
|
||||||
|
|
||||||
|
protected boolean mapGivenName;
|
||||||
|
|
||||||
|
protected boolean mapFamilyName;
|
||||||
|
|
||||||
|
protected boolean mapEmail;
|
||||||
|
|
||||||
|
protected boolean mapPhoneNumber;
|
||||||
|
|
||||||
|
protected boolean mapPhoneNumberAsMobile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapNull
|
||||||
|
* the mapNull to set
|
||||||
|
*/
|
||||||
|
public void setMapNull(final boolean mapNull)
|
||||||
|
{
|
||||||
|
this.mapNull = mapNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapGivenName
|
||||||
|
* the mapGivenName to set
|
||||||
|
*/
|
||||||
|
public void setMapGivenName(final boolean mapGivenName)
|
||||||
|
{
|
||||||
|
this.mapGivenName = mapGivenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapFamilyName
|
||||||
|
* the mapFamilyName to set
|
||||||
|
*/
|
||||||
|
public void setMapFamilyName(final boolean mapFamilyName)
|
||||||
|
{
|
||||||
|
this.mapFamilyName = mapFamilyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapEmail
|
||||||
|
* the mapEmail to set
|
||||||
|
*/
|
||||||
|
public void setMapEmail(final boolean mapEmail)
|
||||||
|
{
|
||||||
|
this.mapEmail = mapEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapPhoneNumber
|
||||||
|
* the mapPhoneNumber to set
|
||||||
|
*/
|
||||||
|
public void setMapPhoneNumber(final boolean mapPhoneNumber)
|
||||||
|
{
|
||||||
|
this.mapPhoneNumber = mapPhoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapPhoneNumberAsMobile
|
||||||
|
* the mapPhoneNumberAsMobile to set
|
||||||
|
*/
|
||||||
|
public void setMapPhoneNumberAsMobile(final boolean mapPhoneNumberAsMobile)
|
||||||
|
{
|
||||||
|
this.mapPhoneNumberAsMobile = mapPhoneNumberAsMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void mapUser(final AccessToken accessToken, final IDToken idToken, final Map<QName, Serializable> personNodeProperties)
|
||||||
|
{
|
||||||
|
if (this.enabled && idToken != null)
|
||||||
|
{
|
||||||
|
if ((this.mapNull || idToken.getGivenName() != null) && this.mapGivenName)
|
||||||
|
{
|
||||||
|
personNodeProperties.put(ContentModel.PROP_FIRSTNAME, idToken.getGivenName());
|
||||||
|
}
|
||||||
|
if ((this.mapNull || idToken.getFamilyName() != null) && this.mapFamilyName)
|
||||||
|
{
|
||||||
|
personNodeProperties.put(ContentModel.PROP_LASTNAME, idToken.getFamilyName());
|
||||||
|
}
|
||||||
|
if ((this.mapNull || idToken.getEmail() != null) && this.mapEmail)
|
||||||
|
{
|
||||||
|
personNodeProperties.put(ContentModel.PROP_EMAIL, idToken.getEmail());
|
||||||
|
}
|
||||||
|
if ((this.mapNull || idToken.getPhoneNumber() != null) && this.mapPhoneNumber)
|
||||||
|
{
|
||||||
|
personNodeProperties.put(this.mapPhoneNumberAsMobile ? ContentModel.PROP_MOBILE : ContentModel.PROP_TELEPHONE,
|
||||||
|
idToken.getPhoneNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -15,46 +15,95 @@
|
|||||||
*/
|
*/
|
||||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
||||||
|
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
|
||||||
|
import org.alfresco.service.cmr.repository.NodeRef;
|
||||||
|
import org.alfresco.service.cmr.repository.NodeService;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
import org.keycloak.adapters.HttpClientBuilder;
|
import org.apache.http.HttpEntity;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.apache.http.HttpResponse;
|
||||||
import org.keycloak.authorization.client.Configuration;
|
import org.apache.http.NameValuePair;
|
||||||
import org.keycloak.authorization.client.util.HttpResponseException;
|
import org.apache.http.client.HttpClient;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.OAuth2Constants;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.ServerRequest;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.rotation.AdapterTokenVerifier;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.VerificationException;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.constants.ServiceUrlConstants;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessTokenResponse;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.IDToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.util.JsonSerialization;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
import net.sf.acegisecurity.Authentication;
|
||||||
|
import net.sf.acegisecurity.GrantedAuthority;
|
||||||
|
import net.sf.acegisecurity.GrantedAuthorityImpl;
|
||||||
|
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
public class KeycloakAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean, InitializingBean
|
public class KeycloakAuthenticationComponent extends AbstractAuthenticationComponent
|
||||||
|
implements InitializingBean, ActivateableBean, ApplicationContextAware
|
||||||
{
|
{
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationComponent.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationComponent.class);
|
||||||
|
|
||||||
|
protected final ThreadLocal<Boolean> lastTokenResponseStoreEnabled = new ThreadLocal<>();
|
||||||
|
|
||||||
|
protected final ThreadLocal<RefreshableAccessTokenHolder> lastTokenResponse = new ThreadLocal<>();
|
||||||
|
|
||||||
protected boolean active;
|
protected boolean active;
|
||||||
|
|
||||||
|
protected ApplicationContext applicationContext;
|
||||||
|
|
||||||
protected boolean allowUserNamePasswordLogin;
|
protected boolean allowUserNamePasswordLogin;
|
||||||
|
|
||||||
|
protected boolean failExpiredTicketTokens;
|
||||||
|
|
||||||
protected boolean allowGuestLogin;
|
protected boolean allowGuestLogin;
|
||||||
|
|
||||||
protected AdapterConfig adapterConfig;
|
protected boolean mapRoles;
|
||||||
|
|
||||||
protected int connectionTimeout;
|
protected boolean mapPersonPropertiesOnLogin;
|
||||||
|
|
||||||
protected int socketTimeout;
|
protected KeycloakDeployment deployment;
|
||||||
|
|
||||||
protected Configuration config;
|
protected Collection<AuthorityExtractor> authorityExtractors;
|
||||||
|
|
||||||
protected AuthzClient authzClient;
|
protected Collection<UserProcessor> userProcessors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -63,54 +112,22 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet()
|
public void afterPropertiesSet()
|
||||||
{
|
{
|
||||||
PropertyCheck.mandatory(this, "adapterConfig", this.adapterConfig);
|
PropertyCheck.mandatory(this, "applicationContext", this.applicationContext);
|
||||||
|
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
|
||||||
|
|
||||||
if (this.allowUserNamePasswordLogin)
|
this.authorityExtractors = Collections
|
||||||
{
|
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(AuthorityExtractor.class, false, true).values()));
|
||||||
Map<String, Object> credentials = this.adapterConfig.getCredentials();
|
this.userProcessors = Collections
|
||||||
if (credentials != null)
|
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(UserProcessor.class, false, true).values()));
|
||||||
{
|
}
|
||||||
credentials = new HashMap<>(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (credentials == null || ((!credentials.containsKey("provider") || "secret".equals(credentials.get("provider")))
|
/**
|
||||||
&& !credentials.containsKey("secret")))
|
* {@inheritDoc}
|
||||||
{
|
*/
|
||||||
if (credentials == null)
|
@Override
|
||||||
{
|
public boolean isActive()
|
||||||
credentials = new HashMap<>();
|
{
|
||||||
}
|
return this.active;
|
||||||
credentials.put("secret", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
|
|
||||||
if (this.connectionTimeout > 0)
|
|
||||||
{
|
|
||||||
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(this.connectionTimeout, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
if (this.socketTimeout > 0)
|
|
||||||
{
|
|
||||||
httpClientBuilder = httpClientBuilder.socketTimeout(this.socketTimeout, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.config = new Configuration(this.adapterConfig.getAuthServerUrl(), this.adapterConfig.getRealm(),
|
|
||||||
this.adapterConfig.getResource(), credentials, httpClientBuilder.build(this.adapterConfig));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.authzClient = AuthzClient.create(this.config);
|
|
||||||
}
|
|
||||||
catch (final RuntimeException e)
|
|
||||||
{
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
{
|
|
||||||
LOGGER.debug("Failed to pre-instantiate Keycloak authz client", e);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOGGER.warn("Failed to pre-instantiate Keycloak authz client: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,6 +139,15 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
this.active = active;
|
this.active = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(final ApplicationContext applicationContext)
|
||||||
|
{
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param allowUserNamePasswordLogin
|
* @param allowUserNamePasswordLogin
|
||||||
* the allowUserNamePasswordLogin to set
|
* the allowUserNamePasswordLogin to set
|
||||||
@@ -131,6 +157,15 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
this.allowUserNamePasswordLogin = allowUserNamePasswordLogin;
|
this.allowUserNamePasswordLogin = allowUserNamePasswordLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param failExpiredTicketTokens
|
||||||
|
* the failExpiredTicketTokens to set
|
||||||
|
*/
|
||||||
|
public void setFailExpiredTicketTokens(final boolean failExpiredTicketTokens)
|
||||||
|
{
|
||||||
|
this.failExpiredTicketTokens = failExpiredTicketTokens;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param allowGuestLogin
|
* @param allowGuestLogin
|
||||||
* the allowGuestLogin to set
|
* the allowGuestLogin to set
|
||||||
@@ -138,42 +173,130 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
public void setAllowGuestLogin(final boolean allowGuestLogin)
|
public void setAllowGuestLogin(final boolean allowGuestLogin)
|
||||||
{
|
{
|
||||||
this.allowGuestLogin = allowGuestLogin;
|
this.allowGuestLogin = allowGuestLogin;
|
||||||
|
super.setAllowGuestLogin(Boolean.valueOf(allowGuestLogin));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param adapterConfig
|
* @param allowGuestLogin
|
||||||
* the adapterConfig to set
|
* the allowGuestLogin to set
|
||||||
*/
|
|
||||||
public void setAdapterConfig(final AdapterConfig adapterConfig)
|
|
||||||
{
|
|
||||||
this.adapterConfig = adapterConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param connectionTimeout
|
|
||||||
* the connectionTimeout to set
|
|
||||||
*/
|
|
||||||
public void setConnectionTimeout(final int connectionTimeout)
|
|
||||||
{
|
|
||||||
this.connectionTimeout = connectionTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param socketTimeout
|
|
||||||
* the socketTimeout to set
|
|
||||||
*/
|
|
||||||
public void setSocketTimeout(final int socketTimeout)
|
|
||||||
{
|
|
||||||
this.socketTimeout = socketTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean isActive()
|
public void setAllowGuestLogin(final Boolean allowGuestLogin)
|
||||||
{
|
{
|
||||||
return this.active;
|
this.setAllowGuestLogin(Boolean.TRUE.equals(allowGuestLogin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapRoles
|
||||||
|
* the mapRoles to set
|
||||||
|
*/
|
||||||
|
public void setMapRoles(final boolean mapRoles)
|
||||||
|
{
|
||||||
|
this.mapRoles = mapRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapPersonPropertiesOnLogin
|
||||||
|
* the mapPersonPropertiesOnLogin to set
|
||||||
|
*/
|
||||||
|
public void setMapPersonPropertiesOnLogin(final boolean mapPersonPropertiesOnLogin)
|
||||||
|
{
|
||||||
|
this.mapPersonPropertiesOnLogin = mapPersonPropertiesOnLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param deployment
|
||||||
|
* the deployment to set
|
||||||
|
*/
|
||||||
|
public void setDeployment(final KeycloakDeployment deployment)
|
||||||
|
{
|
||||||
|
this.deployment = deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the thread-local storage of the last access token response and verified tokens beyond the internal needs of
|
||||||
|
* {@link #authenticateImpl(String, char[]) authenticateImpl}.
|
||||||
|
*/
|
||||||
|
public void enableLastTokenStore()
|
||||||
|
{
|
||||||
|
this.lastTokenResponseStoreEnabled.set(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the thread-local storage of the last access token response and verified tokens beyond the internal needs of
|
||||||
|
* {@link #authenticateImpl(String, char[]) authenticateImpl}.
|
||||||
|
*/
|
||||||
|
public void disableLastTokenStore()
|
||||||
|
{
|
||||||
|
this.lastTokenResponseStoreEnabled.remove();
|
||||||
|
this.lastTokenResponse.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the last access token response kept in the thread-local storage. This will only return a result if the thread is currently
|
||||||
|
* in the process of {@link #authenticateImpl(String, char[]) authenticating a user} or {@link #enableLastTokenStore() storage of the
|
||||||
|
* last response is currently enabled}.
|
||||||
|
*
|
||||||
|
* @return the last token response or {@code null} if no response is stored in the thread local for the current thread
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder getLastTokenResponse()
|
||||||
|
{
|
||||||
|
return this.lastTokenResponse.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a refreshable access token associated with an authentication ticket, refreshing it if necessary, and failing if the token has
|
||||||
|
* expired and the component has been configured to not accept expired tokens.
|
||||||
|
*
|
||||||
|
* @param ticketToken
|
||||||
|
* the refreshable access token to refresh
|
||||||
|
* @return the refreshed access token if a refresh was possible AND necessary, and a new access token has been retrieved from Keycloak -
|
||||||
|
* will be {@code null} if no refresh has taken place
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder checkAndRefreshTicketToken(final RefreshableAccessTokenHolder ticketToken)
|
||||||
|
throws AuthenticationException
|
||||||
|
{
|
||||||
|
if (this.failExpiredTicketTokens && ticketToken.isExpired())
|
||||||
|
{
|
||||||
|
throw new AuthenticationException("Keycloak access token has expired - authentication ticket is no longer valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshableAccessTokenHolder result = null;
|
||||||
|
if (ticketToken.canRefresh() && ticketToken.shouldRefresh(this.deployment.getTokenMinimumTimeToLive()))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AccessTokenResponse response = ServerRequest.invokeRefresh(this.deployment, ticketToken.getRefreshToken());
|
||||||
|
final VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(response.getToken(), response.getIdToken(),
|
||||||
|
this.deployment);
|
||||||
|
|
||||||
|
result = new RefreshableAccessTokenHolder(response, tokens);
|
||||||
|
}
|
||||||
|
catch (final ServerRequest.HttpFailure httpFailure)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error refreshing Keycloak authentication - {} {}", httpFailure.getStatus(), httpFailure.getError());
|
||||||
|
throw new AuthenticationException(
|
||||||
|
"Failed to refresh Keycloak authentication: " + httpFailure.getStatus() + " " + httpFailure.getError());
|
||||||
|
}
|
||||||
|
catch (final VerificationException vex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error refreshing Keycloak authentication - access token verification failed", vex);
|
||||||
|
throw new AuthenticationException("Failed to refresh Keycloak authentication", vex);
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error refreshing Keycloak authentication - unexpected IO exception", ioex);
|
||||||
|
throw new AuthenticationException("Failed to refresh Keycloak authentication", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null || !ticketToken.isExpired())
|
||||||
|
{
|
||||||
|
this.handleUserTokens(result != null ? result.getAccessToken() : ticketToken.getAccessToken(),
|
||||||
|
result != null ? result.getIdToken() : ticketToken.getIdToken(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,28 +310,131 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
throw new AuthenticationException("Simple login via user name + password is not allowed");
|
throw new AuthenticationException("Simple login via user name + password is not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.authzClient == null)
|
final AccessTokenResponse response;
|
||||||
|
final VerifiedTokens tokens;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
response = this.getAccessTokenImpl(userName, new String(password));
|
||||||
|
tokens = AdapterTokenVerifier.verifyTokens(response.getToken(), response.getIdToken(), this.deployment);
|
||||||
|
|
||||||
|
// for potential one-off authentication, we do not care particularly about the token TTL - so no validation here
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(this.lastTokenResponseStoreEnabled.get()))
|
||||||
{
|
{
|
||||||
this.authzClient = AuthzClient.create(this.config);
|
this.lastTokenResponse.set(new RefreshableAccessTokenHolder(response, tokens));
|
||||||
}
|
}
|
||||||
catch (final RuntimeException e)
|
}
|
||||||
|
catch (final VerificationException vex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error authenticating against Keycloak - access token verification failed", vex);
|
||||||
|
throw new AuthenticationException("Failed to authenticate against Keycloak", vex);
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error authenticating against Keycloak - unexpected IO exception", ioex);
|
||||||
|
throw new AuthenticationException("Failed to authenticate against Keycloak", ioex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setCurrentUser(userName);
|
||||||
|
this.handleUserTokens(tokens.getAccessToken(), tokens.getIdToken(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes tokens for authenticated users, mapping them to Alfresco person properties or granted authorities as configured for this
|
||||||
|
* instance.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token
|
||||||
|
* @param idToken
|
||||||
|
* the ID token
|
||||||
|
* @param freshLogin
|
||||||
|
* {@code true} if the tokens are fresh, that is have just been obtained from an initial login, {@code false} otherwise -
|
||||||
|
* Alfresco person node properties will only be mapped for fresh tokens, while granted authorities processors will always be
|
||||||
|
* handled if enabled
|
||||||
|
*/
|
||||||
|
public void handleUserTokens(final AccessToken accessToken, final IDToken idToken, final boolean freshLogin)
|
||||||
|
{
|
||||||
|
if (this.mapRoles)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Mapping roles from Keycloak to user authorities");
|
||||||
|
|
||||||
|
final Set<String> mappedAuthorities = new HashSet<>();
|
||||||
|
this.authorityExtractors.stream().map(extractor -> extractor.extractAuthorities(accessToken))
|
||||||
|
.forEach(mappedAuthorities::addAll);
|
||||||
|
|
||||||
|
LOGGER.debug("Mapped user authorities from roles: {}", mappedAuthorities);
|
||||||
|
|
||||||
|
if (!mappedAuthorities.isEmpty())
|
||||||
{
|
{
|
||||||
LOGGER.warn("Failed to pre-instantiate Keycloak authz client", e);
|
final Authentication currentAuthentication = this.getCurrentAuthentication();
|
||||||
throw new AuthenticationException("Keycloak authentication cannot be performed", e);
|
if (currentAuthentication instanceof UsernamePasswordAuthenticationToken)
|
||||||
|
{
|
||||||
|
GrantedAuthority[] grantedAuthorities = currentAuthentication.getAuthorities();
|
||||||
|
final List<GrantedAuthority> grantedAuthoritiesL = new ArrayList<>(Arrays.asList(grantedAuthorities));
|
||||||
|
|
||||||
|
mappedAuthorities.stream().map(GrantedAuthorityImpl::new).forEach(grantedAuthoritiesL::add);
|
||||||
|
|
||||||
|
grantedAuthorities = grantedAuthoritiesL.toArray(new GrantedAuthority[0]);
|
||||||
|
((UsernamePasswordAuthenticationToken) currentAuthentication).setAuthorities(grantedAuthorities);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.warn(
|
||||||
|
"Authentication for user is not of the expected type {} - roles from Keycloak cannot be mapped to granted authorities",
|
||||||
|
UsernamePasswordAuthenticationToken.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (freshLogin && this.mapPersonPropertiesOnLogin)
|
||||||
{
|
{
|
||||||
this.authzClient.obtainAccessToken(userName, new String(password));
|
final boolean requiresNew = AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
|
||||||
this.setCurrentUser(userName);
|
this.getTransactionService().getRetryingTransactionHelper().doInTransaction(() -> {
|
||||||
|
this.updatePerson(accessToken, idToken);
|
||||||
|
return null;
|
||||||
|
}, false, requiresNew);
|
||||||
}
|
}
|
||||||
catch (final HttpResponseException e)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the person for the current user with data mapped from the Keycloak tokens.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token
|
||||||
|
* @param idToken
|
||||||
|
* the ID token
|
||||||
|
*/
|
||||||
|
protected void updatePerson(final AccessToken accessToken, final IDToken idToken)
|
||||||
|
{
|
||||||
|
final String userName = this.getCurrentUserName();
|
||||||
|
|
||||||
|
LOGGER.debug("Mapping person property updates for user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
||||||
|
|
||||||
|
final NodeRef person = this.getPersonService().getPerson(userName);
|
||||||
|
|
||||||
|
final Map<QName, Serializable> updates = new HashMap<>();
|
||||||
|
this.userProcessors.forEach(processor -> processor.mapUser(accessToken, idToken != null ? idToken : accessToken, updates));
|
||||||
|
|
||||||
|
LOGGER.debug("Determined property updates for person node of user {}", AlfrescoCompatibilityUtil.maskUsername(userName));
|
||||||
|
|
||||||
|
final Set<QName> propertiesToRemove = updates.keySet().stream().filter(k -> updates.get(k) == null).collect(Collectors.toSet());
|
||||||
|
updates.keySet().removeAll(propertiesToRemove);
|
||||||
|
|
||||||
|
final NodeService nodeService = this.getNodeService();
|
||||||
|
final Map<QName, Serializable> currentProperties = nodeService.getProperties(person);
|
||||||
|
|
||||||
|
propertiesToRemove.retainAll(currentProperties.keySet());
|
||||||
|
if (!propertiesToRemove.isEmpty())
|
||||||
{
|
{
|
||||||
LOGGER.debug("Failed to authenticate user against Keycloak. Status: {} Reason: {}", e.getStatusCode(), e.getReasonPhrase());
|
// there is no bulk-remove, so we need to use setProperties to achieve a single update event
|
||||||
throw new AuthenticationException("Failed to authenticate user against Keycloak.", e);
|
final Map<QName, Serializable> newProperties = new HashMap<>(currentProperties);
|
||||||
|
newProperties.putAll(updates);
|
||||||
|
newProperties.keySet().removeAll(propertiesToRemove);
|
||||||
|
nodeService.setProperties(person, newProperties);
|
||||||
|
}
|
||||||
|
else if (!updates.isEmpty())
|
||||||
|
{
|
||||||
|
nodeService.addProperties(person, updates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,4 +446,75 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
{
|
{
|
||||||
return this.allowGuestLogin;
|
return this.allowGuestLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer.
|
||||||
|
*
|
||||||
|
* @param userName
|
||||||
|
* the user to use for synchronisation access
|
||||||
|
* @param password
|
||||||
|
* the password of the user
|
||||||
|
* @return the access token
|
||||||
|
* @throws IOException
|
||||||
|
* when errors occur in the HTTP interaction
|
||||||
|
*/
|
||||||
|
// implementing this method locally avoids having the dependency on Keycloak authz-client
|
||||||
|
// authz-client does not support refresh, so would be of limited value anyway
|
||||||
|
protected AccessTokenResponse getAccessTokenImpl(final String userName, final String password) throws IOException
|
||||||
|
{
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
final HttpClient client = this.deployment.getClient();
|
||||||
|
|
||||||
|
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
||||||
|
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
|
||||||
|
final List<NameValuePair> formParams = new ArrayList<>();
|
||||||
|
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
||||||
|
formParams.add(new BasicNameValuePair("username", userName));
|
||||||
|
formParams.add(new BasicNameValuePair("password", password));
|
||||||
|
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(this.deployment, post, formParams);
|
||||||
|
|
||||||
|
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
|
||||||
|
final HttpResponse response = client.execute(post);
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity entity = response.getEntity();
|
||||||
|
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
final String statusReason = response.getStatusLine().getReasonPhrase();
|
||||||
|
LOGGER.debug("Failed to retrieve access token due to HTTP {}: {}", status, statusReason);
|
||||||
|
EntityUtils.consumeQuietly(entity);
|
||||||
|
|
||||||
|
LOGGER.debug("Failed to authenticate user against Keycloak. Status: {} Reason: {}", status, statusReason);
|
||||||
|
throw new AuthenticationException("Failed to authenticate against Keycloak - Status: " + status + ", Reason: " + statusReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Failed to authenticate against Keycloak - Response did not contain a message body");
|
||||||
|
throw new AuthenticationException("Failed to authenticate against Keycloak - Response did not contain a message body");
|
||||||
|
}
|
||||||
|
|
||||||
|
final InputStream is = entity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -33,9 +33,9 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
import org.alfresco.repo.SessionUser;
|
import org.alfresco.repo.SessionUser;
|
||||||
|
import org.alfresco.repo.cache.SimpleCache;
|
||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
|
||||||
import org.alfresco.repo.security.authentication.Authorization;
|
import org.alfresco.repo.security.authentication.Authorization;
|
||||||
import org.alfresco.repo.web.auth.BasicAuthCredentials;
|
import org.alfresco.repo.web.auth.BasicAuthCredentials;
|
||||||
import org.alfresco.repo.web.auth.TicketCredentials;
|
import org.alfresco.repo.web.auth.TicketCredentials;
|
||||||
@@ -48,25 +48,29 @@ import org.alfresco.util.PropertyCheck;
|
|||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
|
||||||
import org.keycloak.adapters.AuthenticatedActionsHandler;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.OidcKeycloakAccount;
|
|
||||||
import org.keycloak.adapters.PreAuthActionsHandler;
|
|
||||||
import org.keycloak.adapters.servlet.FilterRequestAuthenticator;
|
|
||||||
import org.keycloak.adapters.servlet.OIDCFilterSessionStore;
|
|
||||||
import org.keycloak.adapters.servlet.OIDCServletHttpFacade;
|
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
|
||||||
import org.keycloak.adapters.spi.AuthenticationError;
|
|
||||||
import org.keycloak.adapters.spi.KeycloakAccount;
|
|
||||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
|
||||||
import org.keycloak.adapters.spi.UserSessionManagement;
|
|
||||||
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.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.KeycloakSecurityContext;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.AuthenticatedActionsHandler;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.OidcKeycloakAccount;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.PreAuthActionsHandler;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.servlet.FilterRequestAuthenticator;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.servlet.OIDCFilterSessionStore;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.servlet.OIDCServletHttpFacade;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.AuthenticationError;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.KeycloakAccount;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.UserSessionManagement;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides a Keycloak-based authentication filter which can be used in the role of both global and WebDAV authentication filter.
|
* This class provides a Keycloak-based authentication filter which can be used in the role of both global and WebDAV authentication filter.
|
||||||
*
|
*
|
||||||
@@ -108,6 +112,10 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
|
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
|
|
||||||
|
protected KeycloakAuthenticationComponent keycloakAuthenticationComponent;
|
||||||
|
|
||||||
|
protected SimpleCache<String, RefreshableAccessTokenHolder> keycloakTicketTokenCache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +124,8 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
{
|
{
|
||||||
PropertyCheck.mandatory(this, "keycloakDeployment", this.keycloakDeployment);
|
PropertyCheck.mandatory(this, "keycloakDeployment", this.keycloakDeployment);
|
||||||
PropertyCheck.mandatory(this, "sessionIdMapper", this.sessionIdMapper);
|
PropertyCheck.mandatory(this, "sessionIdMapper", this.sessionIdMapper);
|
||||||
|
PropertyCheck.mandatory(this, "keycloakTicketTokenCache", this.keycloakTicketTokenCache);
|
||||||
|
PropertyCheck.mandatory(this, "keycloakAuthenticationComponent", this.keycloakAuthenticationComponent);
|
||||||
|
|
||||||
// parent class does not check, so we do
|
// parent class does not check, so we do
|
||||||
PropertyCheck.mandatory(this, "authenticationService", this.authenticationService);
|
PropertyCheck.mandatory(this, "authenticationService", this.authenticationService);
|
||||||
@@ -209,6 +219,24 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
this.sessionIdMapper = sessionIdMapper;
|
this.sessionIdMapper = sessionIdMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param keycloakAuthenticationComponent
|
||||||
|
* the keycloakAuthenticationComponent to set
|
||||||
|
*/
|
||||||
|
public void setKeycloakAuthenticationComponent(final KeycloakAuthenticationComponent keycloakAuthenticationComponent)
|
||||||
|
{
|
||||||
|
this.keycloakAuthenticationComponent = keycloakAuthenticationComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param keycloakTicketTokenCache
|
||||||
|
* the keycloakTicketTokenCache to set
|
||||||
|
*/
|
||||||
|
public void setKeycloakTicketTokenCache(final SimpleCache<String, RefreshableAccessTokenHolder> keycloakTicketTokenCache)
|
||||||
|
{
|
||||||
|
this.keycloakTicketTokenCache = keycloakTicketTokenCache;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -289,7 +317,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
this.authenticationService.getCurrentTicket(), false);
|
this.authenticationService.getCurrentTicket(), false);
|
||||||
|
|
||||||
LOGGER.debug("Authenticated user {} via HTTP Basic authentication using an authentication ticket",
|
LOGGER.debug("Authenticated user {} via HTTP Basic authentication using an authentication ticket",
|
||||||
AuthenticationUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
AlfrescoCompatibilityUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
||||||
|
|
||||||
this.authenticationListener.userAuthenticated(new TicketCredentials(password));
|
this.authenticationListener.userAuthenticated(new TicketCredentials(password));
|
||||||
|
|
||||||
@@ -310,7 +338,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
this.authenticationService.getCurrentTicket(), false);
|
this.authenticationService.getCurrentTicket(), false);
|
||||||
|
|
||||||
LOGGER.debug("Authenticated user {} via HTTP Basic authentication using locally stored credentials",
|
LOGGER.debug("Authenticated user {} via HTTP Basic authentication using locally stored credentials",
|
||||||
AuthenticationUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
AlfrescoCompatibilityUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
||||||
|
|
||||||
this.authenticationListener.userAuthenticated(new BasicAuthCredentials(userName, password));
|
this.authenticationListener.userAuthenticated(new BasicAuthCredentials(userName, password));
|
||||||
|
|
||||||
@@ -451,14 +479,28 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
final AccessToken accessToken = keycloakSecurityContext.getToken();
|
final AccessToken accessToken = keycloakSecurityContext.getToken();
|
||||||
final String userId = accessToken.getPreferredUsername();
|
final String userId = accessToken.getPreferredUsername();
|
||||||
|
|
||||||
LOGGER.debug("User {} successfully authenticated via Keycloak", AuthenticationUtil.maskUsername(userId));
|
LOGGER.debug("User {} successfully authenticated via Keycloak", AlfrescoCompatibilityUtil.maskUsername(userId));
|
||||||
|
|
||||||
final SessionUser sessionUser = this.createUserEnvironment(session, userId);
|
final SessionUser sessionUser = this.createUserEnvironment(session, userId);
|
||||||
|
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, keycloakSecurityContext.getIdToken(), true);
|
||||||
|
|
||||||
// need different attribute name than default for integration with web scripts framework
|
// need different attribute name than default for integration with web scripts framework
|
||||||
// default attribute name seems to be no longer used
|
// default attribute name seems to be no longer used
|
||||||
session.setAttribute(AuthenticationDriver.AUTHENTICATION_USER, sessionUser);
|
session.setAttribute(AuthenticationDriver.AUTHENTICATION_USER, sessionUser);
|
||||||
|
|
||||||
this.authenticationListener.userAuthenticated(new KeycloakCredentials(accessToken));
|
this.authenticationListener.userAuthenticated(new KeycloakCredentials(accessToken));
|
||||||
|
|
||||||
|
// store tokens in cache as well for ticket validation
|
||||||
|
// -> necessary i.e. because web script RemoteUserAuthenticator is "evil"
|
||||||
|
// it throws away any authentication from authentication filters like this,
|
||||||
|
// and re-validates via the ticket in the session user
|
||||||
|
|
||||||
|
final RefreshableAccessTokenHolder tokenHolder = new RefreshableAccessTokenHolder(keycloakSecurityContext.getToken(),
|
||||||
|
keycloakSecurityContext.getIdToken(), keycloakSecurityContext.getTokenString(),
|
||||||
|
keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext
|
||||||
|
? ((RefreshableKeycloakSecurityContext) keycloakSecurityContext).getRefreshToken()
|
||||||
|
: null);
|
||||||
|
this.keycloakTicketTokenCache.put(sessionUser.getTicket(), tokenHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (facade.isEnded())
|
if (facade.isEnded())
|
||||||
@@ -555,7 +597,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
&& !this.sessionIdMapper.hasSession(session.getId()))
|
&& !this.sessionIdMapper.hasSession(session.getId()))
|
||||||
{
|
{
|
||||||
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", session.getId(),
|
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", session.getId(),
|
||||||
AuthenticationUtil.maskUsername(sessionUser.getUserName()));
|
AlfrescoCompatibilityUtil.maskUsername(sessionUser.getUserName()));
|
||||||
this.invalidateSession(req);
|
this.invalidateSession(req);
|
||||||
session = req.getSession(false);
|
session = req.getSession(false);
|
||||||
}
|
}
|
||||||
@@ -600,7 +642,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
final KeycloakAccount keycloakAccount = (KeycloakAccount) session.getAttribute(KeycloakAccount.class.getName());
|
final KeycloakAccount keycloakAccount = (KeycloakAccount) session.getAttribute(KeycloakAccount.class.getName());
|
||||||
if (keycloakAccount != null)
|
if (keycloakAccount != null)
|
||||||
{
|
{
|
||||||
skip = this.validateAndRefreshKeycloakAuthentication(req, res, sessionUser.getUserName(), keycloakAccount);
|
skip = this.validateAndRefreshKeycloakAuthentication(req, res, sessionUser.getUserName());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -623,18 +665,40 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
* the HTTP servlet response
|
* the HTTP servlet response
|
||||||
* @param userId
|
* @param userId
|
||||||
* the ID of the authenticated user
|
* the ID of the authenticated user
|
||||||
* @param keycloakAccount
|
|
||||||
* the Keycloak account object
|
|
||||||
* @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter
|
* @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter
|
||||||
* operation} can be skipped as the account represents a valid and still active authentication, {@code false} otherwise
|
* operation} can be skipped as the account represents a valid and still active authentication, {@code false} otherwise
|
||||||
*/
|
*/
|
||||||
protected boolean validateAndRefreshKeycloakAuthentication(final HttpServletRequest req, final HttpServletResponse res,
|
protected boolean validateAndRefreshKeycloakAuthentication(final HttpServletRequest req, final HttpServletResponse res,
|
||||||
final String userId, final KeycloakAccount keycloakAccount)
|
final String userId)
|
||||||
{
|
{
|
||||||
final OIDCServletHttpFacade facade = new OIDCServletHttpFacade(req, res);
|
final OIDCServletHttpFacade facade = new OIDCServletHttpFacade(req, res);
|
||||||
|
|
||||||
final OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(req, facade,
|
final OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(req, facade,
|
||||||
this.bodyBufferLimit > 0 ? this.bodyBufferLimit : DEFAULT_BODY_BUFFER_LIMIT, this.keycloakDeployment, null);
|
this.bodyBufferLimit > 0 ? this.bodyBufferLimit : DEFAULT_BODY_BUFFER_LIMIT, this.keycloakDeployment, null)
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(final RefreshableKeycloakSecurityContext securityContext)
|
||||||
|
{
|
||||||
|
// store tokens in cache as well for ticket validation
|
||||||
|
// -> necessary i.e. because web script RemoteUserAuthenticator is "evil"
|
||||||
|
// it throws away any authentication from authentication filters like this,
|
||||||
|
// and re-validates via the ticket in the session user
|
||||||
|
|
||||||
|
final SessionUser user = (SessionUser) req.getSession()
|
||||||
|
.getAttribute(KeycloakAuthenticationFilter.this.getUserAttributeName());
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
final RefreshableAccessTokenHolder tokenHolder = new RefreshableAccessTokenHolder(securityContext.getToken(),
|
||||||
|
securityContext.getIdToken(), securityContext.getTokenString(), securityContext.getRefreshToken());
|
||||||
|
KeycloakAuthenticationFilter.this.keycloakTicketTokenCache.put(user.getTicket(), tokenHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
final String oldSessionId = req.getSession().getId();
|
final String oldSessionId = req.getSession().getId();
|
||||||
|
|
||||||
@@ -645,6 +709,15 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
boolean skip = false;
|
boolean skip = false;
|
||||||
if (currentSession != null)
|
if (currentSession != null)
|
||||||
{
|
{
|
||||||
|
final Object keycloakAccount = currentSession.getAttribute(KeycloakAccount.class.getName());
|
||||||
|
if (keycloakAccount instanceof OidcKeycloakAccount)
|
||||||
|
{
|
||||||
|
final KeycloakSecurityContext keycloakSecurityContext = ((OidcKeycloakAccount) keycloakAccount)
|
||||||
|
.getKeycloakSecurityContext();
|
||||||
|
this.keycloakAuthenticationComponent.handleUserTokens(keycloakSecurityContext.getToken(),
|
||||||
|
keycloakSecurityContext.getIdToken(), false);
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.trace("Skipping doFilter as Keycloak-authentication session is still valid");
|
LOGGER.trace("Skipping doFilter as Keycloak-authentication session is still valid");
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
@@ -652,7 +725,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
{
|
{
|
||||||
this.sessionIdMapper.removeSession(oldSessionId);
|
this.sessionIdMapper.removeSession(oldSessionId);
|
||||||
LOGGER.debug("Keycloak-authenticated session for user {} was invalidated after token expiration",
|
LOGGER.debug("Keycloak-authenticated session for user {} was invalidated after token expiration",
|
||||||
AuthenticationUtil.maskUsername(userId));
|
AlfrescoCompatibilityUtil.maskUsername(userId));
|
||||||
}
|
}
|
||||||
return skip;
|
return skip;
|
||||||
}
|
}
|
||||||
@@ -704,7 +777,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
this.authenticationService.getCurrentTicket(), true);
|
this.authenticationService.getCurrentTicket(), true);
|
||||||
|
|
||||||
LOGGER.debug("Authenticated user {} via URL-provided authentication ticket",
|
LOGGER.debug("Authenticated user {} via URL-provided authentication ticket",
|
||||||
AuthenticationUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
AlfrescoCompatibilityUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
||||||
|
|
||||||
this.authenticationListener.userAuthenticated(new TicketCredentials(ticket));
|
this.authenticationListener.userAuthenticated(new TicketCredentials(ticket));
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import org.alfresco.repo.cache.SimpleCache;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationServiceImpl;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.repo.security.authentication.TicketComponent;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this specialised authentication service sub-class keep track of Keycloak token responses to password-based logins, and
|
||||||
|
* validate / refresh the Keycloak session whenever the associated user authentication ticket is validated.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticationServiceImpl extends AuthenticationServiceImpl implements InitializingBean
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationServiceImpl.class);
|
||||||
|
|
||||||
|
// need to copy these fields from base class because they are package-protected there
|
||||||
|
protected KeycloakAuthenticationComponent authenticationComponent;
|
||||||
|
|
||||||
|
protected TicketComponent ticketComponent;
|
||||||
|
|
||||||
|
protected SimpleCache<String, RefreshableAccessTokenHolder> keycloakTicketTokenCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "authenticationComponent", this.authenticationComponent);
|
||||||
|
PropertyCheck.mandatory(this, "ticketComponent", this.ticketComponent);
|
||||||
|
PropertyCheck.mandatory(this, "keycloakTicketTokenCache", this.keycloakTicketTokenCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param authenticationComponent
|
||||||
|
* the authenticationComponent to set
|
||||||
|
*/
|
||||||
|
public void setAuthenticationComponent(final KeycloakAuthenticationComponent authenticationComponent)
|
||||||
|
{
|
||||||
|
this.authenticationComponent = authenticationComponent;
|
||||||
|
super.setAuthenticationComponent(authenticationComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setTicketComponent(final TicketComponent ticketComponent)
|
||||||
|
{
|
||||||
|
this.ticketComponent = ticketComponent;
|
||||||
|
super.setTicketComponent(ticketComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param keycloakTicketTokenCache
|
||||||
|
* the keycloakTicketTokenCache to set
|
||||||
|
*/
|
||||||
|
public void setKeycloakTicketTokenCache(final SimpleCache<String, RefreshableAccessTokenHolder> keycloakTicketTokenCache)
|
||||||
|
{
|
||||||
|
this.keycloakTicketTokenCache = keycloakTicketTokenCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void authenticate(final String userName, final char[] password) throws AuthenticationException
|
||||||
|
{
|
||||||
|
this.authenticationComponent.enableLastTokenStore();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
super.authenticate(userName, password);
|
||||||
|
|
||||||
|
final RefreshableAccessTokenHolder lastTokenResponse = this.authenticationComponent.getLastTokenResponse();
|
||||||
|
if (lastTokenResponse != null)
|
||||||
|
{
|
||||||
|
final String currentTicket = this.getCurrentTicket();
|
||||||
|
LOGGER.debug("Associating ticket {} for user {} with Keycloak access token", currentTicket,
|
||||||
|
AlfrescoCompatibilityUtil.maskUsername(userName));
|
||||||
|
this.keycloakTicketTokenCache.put(currentTicket, lastTokenResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.authenticationComponent.disableLastTokenStore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void validate(final String ticket) throws AuthenticationException
|
||||||
|
{
|
||||||
|
super.validate(ticket);
|
||||||
|
|
||||||
|
if (this.keycloakTicketTokenCache.contains(ticket))
|
||||||
|
{
|
||||||
|
final RefreshableAccessTokenHolder refreshableAccessToken = this.keycloakTicketTokenCache.get(ticket);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final RefreshableAccessTokenHolder refreshedToken = this.authenticationComponent
|
||||||
|
.checkAndRefreshTicketToken(refreshableAccessToken);
|
||||||
|
if (refreshedToken != null)
|
||||||
|
{
|
||||||
|
this.keycloakTicketTokenCache.put(ticket, refreshedToken);
|
||||||
|
}
|
||||||
|
// apparently expiration is allowed - remove from cache to avoid unnecessary checks in the future
|
||||||
|
else if (refreshableAccessToken.isExpired())
|
||||||
|
{
|
||||||
|
LOGGER.warn(
|
||||||
|
"The Keycloak access token associated with ticket {} for user {} has expired - Keycloak roles / claims are no longer available for the corresponding user",
|
||||||
|
ticket, AlfrescoCompatibilityUtil.maskUsername(AuthenticationUtil.getFullyAuthenticatedUser()));
|
||||||
|
this.keycloakTicketTokenCache.remove(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final AuthenticationException ae)
|
||||||
|
{
|
||||||
|
this.clearCurrentSecurityContext();
|
||||||
|
throw ae;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -17,7 +17,8 @@ package de.acosix.alfresco.keycloak.repo.authentication;
|
|||||||
|
|
||||||
import org.alfresco.repo.web.auth.WebCredentials;
|
import org.alfresco.repo.web.auth.WebCredentials;
|
||||||
import org.alfresco.util.ParameterCheck;
|
import org.alfresco.util.ParameterCheck;
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -25,13 +25,14 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
|||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
||||||
import org.alfresco.service.cmr.security.PersonService;
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
import org.keycloak.adapters.BearerTokenRequestAuthenticator;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.BearerTokenRequestAuthenticator;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -26,8 +26,9 @@ import java.util.Map;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.alfresco.util.Pair;
|
import org.alfresco.util.Pair;
|
||||||
import org.keycloak.adapters.servlet.ServletHttpFacade;
|
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.servlet.ServletHttpFacade;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This {@link HttpFacade} wraps servlet requests and responses in such a way that any response headers / cookies being set by Keycloak
|
* This {@link HttpFacade} wraps servlet requests and responses in such a way that any response headers / cookies being set by Keycloak
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -21,9 +21,10 @@ import java.util.Set;
|
|||||||
|
|
||||||
import org.alfresco.repo.cache.SimpleCache;
|
import org.alfresco.repo.cache.SimpleCache;
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.IDToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface are used to map data from Keycloak authenticated users to the Alfresco person node. All instances of this
|
||||||
|
* interface in the Keycloak authentication subsystem will be consulted in the order the beans are defined in the Spring application
|
||||||
|
* context, resulting in an aggregated map of person node properties.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface UserProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps data from Keycloak access and ID tokens to a map of properties for the corresponding person node.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the Keycloak access token for the authenticated user
|
||||||
|
* @param idToken
|
||||||
|
* the Keycloak ID token for the authenticated user - may be {@code null} if not contained in the authentication response
|
||||||
|
* @param personNodeProperties
|
||||||
|
* the properties to set on the Alfresco person node corresponding to the authenticated user
|
||||||
|
*/
|
||||||
|
void mapUser(AccessToken accessToken, IDToken idToken, Map<QName, Serializable> personNodeProperties);
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.authority;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.repo.security.authority.AuthorityServiceImpl;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityService;
|
||||||
|
import org.alfresco.service.cmr.security.PermissionService;
|
||||||
|
|
||||||
|
import net.sf.acegisecurity.Authentication;
|
||||||
|
import net.sf.acegisecurity.GrantedAuthority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This specialisation of the Alfresco default authority service includes the current user's {@link GrantedAuthority granted authorities} in
|
||||||
|
* the set of authorities. This is necessary to ensure that operations such as {@link AuthorityService#isAdminAuthority(String) admin
|
||||||
|
* checks} work correctly, where as permission checks performed by the {@link PermissionService permission service} already take granted
|
||||||
|
* authorities into account.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class GrantedAuthorityAwareAuthorityServiceImpl extends AuthorityServiceImpl
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Set<String> getAuthoritiesForUser(final String currentUserName)
|
||||||
|
{
|
||||||
|
final Set<String> authoritiesForUser = super.getAuthoritiesForUser(currentUserName);
|
||||||
|
|
||||||
|
final String runAsUser = AuthenticationUtil.getRunAsUser();
|
||||||
|
final String fullUser = AuthenticationUtil.getFullyAuthenticatedUser();
|
||||||
|
|
||||||
|
final Authentication runAsAuthentication = AuthenticationUtil.getRunAsAuthentication();
|
||||||
|
final Authentication fullAuthentication = AuthenticationUtil.getFullAuthentication();
|
||||||
|
|
||||||
|
if (runAsAuthentication != null && currentUserName.equals(runAsUser))
|
||||||
|
{
|
||||||
|
for (final GrantedAuthority authority : runAsAuthentication.getAuthorities())
|
||||||
|
{
|
||||||
|
authoritiesForUser.add(authority.getAuthority());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (fullAuthentication != null && currentUserName.equals(fullUser))
|
||||||
|
{
|
||||||
|
for (final GrantedAuthority authority : fullAuthentication.getAuthorities())
|
||||||
|
{
|
||||||
|
authoritiesForUser.add(authority.getAuthority());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authoritiesForUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.client;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface wrap the relevant Keycloak admin ReST API for the synchronisation of users and groups from a Keycloak
|
||||||
|
* instance.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface IDMClient
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the number of users within the Keycloak IDM database.
|
||||||
|
*
|
||||||
|
* @return the count of users in the Keycloak database
|
||||||
|
*/
|
||||||
|
int countUsers();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the number of groups within the Keycloak IDM database.
|
||||||
|
*
|
||||||
|
* @return the count of groups in the Keycloak database
|
||||||
|
*/
|
||||||
|
int countGroups();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the details of one specific group from Keycloak.
|
||||||
|
*
|
||||||
|
* @param groupId
|
||||||
|
* the ID of the group in Keycloak
|
||||||
|
* @return the group details
|
||||||
|
*/
|
||||||
|
GroupRepresentation getGroup(String groupId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and processes a batch of users from Keycloak using an externally specified processor.
|
||||||
|
*
|
||||||
|
* @param offset
|
||||||
|
* the index of the first user to retrieve
|
||||||
|
* @param userBatchSize
|
||||||
|
* the number of users to load in one batch
|
||||||
|
* @param userProcessor
|
||||||
|
* the processor handling the loaded users
|
||||||
|
* @return the number of processed users
|
||||||
|
*/
|
||||||
|
int processUsers(int offset, int userBatchSize, Consumer<UserRepresentation> userProcessor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and processes a batch of groups of a specific user from Keycloak using an externally specified processor.
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* the ID of user for which to process groups
|
||||||
|
* @param offset
|
||||||
|
* the index of the first group to retrieve
|
||||||
|
* @param groupBatchSize
|
||||||
|
* the number of groups to load in one batch
|
||||||
|
* @param groupProcessor
|
||||||
|
* the processor handling the loaded groups
|
||||||
|
* @return the number of processed groups
|
||||||
|
*/
|
||||||
|
int processUserGroups(String userId, int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and processes a batch of groups from Keycloak using an externally specified processor.
|
||||||
|
*
|
||||||
|
* @param offset
|
||||||
|
* the index of the first group to retrieve
|
||||||
|
* @param groupBatchSize
|
||||||
|
* the number of groups to load in one batch
|
||||||
|
* @param groupProcessor
|
||||||
|
* the processor handling the loaded groups
|
||||||
|
* @return the number of processed groups
|
||||||
|
*/
|
||||||
|
int processGroups(int offset, int groupBatchSize, Consumer<GroupRepresentation> groupProcessor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and processes a batch of users / members of a group from Keycloak using an externally specified processor.
|
||||||
|
*
|
||||||
|
* @param groupId
|
||||||
|
* the ID of group for which to process members
|
||||||
|
* @param offset
|
||||||
|
* the index of the first user to retrieve
|
||||||
|
* @param userBatchSize
|
||||||
|
* the number of users to load in one batch
|
||||||
|
* @param userProcessor
|
||||||
|
* the processor handling the loaded users
|
||||||
|
* @return the number of processed users
|
||||||
|
*/
|
||||||
|
int processMembers(String groupId, int offset, int userBatchSize, Consumer<UserRepresentation> userProcessor);
|
||||||
|
}
|
@@ -0,0 +1,699 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
|
import org.alfresco.httpclient.HttpClientFactory.NonBlockingHttpParamsFactory;
|
||||||
|
import org.alfresco.repo.content.MimetypeMap;
|
||||||
|
import org.alfresco.util.ParameterCheck;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.apache.commons.httpclient.params.DefaultHttpParams;
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.NameValuePair;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.MappingIterator;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.OAuth2Constants;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.ServerRequest;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.rotation.AdapterTokenVerifier;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.VerificationException;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.util.Time;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.constants.ServiceUrlConstants;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessTokenResponse;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.util.JsonSerialization;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the API for a client to the Keycloak admin ReST API specific to IDM structures.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class IDMClientImpl implements InitializingBean, IDMClient
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(IDMClientImpl.class);
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
// use same Alfresco NonBlockingHttpParamsFactory as SolrQueryHTTPClient (indirectly) does
|
||||||
|
DefaultHttpParams.setHttpParamsFactory(new NonBlockingHttpParamsFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final ReentrantReadWriteLock tokenLock = new ReentrantReadWriteLock(true);
|
||||||
|
|
||||||
|
protected KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
protected String userName;
|
||||||
|
|
||||||
|
protected String password;
|
||||||
|
|
||||||
|
protected RefreshableAccessTokenHolder token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param deployment
|
||||||
|
* the deployment to set
|
||||||
|
*/
|
||||||
|
public void setDeployment(final KeycloakDeployment deployment)
|
||||||
|
{
|
||||||
|
this.deployment = deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param userName
|
||||||
|
* the userName to set
|
||||||
|
*/
|
||||||
|
public void setUserName(final String userName)
|
||||||
|
{
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param password
|
||||||
|
* the password to set
|
||||||
|
*/
|
||||||
|
public void setPassword(final String password)
|
||||||
|
{
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int countUsers()
|
||||||
|
{
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users/count")
|
||||||
|
.build(this.deployment.getRealm());
|
||||||
|
|
||||||
|
final AtomicInteger count = new AtomicInteger(0);
|
||||||
|
this.processGenericGet(uri, root -> {
|
||||||
|
if (root.isInt())
|
||||||
|
{
|
||||||
|
count.set(root.intValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AlfrescoRuntimeException("Keycloak admin API did not yield expected data for user count");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return count.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int countGroups()
|
||||||
|
{
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups/count")
|
||||||
|
.build(this.deployment.getRealm());
|
||||||
|
|
||||||
|
final AtomicInteger count = new AtomicInteger(0);
|
||||||
|
this.processGenericGet(uri, root -> {
|
||||||
|
if (root.isObject() && root.has("count"))
|
||||||
|
{
|
||||||
|
count.set(root.get("count").intValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AlfrescoRuntimeException("Keycloak admin API did not yield expected JSON data for group count");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return count.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GroupRepresentation getGroup(final String groupId)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatoryString("groupId", groupId);
|
||||||
|
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups/{groupId}")
|
||||||
|
.build(this.deployment.getRealm(), groupId);
|
||||||
|
|
||||||
|
final GroupRepresentation group = this.processGenericGet(uri, GroupRepresentation.class);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int processUsers(final int offset, final int userBatchSize, final Consumer<UserRepresentation> userProcessor)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("userProcessor", userProcessor);
|
||||||
|
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("offset must be a non-negative integer");
|
||||||
|
}
|
||||||
|
if (userBatchSize <= 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("userBatchSize must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users")
|
||||||
|
.queryParam("first", offset).queryParam("max", userBatchSize).build(this.deployment.getRealm());
|
||||||
|
|
||||||
|
final int processedUsers = this.processEntityBatch(uri, userProcessor, UserRepresentation.class);
|
||||||
|
return processedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int processUserGroups(final String userId, final int offset, final int groupBatchSize,
|
||||||
|
final Consumer<GroupRepresentation> groupProcessor)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatoryString("userId", userId);
|
||||||
|
ParameterCheck.mandatory("groupProcessor", groupProcessor);
|
||||||
|
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/users/{user}/groups")
|
||||||
|
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm(), userId);
|
||||||
|
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("offset must be a non-negative integer");
|
||||||
|
}
|
||||||
|
if (groupBatchSize <= 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("groupBatchSize must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int processedGroups = this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
|
||||||
|
return processedGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int processGroups(final int offset, final int groupBatchSize, final Consumer<GroupRepresentation> groupProcessor)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("groupProcessor", groupProcessor);
|
||||||
|
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()).path("/admin/realms/{realm}/groups")
|
||||||
|
.queryParam("first", offset).queryParam("max", groupBatchSize).build(this.deployment.getRealm());
|
||||||
|
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("offset must be a non-negative integer");
|
||||||
|
}
|
||||||
|
if (groupBatchSize <= 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("groupBatchSize must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int processedGroups = this.processEntityBatch(uri, groupProcessor, GroupRepresentation.class);
|
||||||
|
return processedGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int processMembers(final String groupId, final int offset, final int userBatchSize,
|
||||||
|
final Consumer<UserRepresentation> userProcessor)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatoryString("groupId", groupId);
|
||||||
|
ParameterCheck.mandatory("userProcessor", userProcessor);
|
||||||
|
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("offset must be a non-negative integer");
|
||||||
|
}
|
||||||
|
if (userBatchSize <= 0)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("userBatchSize must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
final URI uri = KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
||||||
|
.path("/admin/realms/{realm}/groups/{groupId}/members").queryParam("first", offset).queryParam("max", userBatchSize)
|
||||||
|
.build(this.deployment.getRealm(), groupId);
|
||||||
|
|
||||||
|
final int processedUsers = this.processEntityBatch(uri, userProcessor, UserRepresentation.class);
|
||||||
|
return processedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and processes a batch of generic entities from Keycloak.
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* the type of the response entities
|
||||||
|
* @param uri
|
||||||
|
* the URI to call
|
||||||
|
* @param entityProcessor
|
||||||
|
* the processor handling the loaded entities
|
||||||
|
* @param entityClass
|
||||||
|
* the type of the expected response entities
|
||||||
|
* @return the number of processed entities
|
||||||
|
*/
|
||||||
|
protected <T> int processEntityBatch(final URI uri, final Consumer<T> entityProcessor, final Class<T> entityClass)
|
||||||
|
{
|
||||||
|
final HttpGet get = new HttpGet(uri);
|
||||||
|
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
|
||||||
|
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final HttpClient client = this.deployment.getClient();
|
||||||
|
final HttpResponse response = client.execute(get);
|
||||||
|
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity httpEntity = response.getEntity();
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
EntityUtils.consumeQuietly(httpEntity);
|
||||||
|
throw new IOException("Bad status: " + status);
|
||||||
|
}
|
||||||
|
if (httpEntity == null)
|
||||||
|
{
|
||||||
|
throw new IOException("Response does not contain a body");
|
||||||
|
}
|
||||||
|
|
||||||
|
final InputStream is = httpEntity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final MappingIterator<T> iterator = JsonSerialization.mapper.readerFor(entityClass).readValues(is);
|
||||||
|
|
||||||
|
int entitiesProcessed = 0;
|
||||||
|
while (iterator.hasNextValue())
|
||||||
|
{
|
||||||
|
final T loadedEntity = iterator.nextValue();
|
||||||
|
entityProcessor.accept(loadedEntity);
|
||||||
|
entitiesProcessed++;
|
||||||
|
}
|
||||||
|
return entitiesProcessed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Failed to retrieve entities", ioex);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a generic HTTP GET operation yielding a JSON response.
|
||||||
|
*
|
||||||
|
* @param uri
|
||||||
|
* the URI to call
|
||||||
|
* @param responseProcessor
|
||||||
|
* the processor handling the response JSON
|
||||||
|
*/
|
||||||
|
protected void processGenericGet(final URI uri, final Consumer<JsonNode> responseProcessor)
|
||||||
|
{
|
||||||
|
final HttpGet get = new HttpGet(uri);
|
||||||
|
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
|
||||||
|
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final HttpClient client = this.deployment.getClient();
|
||||||
|
final HttpResponse response = client.execute(get);
|
||||||
|
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity httpEntity = response.getEntity();
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
EntityUtils.consumeQuietly(httpEntity);
|
||||||
|
throw new IOException("Bad status: " + status);
|
||||||
|
}
|
||||||
|
if (httpEntity == null)
|
||||||
|
{
|
||||||
|
throw new IOException("Response does not contain a body");
|
||||||
|
}
|
||||||
|
|
||||||
|
final InputStream is = httpEntity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final JsonNode root = JsonSerialization.mapper.readTree(is);
|
||||||
|
responseProcessor.accept(root);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Failed to retrieve entities", ioex);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a generic HTTP GET operation yielding a mapped response entity.
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* the type of the response entity
|
||||||
|
* @param uri
|
||||||
|
* the URI to call
|
||||||
|
* @param responseType
|
||||||
|
* the class object for the type of the response entity
|
||||||
|
* @return the response entity
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected <T> T processGenericGet(final URI uri, final Class<T> responseType)
|
||||||
|
{
|
||||||
|
final HttpGet get = new HttpGet(uri);
|
||||||
|
get.addHeader("Accept", MimetypeMap.MIMETYPE_JSON);
|
||||||
|
get.addHeader("Authorization", "Bearer " + this.getValidAccessTokenForRequest());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final HttpClient client = this.deployment.getClient();
|
||||||
|
final HttpResponse response = client.execute(get);
|
||||||
|
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity httpEntity = response.getEntity();
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
EntityUtils.consumeQuietly(httpEntity);
|
||||||
|
throw new IOException("Bad status: " + status);
|
||||||
|
}
|
||||||
|
if (httpEntity == null)
|
||||||
|
{
|
||||||
|
throw new IOException("Response does not contain a body");
|
||||||
|
}
|
||||||
|
|
||||||
|
final InputStream is = httpEntity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final T responseEntity = JsonSerialization.mapper.readValue(is, responseType);
|
||||||
|
return responseEntity;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Failed to retrieve entities", ioex);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to retrieve entities", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves / determines a valid access token for a request to the admin ReST API.
|
||||||
|
*
|
||||||
|
* @return the valid access token to use in a request immediately following this operation
|
||||||
|
*/
|
||||||
|
protected String getValidAccessTokenForRequest()
|
||||||
|
{
|
||||||
|
String validToken = null;
|
||||||
|
|
||||||
|
this.tokenLock.readLock().lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.token != null && (!this.token.canRefresh() || !this.token.shouldRefresh(this.deployment.getTokenMinimumTimeToLive())))
|
||||||
|
{
|
||||||
|
validToken = this.token.getToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.tokenLock.readLock().unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validToken == null)
|
||||||
|
{
|
||||||
|
this.tokenLock.writeLock().lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.token != null
|
||||||
|
&& (!this.token.canRefresh() || !this.token.shouldRefresh(this.deployment.getTokenMinimumTimeToLive())))
|
||||||
|
{
|
||||||
|
validToken = this.token.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validToken == null)
|
||||||
|
{
|
||||||
|
this.obtainOrRefreshAccessToken();
|
||||||
|
|
||||||
|
validToken = this.token.getToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.tokenLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves or refreshes an access token, depending on the presence of valid refresh token for a previous access token.
|
||||||
|
*/
|
||||||
|
protected void obtainOrRefreshAccessToken()
|
||||||
|
{
|
||||||
|
AccessTokenResponse response;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.token != null && this.token.canRefresh())
|
||||||
|
{
|
||||||
|
response = ServerRequest.invokeRefresh(this.deployment, this.token.getRefreshToken());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response = this.userName != null && !this.userName.isEmpty() ? this.getAccessToken(this.userName, this.password)
|
||||||
|
: this.getAccessToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error retrieving / refreshing access token", ioex);
|
||||||
|
throw new AlfrescoRuntimeException("Error retrieving / refreshing access token", ioex);
|
||||||
|
}
|
||||||
|
catch (final ServerRequest.HttpFailure httpFailure)
|
||||||
|
{
|
||||||
|
LOGGER.error("Refreshing access token failed: {} {}", httpFailure.getStatus(), httpFailure.getError());
|
||||||
|
throw new AlfrescoRuntimeException("Failed to refresh access token: " + httpFailure.getStatus() + " " + httpFailure.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
final String tokenString = response.getToken();
|
||||||
|
|
||||||
|
final AdapterTokenVerifier.VerifiedTokens tokens;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tokens = AdapterTokenVerifier.verifyTokens(tokenString, response.getIdToken(), this.deployment);
|
||||||
|
}
|
||||||
|
catch (final VerificationException vex)
|
||||||
|
{
|
||||||
|
LOGGER.error("Token verification failed", vex);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to verify token", vex);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AccessToken accessToken = tokens.getAccessToken();
|
||||||
|
|
||||||
|
if ((accessToken.getExpiration() - this.deployment.getTokenMinimumTimeToLive()) <= Time.currentTime())
|
||||||
|
{
|
||||||
|
throw new AlfrescoRuntimeException("Failed to retrieve / refresh the access token with a longer time-to-live than the minimum");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tokenLock.writeLock().lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.token = new RefreshableAccessTokenHolder(response, tokens);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.tokenLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.getNotBeforePolicy() > this.deployment.getNotBefore())
|
||||||
|
{
|
||||||
|
this.deployment.updateNotBefore(response.getNotBeforePolicy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an access token using client credentials.
|
||||||
|
*
|
||||||
|
* @return the access token
|
||||||
|
* @throws IOException
|
||||||
|
* when errors occur in the HTTP interaction
|
||||||
|
*/
|
||||||
|
protected AccessTokenResponse getAccessToken() throws IOException
|
||||||
|
{
|
||||||
|
LOGGER.debug("Retrieving access token with client credentrials");
|
||||||
|
final AccessTokenResponse tokenResponse = this.getAccessTokenImpl(formParams -> {
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
||||||
|
});
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an access token for a specified synchronisation user.
|
||||||
|
*
|
||||||
|
* @param userName
|
||||||
|
* the user to use for synchronisation access
|
||||||
|
* @param password
|
||||||
|
* the password of the user
|
||||||
|
* @return the access token
|
||||||
|
* @throws IOException
|
||||||
|
* when errors occur in the HTTP interaction
|
||||||
|
*/
|
||||||
|
protected AccessTokenResponse getAccessToken(final String userName, final String password) throws IOException
|
||||||
|
{
|
||||||
|
LOGGER.debug("Retrieving access token for user {}", userName);
|
||||||
|
final AccessTokenResponse tokenResponse = this.getAccessTokenImpl(formParams -> {
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
||||||
|
formParams.add(new BasicNameValuePair("username", userName));
|
||||||
|
formParams.add(new BasicNameValuePair("password", password));
|
||||||
|
});
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer.
|
||||||
|
*
|
||||||
|
* @param postParamProvider
|
||||||
|
* a provider of HTTP POST parameters for the access token request
|
||||||
|
* @return the access token
|
||||||
|
* @throws IOException
|
||||||
|
* when errors occur in the HTTP interaction
|
||||||
|
*/
|
||||||
|
// implementing this method locally avoids having the dependency on Keycloak authz-client
|
||||||
|
// authz-client does not support refresh, so would be of limited value anyway
|
||||||
|
protected AccessTokenResponse getAccessTokenImpl(final Consumer<List<NameValuePair>> postParamProvider) throws IOException
|
||||||
|
{
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
final HttpClient client = this.deployment.getClient();
|
||||||
|
|
||||||
|
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
||||||
|
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
|
||||||
|
final List<NameValuePair> formParams = new ArrayList<>();
|
||||||
|
|
||||||
|
postParamProvider.accept(formParams);
|
||||||
|
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(this.deployment, post, formParams);
|
||||||
|
|
||||||
|
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
|
||||||
|
final HttpResponse response = client.execute(post);
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
final String statusReason = response.getStatusLine().getReasonPhrase();
|
||||||
|
LOGGER.debug("Failed to retrieve access token due to HTTP {}: {}", status, statusReason);
|
||||||
|
EntityUtils.consumeQuietly(entity);
|
||||||
|
throw new AlfrescoRuntimeException("Failed to retrieve access token due to HTTP error " + status + ": " + statusReason);
|
||||||
|
}
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
throw new AlfrescoRuntimeException("Response to access token request did not contain a response body");
|
||||||
|
}
|
||||||
|
|
||||||
|
final InputStream is = entity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -31,7 +31,6 @@ import java.util.Set;
|
|||||||
|
|
||||||
import org.alfresco.error.AlfrescoRuntimeException;
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
@@ -41,6 +40,8 @@ import org.springframework.util.PropertyPlaceholderHelper;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -18,13 +18,14 @@ package de.acosix.alfresco.keycloak.repo.spring;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
import org.keycloak.adapters.HttpClientBuilder;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.HttpClientBuilder;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
import org.alfresco.service.namespace.NamespaceService;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides common configuration properties and logic for mapping processor handling Keycloak authority attributes.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public abstract class BaseAttributeProcessor implements InitializingBean
|
||||||
|
{
|
||||||
|
|
||||||
|
protected NamespaceService namespaceService;
|
||||||
|
|
||||||
|
protected Map<String, String> attributePropertyMappings;
|
||||||
|
|
||||||
|
protected Map<String, QName> attributePropertyQNameMappings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "namespaceService", this.namespaceService);
|
||||||
|
|
||||||
|
if (this.attributePropertyMappings != null && !this.attributePropertyMappings.isEmpty())
|
||||||
|
{
|
||||||
|
this.attributePropertyQNameMappings = new HashMap<>();
|
||||||
|
this.attributePropertyMappings
|
||||||
|
.forEach((k, v) -> this.attributePropertyQNameMappings.put(k, QName.resolveToQName(this.namespaceService, v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param namespaceService
|
||||||
|
* the namespaceService to set
|
||||||
|
*/
|
||||||
|
public void setNamespaceService(final NamespaceService namespaceService)
|
||||||
|
{
|
||||||
|
this.namespaceService = namespaceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param attributePropertyMappings
|
||||||
|
* the attributePropertyMappings to set
|
||||||
|
*/
|
||||||
|
public void setAttributePropertyMappings(final Map<String, String> attributePropertyMappings)
|
||||||
|
{
|
||||||
|
this.attributePropertyMappings = attributePropertyMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the general attribute to property mapping. This operation will not handle any value type conversions, relying instead on the
|
||||||
|
* underlying data types and registered type converters in Alfresco to handle conversion in the persistence layer. Any attribute which
|
||||||
|
* is only associated with a single value will be unwrapped from a list to the singular value, while all other attributes will be kept
|
||||||
|
* as-is. THis operation also does not perform any checks whether a configured target property actually supports a multi-valued
|
||||||
|
* property, again leaving that kind of processing to the Alfresco default functionality of integrity checking.
|
||||||
|
*
|
||||||
|
* @param attributes
|
||||||
|
* the list of attributes
|
||||||
|
* @param nodeDescription
|
||||||
|
* the node description to enhance
|
||||||
|
*/
|
||||||
|
protected void map(final Map<String, List<String>> attributes, final NodeDescription nodeDescription)
|
||||||
|
{
|
||||||
|
if (this.attributePropertyQNameMappings != null && attributes != null)
|
||||||
|
{
|
||||||
|
attributes.keySet().stream().filter(this.attributePropertyQNameMappings::containsKey)
|
||||||
|
.forEach(k -> this.mapAttribute(k, attributes, nodeDescription));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an individual attribute to the correlating node property of the node description.
|
||||||
|
*
|
||||||
|
* @param attribute
|
||||||
|
* the name of the attribute to map
|
||||||
|
* @param attributes
|
||||||
|
* the list of attributes
|
||||||
|
* @param nodeDescription
|
||||||
|
* the node description to enhance
|
||||||
|
*/
|
||||||
|
protected void mapAttribute(final String attribute, final Map<String, List<String>> attributes, final NodeDescription nodeDescription)
|
||||||
|
{
|
||||||
|
final QName propertyQName = this.attributePropertyQNameMappings.get(attribute);
|
||||||
|
final List<String> values = attributes.get(attribute);
|
||||||
|
if (values != null)
|
||||||
|
{
|
||||||
|
Serializable value;
|
||||||
|
if (values.size() == 1)
|
||||||
|
{
|
||||||
|
value = values.get(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = new ArrayList<>(values);
|
||||||
|
}
|
||||||
|
nodeDescription.getProperties().put(propertyQName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.alfresco.util.ParameterCheck;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.client.IDMClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides common configuration and logic relevant for any filter based on authority group containments.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public abstract class BaseGroupContainmentFilter implements InitializingBean
|
||||||
|
{
|
||||||
|
|
||||||
|
protected IDMClient idmClient;
|
||||||
|
|
||||||
|
protected List<String> groupPaths;
|
||||||
|
|
||||||
|
protected List<String> groupIds;
|
||||||
|
|
||||||
|
protected List<String> idResolvedGroupPaths;
|
||||||
|
|
||||||
|
protected boolean requireAll = false;
|
||||||
|
|
||||||
|
protected boolean allowTransitive = true;
|
||||||
|
|
||||||
|
protected int groupLoadBatchSize = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "idmClient", this.idmClient);
|
||||||
|
|
||||||
|
if (this.groupIds != null && !this.groupIds.isEmpty())
|
||||||
|
{
|
||||||
|
this.idResolvedGroupPaths = new ArrayList<>();
|
||||||
|
this.groupIds.stream().map(id -> this.idmClient.getGroup(id).getPath()).forEach(this.idResolvedGroupPaths::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param idmClient
|
||||||
|
* the idmClient to set
|
||||||
|
*/
|
||||||
|
public void setIdmClient(final IDMClient idmClient)
|
||||||
|
{
|
||||||
|
this.idmClient = idmClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groupPaths
|
||||||
|
* the groupPaths to set as a comma-separated string of paths
|
||||||
|
*/
|
||||||
|
public void setGroupPaths(final String groupPaths)
|
||||||
|
{
|
||||||
|
this.groupPaths = groupPaths != null && !groupPaths.isEmpty() ? Arrays.asList(groupPaths.split(",")) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groupIds
|
||||||
|
* the groupIds to set as a comma-separated string of paths
|
||||||
|
*/
|
||||||
|
public void setGroupIds(final String groupIds)
|
||||||
|
{
|
||||||
|
this.groupIds = groupIds != null && !groupIds.isEmpty() ? Arrays.asList(groupIds.split(",")) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param requireAll
|
||||||
|
* the requireAll to set
|
||||||
|
*/
|
||||||
|
public void setRequireAll(final boolean requireAll)
|
||||||
|
{
|
||||||
|
this.requireAll = requireAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param allowTransitive
|
||||||
|
* the allowTransitive to set
|
||||||
|
*/
|
||||||
|
public void setAllowTransitive(final boolean allowTransitive)
|
||||||
|
{
|
||||||
|
this.allowTransitive = allowTransitive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groupLoadBatchSize
|
||||||
|
* the groupLoadBatchSize to set
|
||||||
|
*/
|
||||||
|
public void setGroupLoadBatchSize(final int groupLoadBatchSize)
|
||||||
|
{
|
||||||
|
this.groupLoadBatchSize = groupLoadBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether parent groups match the configured restrictions.
|
||||||
|
*
|
||||||
|
* @param parentGroupIds
|
||||||
|
* the list of parent group IDs for an authority
|
||||||
|
* @param parentGroupPaths
|
||||||
|
* the list of parent group paths for an authority
|
||||||
|
* @return {@code true} if the parent groups match the configured restrictions, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
protected boolean parentGroupsMatch(final List<String> parentGroupIds, final List<String> parentGroupPaths)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("parentGroupIds", parentGroupIds);
|
||||||
|
ParameterCheck.mandatory("parentGroupPaths", parentGroupPaths);
|
||||||
|
|
||||||
|
boolean matches;
|
||||||
|
|
||||||
|
if (this.requireAll)
|
||||||
|
{
|
||||||
|
if (this.allowTransitive)
|
||||||
|
{
|
||||||
|
final boolean allPathsMatch = this.groupPaths == null
|
||||||
|
|| this.groupPaths.stream().allMatch(path -> this.groupPathOrTransitiveContained(path, parentGroupPaths));
|
||||||
|
final boolean allResolvedPathsMatch = this.idResolvedGroupPaths == null
|
||||||
|
|| this.idResolvedGroupPaths.stream().allMatch(path -> this.groupPathOrTransitiveContained(path, parentGroupPaths));
|
||||||
|
matches = allPathsMatch && allResolvedPathsMatch;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
final boolean allPathsMatch = this.groupPaths == null || this.groupPaths.stream().allMatch(parentGroupPaths::contains);
|
||||||
|
// parentGroupIds might be empty if they cannot be efficiently retrieved or paths are sufficiently known
|
||||||
|
final boolean allIdsMatch = this.groupIds == null || this.groupIds.stream().allMatch(parentGroupIds::contains)
|
||||||
|
|| this.idResolvedGroupPaths.stream().allMatch(parentGroupPaths::contains);
|
||||||
|
matches = allPathsMatch && allIdsMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (this.allowTransitive)
|
||||||
|
{
|
||||||
|
matches = (this.groupPaths != null
|
||||||
|
&& this.groupPaths.stream().anyMatch(path -> this.groupPathOrTransitiveContained(path, parentGroupPaths)));
|
||||||
|
matches = matches || (this.idResolvedGroupPaths != null && this.idResolvedGroupPaths.stream()
|
||||||
|
.anyMatch(path -> this.groupPathOrTransitiveContained(path, parentGroupPaths)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
matches = (this.groupPaths != null && this.groupPaths.stream().anyMatch(parentGroupPaths::contains));
|
||||||
|
matches = matches || (this.groupIds != null && (this.groupIds.stream().anyMatch(parentGroupIds::contains)
|
||||||
|
|| this.idResolvedGroupPaths.stream().anyMatch(parentGroupPaths::contains)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a specific group path matches any entry in a list of paths using either exact match or prefix matching.
|
||||||
|
*
|
||||||
|
* @param groupPath
|
||||||
|
* the path to check
|
||||||
|
* @param groupPaths
|
||||||
|
* the paths to check against
|
||||||
|
* @return {@code true} if the path matches one of the paths in exact match or prefix matching mode
|
||||||
|
*/
|
||||||
|
protected boolean groupPathOrTransitiveContained(final String groupPath, final Collection<String> groupPaths)
|
||||||
|
{
|
||||||
|
boolean contained = groupPaths.contains(groupPath);
|
||||||
|
final String groupPathPrefix = groupPath + "/";
|
||||||
|
contained = contained || groupPaths.stream().anyMatch(path -> path.startsWith(groupPathPrefix));
|
||||||
|
return contained;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityType;
|
||||||
|
import org.alfresco.util.PropertyMap;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This user synchronisation mapping processor maps the default Alfresco authority container properties from a Keycloak group.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class DefaultGroupProcessor implements GroupProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void mapGroup(final GroupRepresentation group, final NodeDescription groupNode)
|
||||||
|
{
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
final PropertyMap properties = groupNode.getProperties();
|
||||||
|
|
||||||
|
properties.put(ContentModel.PROP_AUTHORITY_NAME, AuthorityType.GROUP.getPrefixString() + group.getId());
|
||||||
|
properties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, group.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
import org.alfresco.util.PropertyMap;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This user synchronisation mapping processor maps the default Alfresco person properties from a Keycloak user.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class DefaultPersonProcessor implements UserProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
protected boolean mapNull;
|
||||||
|
|
||||||
|
protected boolean mapFirstName;
|
||||||
|
|
||||||
|
protected boolean mapLastName;
|
||||||
|
|
||||||
|
protected boolean mapEmail;
|
||||||
|
|
||||||
|
protected boolean mapEnabledState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapNull
|
||||||
|
* the mapNull to set
|
||||||
|
*/
|
||||||
|
public void setMapNull(final boolean mapNull)
|
||||||
|
{
|
||||||
|
this.mapNull = mapNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapFirstName
|
||||||
|
* the mapFirstName to set
|
||||||
|
*/
|
||||||
|
public void setMapFirstName(final boolean mapFirstName)
|
||||||
|
{
|
||||||
|
this.mapFirstName = mapFirstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapLastName
|
||||||
|
* the mapLastName to set
|
||||||
|
*/
|
||||||
|
public void setMapLastName(final boolean mapLastName)
|
||||||
|
{
|
||||||
|
this.mapLastName = mapLastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapEmail
|
||||||
|
* the mapEmail to set
|
||||||
|
*/
|
||||||
|
public void setMapEmail(final boolean mapEmail)
|
||||||
|
{
|
||||||
|
this.mapEmail = mapEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mapEnabledState
|
||||||
|
* the mapEnabledState to set
|
||||||
|
*/
|
||||||
|
public void setMapEnabledState(final boolean mapEnabledState)
|
||||||
|
{
|
||||||
|
this.mapEnabledState = mapEnabledState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void mapUser(final UserRepresentation user, final NodeDescription person)
|
||||||
|
{
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
final PropertyMap properties = person.getProperties();
|
||||||
|
|
||||||
|
if ((this.mapNull || user.getFirstName() != null) && this.mapFirstName)
|
||||||
|
{
|
||||||
|
properties.put(ContentModel.PROP_FIRSTNAME, user.getFirstName());
|
||||||
|
}
|
||||||
|
if ((this.mapNull || user.getLastName() != null) && this.mapLastName)
|
||||||
|
{
|
||||||
|
properties.put(ContentModel.PROP_LASTNAME, user.getLastName());
|
||||||
|
}
|
||||||
|
if ((this.mapNull || user.getEmail() != null) && this.mapEmail)
|
||||||
|
{
|
||||||
|
properties.put(ContentModel.PROP_EMAIL, user.getEmail());
|
||||||
|
}
|
||||||
|
if ((this.mapNull || user.isEnabled() != null) && this.mapEnabledState)
|
||||||
|
{
|
||||||
|
properties.put(ContentModel.PROP_ENABLED, user.isEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<QName> getMappedProperties()
|
||||||
|
{
|
||||||
|
Collection<QName> mappedProperties;
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
mappedProperties = new ArrayList<>(4);
|
||||||
|
if (this.mapFirstName)
|
||||||
|
{
|
||||||
|
mappedProperties.add(ContentModel.PROP_FIRSTNAME);
|
||||||
|
}
|
||||||
|
if (this.mapLastName)
|
||||||
|
{
|
||||||
|
mappedProperties.add(ContentModel.PROP_LASTNAME);
|
||||||
|
}
|
||||||
|
if (this.mapEmail)
|
||||||
|
{
|
||||||
|
mappedProperties.add(ContentModel.PROP_EMAIL);
|
||||||
|
}
|
||||||
|
if (this.mapEnabledState)
|
||||||
|
{
|
||||||
|
mappedProperties.add(ContentModel.PROP_ENABLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mappedProperties = Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedProperties;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides filter capabilities for groups to be synchronised based on their parent group and whether they are contained in
|
||||||
|
* specific groups.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class GroupContainmentGroupFilter extends BaseGroupContainmentFilter
|
||||||
|
implements GroupFilter
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(GroupContainmentGroupFilter.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean shouldIncludeGroup(final GroupRepresentation group)
|
||||||
|
{
|
||||||
|
boolean matches;
|
||||||
|
|
||||||
|
if ((this.groupPaths != null && !this.groupPaths.isEmpty()) || (this.groupIds != null && !this.groupIds.isEmpty()))
|
||||||
|
{
|
||||||
|
LOGGER.debug(
|
||||||
|
"Checking group {} ({}) for containment in groups with paths {} / IDs {}, using allowTransitive={} and requireAll={}",
|
||||||
|
group.getId(), group.getPath(), this.groupPaths, this.groupIds, this.allowTransitive, this.requireAll);
|
||||||
|
|
||||||
|
// no need to retrieve parent group ID as path should be sufficient
|
||||||
|
// Keycloak groups can only ever have one parent
|
||||||
|
|
||||||
|
final List<String> parentGroupIds = Collections.emptyList();
|
||||||
|
final List<String> parentGroupPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
final String groupPath = group.getPath();
|
||||||
|
final String parentPath = groupPath.substring(0, groupPath.lastIndexOf('/'));
|
||||||
|
if (!parentPath.isEmpty())
|
||||||
|
{
|
||||||
|
parentGroupPaths.add(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = this.parentGroupsMatch(parentGroupIds, parentGroupPaths);
|
||||||
|
|
||||||
|
LOGGER.debug("Group containment result for group {}: {}", group.getId(), matches);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides filter capabilities for users to be synchronised based on the groups they are a member of and whether they are
|
||||||
|
* contained in specific groups.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class GroupContainmentUserFilter extends BaseGroupContainmentFilter
|
||||||
|
implements UserFilter, InitializingBean
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(GroupContainmentUserFilter.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean shouldIncludeUser(final UserRepresentation user)
|
||||||
|
{
|
||||||
|
boolean matches;
|
||||||
|
|
||||||
|
if ((this.groupPaths != null && !this.groupPaths.isEmpty()) || (this.groupIds != null && !this.groupIds.isEmpty()))
|
||||||
|
{
|
||||||
|
LOGGER.debug("Checking user {} for containment in groups with paths {} / IDs {}, using allowTransitive={} and requireAll={}",
|
||||||
|
user.getUsername(), this.groupPaths, this.groupIds, this.allowTransitive, this.requireAll);
|
||||||
|
|
||||||
|
final List<String> parentGroupIds = new ArrayList<>();
|
||||||
|
final List<String> parentGroupPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
int processedGroups = 1;
|
||||||
|
while (processedGroups > 0)
|
||||||
|
{
|
||||||
|
processedGroups = this.idmClient.processUserGroups(user.getId(), offset, this.groupLoadBatchSize, group -> {
|
||||||
|
parentGroupIds.add(group.getId());
|
||||||
|
parentGroupPaths.add(group.getPath());
|
||||||
|
});
|
||||||
|
offset += processedGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = this.parentGroupsMatch(parentGroupIds, parentGroupPaths);
|
||||||
|
|
||||||
|
LOGGER.debug("Group containment result for user {}: {}", user.getUsername(), matches);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface are used to determine which groups should be synchronised. All instances of this interface in the Keycloak
|
||||||
|
* authentication subsystem will be consulted and only groups for which every filter returns {@code true} will be synchronised. If no filter
|
||||||
|
* instances have been defined, groups will always be synchronised without any filtering.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface GroupFilter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this group should be included in the synchronisation.
|
||||||
|
*
|
||||||
|
* @param group
|
||||||
|
* the group to consider
|
||||||
|
* @return {@code true} if the group should be synchronised, {@code false} if not
|
||||||
|
*/
|
||||||
|
boolean shouldIncludeGroup(GroupRepresentation group);
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface are to map data from Keycloak groups to the Alfresco authority container node description. All instances of
|
||||||
|
* this interface in the Keycloak authentication subsystem will be consulted in the order the beans are defined in the Spring application
|
||||||
|
* context, resulting in an aggregated authority container node description.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface GroupProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps data from a Keycloak group representation to a description of an Alfresco node for the authority container.
|
||||||
|
*
|
||||||
|
* @param group
|
||||||
|
* the Keycloak group representation
|
||||||
|
* @param groupNodeDescription
|
||||||
|
* the Alfresco node description
|
||||||
|
*/
|
||||||
|
void mapGroup(GroupRepresentation group, NodeDescription groupNodeDescription);
|
||||||
|
}
|
@@ -0,0 +1,531 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.AbstractCollection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
import org.alfresco.repo.security.sync.UserRegistry;
|
||||||
|
import org.alfresco.service.cmr.security.AuthorityType;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.alfresco.util.PropertyMap;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.client.IDMClient;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.client.IDMClientImpl;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides a Keycloak-based user registry to support synchronisation with Keycloak managed users and groups.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class KeycloakUserRegistry implements UserRegistry, InitializingBean, ActivateableBean, ApplicationContextAware
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakUserRegistry.class);
|
||||||
|
|
||||||
|
protected boolean active;
|
||||||
|
|
||||||
|
protected ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
protected IDMClient idmClient;
|
||||||
|
|
||||||
|
protected Collection<UserFilter> userFilters;
|
||||||
|
|
||||||
|
protected Collection<GroupFilter> groupFilters;
|
||||||
|
|
||||||
|
protected Collection<UserProcessor> userProcessors;
|
||||||
|
|
||||||
|
protected Collection<GroupProcessor> groupProcessors;
|
||||||
|
|
||||||
|
protected int personLoadBatchSize = 50;
|
||||||
|
|
||||||
|
protected int groupLoadBatchSize = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "applicationContext", this.applicationContext);
|
||||||
|
PropertyCheck.mandatory(this, "idmClient", this.idmClient);
|
||||||
|
|
||||||
|
this.userFilters = Collections.unmodifiableList(
|
||||||
|
new ArrayList<>(this.applicationContext.getBeansOfType(UserFilter.class, false, true).values()));
|
||||||
|
this.groupFilters = Collections.unmodifiableList(
|
||||||
|
new ArrayList<>(this.applicationContext.getBeansOfType(GroupFilter.class, false, true).values()));
|
||||||
|
this.userProcessors = Collections.unmodifiableList(
|
||||||
|
new ArrayList<>(this.applicationContext.getBeansOfType(UserProcessor.class, false, true).values()));
|
||||||
|
this.groupProcessors = Collections.unmodifiableList(
|
||||||
|
new ArrayList<>(this.applicationContext.getBeansOfType(GroupProcessor.class, false, true).values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
return this.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param active
|
||||||
|
* the active to set
|
||||||
|
*/
|
||||||
|
public void setActive(final boolean active)
|
||||||
|
{
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(final ApplicationContext applicationContext)
|
||||||
|
{
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param idmClient
|
||||||
|
* the idmClient to set
|
||||||
|
*/
|
||||||
|
public void setIdmClient(final IDMClientImpl idmClient)
|
||||||
|
{
|
||||||
|
this.idmClient = idmClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param personLoadBatchSize
|
||||||
|
* the personLoadBatchSize to set
|
||||||
|
*/
|
||||||
|
public void setPersonLoadBatchSize(final int personLoadBatchSize)
|
||||||
|
{
|
||||||
|
this.personLoadBatchSize = personLoadBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groupLoadBatchSize
|
||||||
|
* the groupLoadBatchSize to set
|
||||||
|
*/
|
||||||
|
public void setGroupLoadBatchSize(final int groupLoadBatchSize)
|
||||||
|
{
|
||||||
|
this.groupLoadBatchSize = groupLoadBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<NodeDescription> getPersons(final Date modifiedSince)
|
||||||
|
{
|
||||||
|
// Keycloak does not support any "modifiedSince" semantics
|
||||||
|
|
||||||
|
Collection<NodeDescription> people = Collections.emptyList();
|
||||||
|
|
||||||
|
if (this.active)
|
||||||
|
{
|
||||||
|
people = new UserCollection<>(this.personLoadBatchSize, this.idmClient.countUsers(), this::mapUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return people;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<NodeDescription> getGroups(final Date modifiedSince)
|
||||||
|
{
|
||||||
|
// Keycloak does not support any "modifiedSince" semantics
|
||||||
|
|
||||||
|
Collection<NodeDescription> groups = Collections.emptySet();
|
||||||
|
|
||||||
|
if (this.active)
|
||||||
|
{
|
||||||
|
groups = new GroupCollection<>(this.groupLoadBatchSize, this.idmClient.countGroups(), this::mapGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<String> getPersonNames()
|
||||||
|
{
|
||||||
|
Collection<String> personNames = Collections.emptySet();
|
||||||
|
|
||||||
|
if (this.active)
|
||||||
|
{
|
||||||
|
personNames = new UserCollection<>(this.personLoadBatchSize, this.idmClient.countUsers(), UserRepresentation::getUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
return personNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<String> getGroupNames()
|
||||||
|
{
|
||||||
|
Collection<String> groupNames = Collections.emptySet();
|
||||||
|
|
||||||
|
if (this.active)
|
||||||
|
{
|
||||||
|
groupNames = new GroupCollection<>(this.groupLoadBatchSize, this.idmClient.countGroups(),
|
||||||
|
group -> AuthorityType.GROUP.getPrefixString() + group.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Set<QName> getPersonMappedProperties()
|
||||||
|
{
|
||||||
|
final Set<QName> mappedProperties = new HashSet<>();
|
||||||
|
|
||||||
|
this.userProcessors.stream().map(UserProcessor::getMappedProperties).forEach(mappedProperties::addAll);
|
||||||
|
|
||||||
|
return mappedProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a single user from the Keycloak representation into an abstract description of a person node.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* the user to map
|
||||||
|
* @return the mapped person node description
|
||||||
|
*/
|
||||||
|
protected NodeDescription mapUser(final UserRepresentation user)
|
||||||
|
{
|
||||||
|
final NodeDescription person = new NodeDescription(user.getId());
|
||||||
|
final PropertyMap personProperties = person.getProperties();
|
||||||
|
|
||||||
|
LOGGER.debug("Mapping user {}", user.getUsername());
|
||||||
|
|
||||||
|
this.userProcessors.forEach(processor -> processor.mapUser(user, person));
|
||||||
|
|
||||||
|
// always wins against user-defined mappings for cm:userName
|
||||||
|
personProperties.put(ContentModel.PROP_USERNAME, user.getUsername());
|
||||||
|
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a single group from the Keycloak representation into an abstract description of a group node.
|
||||||
|
*
|
||||||
|
* @param group
|
||||||
|
* the group to map
|
||||||
|
* @return the mapped group node description
|
||||||
|
*/
|
||||||
|
protected NodeDescription mapGroup(final GroupRepresentation group)
|
||||||
|
{
|
||||||
|
// need to use group ID as unique name as Keycloak group name itself is non-unique
|
||||||
|
|
||||||
|
final NodeDescription groupD = new NodeDescription(group.getId());
|
||||||
|
final PropertyMap groupProperties = groupD.getProperties();
|
||||||
|
|
||||||
|
final String groupName = AuthorityType.GROUP.getPrefixString() + group.getId();
|
||||||
|
LOGGER.debug("Mapping group {}", groupName);
|
||||||
|
|
||||||
|
this.groupProcessors.forEach(processor -> processor.mapGroup(group, groupD));
|
||||||
|
|
||||||
|
// always wins against user-defined mappings for cm:authorityName
|
||||||
|
groupProperties.put(ContentModel.PROP_AUTHORITY_NAME, groupName);
|
||||||
|
|
||||||
|
final Set<String> childAssociations = groupD.getChildAssociations();
|
||||||
|
group.getSubGroups().stream()
|
||||||
|
.filter(subGroup -> !this.groupFilters.stream().anyMatch(filter -> !filter.shouldIncludeGroup(subGroup)))
|
||||||
|
.forEach(subGroup -> childAssociations.add(AuthorityType.GROUP.getPrefixString() + subGroup.getId()));
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
int processedMembers = 1;
|
||||||
|
while (processedMembers > 0)
|
||||||
|
{
|
||||||
|
processedMembers = this.idmClient.processMembers(group.getId(), offset, this.personLoadBatchSize, user -> {
|
||||||
|
final boolean skipSync = this.userFilters.stream().anyMatch(filter -> !filter.shouldIncludeUser(user));
|
||||||
|
if (!skipSync)
|
||||||
|
{
|
||||||
|
childAssociations.add(user.getUsername());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
offset += processedMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.debug("Mapped members of group {}: {}", groupName, childAssociations);
|
||||||
|
|
||||||
|
return groupD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides common basic functionalities for a collection of Keycloak authority-based data elements, supporting basic batch
|
||||||
|
* load-based pagination / iterative traversal.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
protected abstract class KeycloakAuthorityCollection<T, AR> extends AbstractCollection<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
protected final int batchSize;
|
||||||
|
|
||||||
|
protected final int totalUpperBound;
|
||||||
|
|
||||||
|
protected final Function<AR, T> mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class.
|
||||||
|
*
|
||||||
|
* @param batchSize
|
||||||
|
* the size of batches to use for incrementally loading data elements in the iterator
|
||||||
|
* @param totalUpperBound
|
||||||
|
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
|
||||||
|
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
|
||||||
|
* @param mapper
|
||||||
|
* the mapping handler to turn a low-level authority representation into the actual collection value representation
|
||||||
|
*/
|
||||||
|
protected KeycloakAuthorityCollection(final int batchSize, final int totalUpperBound, final Function<AR, T> mapper)
|
||||||
|
{
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
this.totalUpperBound = totalUpperBound;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int size()
|
||||||
|
{
|
||||||
|
return this.totalUpperBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Iterator<T> iterator()
|
||||||
|
{
|
||||||
|
return new KeycloakAuthorityIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the next batch of authority representations.
|
||||||
|
*
|
||||||
|
* @param offset
|
||||||
|
* the index of the first low-level authority to load
|
||||||
|
* @param batchSize
|
||||||
|
* the maximum number of low-level authorities to load from the backend
|
||||||
|
* @param authorityProcessor
|
||||||
|
* the processor to consume individual authority representations - the number of representations passed to this processor
|
||||||
|
* may be different than the number of authorities loaded from the backend due to filtering and potential pre-processing
|
||||||
|
* (e.g. splitting of groups and sub-groups)
|
||||||
|
* @return the number of low-level authorities loaded in this batch to properly adjust the offset for the next load operation
|
||||||
|
*/
|
||||||
|
protected abstract int loadNext(int offset, int batchSize, Consumer<AR> authorityProcessor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an authority representation into the type of object to be exposed as values of the collection.
|
||||||
|
*
|
||||||
|
* @param authorityRepresentation
|
||||||
|
* the authority representation to convert
|
||||||
|
* @return the converted value
|
||||||
|
*/
|
||||||
|
protected T convert(final AR authorityRepresentation)
|
||||||
|
{
|
||||||
|
return this.mapper.apply(authorityRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class KeycloakAuthorityIterator implements Iterator<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
private final List<T> buffer = new ArrayList<>();
|
||||||
|
|
||||||
|
private int offset;
|
||||||
|
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
private boolean noMoreResults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized boolean hasNext()
|
||||||
|
{
|
||||||
|
this.checkAndFillBuffer();
|
||||||
|
|
||||||
|
final boolean hasNext = !this.buffer.isEmpty() && this.index < this.buffer.size();
|
||||||
|
return hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized T next()
|
||||||
|
{
|
||||||
|
this.checkAndFillBuffer();
|
||||||
|
|
||||||
|
T next;
|
||||||
|
if (!this.buffer.isEmpty() && this.index < this.buffer.size())
|
||||||
|
{
|
||||||
|
next = this.buffer.get(this.index++);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized void checkAndFillBuffer()
|
||||||
|
{
|
||||||
|
if ((this.buffer.isEmpty() || this.index >= this.buffer.size()) && !this.noMoreResults)
|
||||||
|
{
|
||||||
|
this.buffer.clear();
|
||||||
|
|
||||||
|
this.index = 0;
|
||||||
|
this.offset += KeycloakAuthorityCollection.this.loadNext(this.offset, KeycloakAuthorityCollection.this.batchSize,
|
||||||
|
authority -> this.buffer.add(KeycloakAuthorityCollection.this.convert(authority)));
|
||||||
|
|
||||||
|
this.noMoreResults = this.buffer.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides the basis for all user-related collections.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
protected class UserCollection<T> extends KeycloakAuthorityCollection<T, UserRepresentation>
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class.
|
||||||
|
*
|
||||||
|
* @param batchSize
|
||||||
|
* the size of batches to use for incrementally loading data elements in the iterator
|
||||||
|
* @param totalUpperBound
|
||||||
|
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
|
||||||
|
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
|
||||||
|
* @param mapper
|
||||||
|
* the mapping handler to turn a low-level authority representation into the actual collection value representation
|
||||||
|
*/
|
||||||
|
public UserCollection(final int batchSize, final int totalUpperBound, final Function<UserRepresentation, T> mapper)
|
||||||
|
{
|
||||||
|
super(batchSize, totalUpperBound, mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int loadNext(final int offset, final int batchSize, final Consumer<UserRepresentation> authorityProcessor)
|
||||||
|
{
|
||||||
|
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
|
||||||
|
// How to count totals in advance though?
|
||||||
|
return KeycloakUserRegistry.this.idmClient.processUsers(offset, batchSize, user -> {
|
||||||
|
final boolean skipSync = KeycloakUserRegistry.this.userFilters.stream().anyMatch(filter -> !filter.shouldIncludeUser(user));
|
||||||
|
if (!skipSync)
|
||||||
|
{
|
||||||
|
authorityProcessor.accept(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides the basis for all group-related collections.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
protected class GroupCollection<T> extends KeycloakAuthorityCollection<T, GroupRepresentation>
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class.
|
||||||
|
*
|
||||||
|
* @param batchSize
|
||||||
|
* the size of batches to use for incrementally loading data elements in the iterator
|
||||||
|
* @param totalUpperBound
|
||||||
|
* the upper bound of the total number of elements to expect in this collection - this is just an estimation (without
|
||||||
|
* adjusting for any potential filtering) and will be used as the {@link #size() collection's size}.
|
||||||
|
* @param mapper
|
||||||
|
* the mapping handler to turn a low-level authority representation into the actual collection value representation
|
||||||
|
*/
|
||||||
|
public GroupCollection(final int batchSize, final int totalUpperBound, final Function<GroupRepresentation, T> mapper)
|
||||||
|
{
|
||||||
|
super(batchSize, totalUpperBound, mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int loadNext(final int offset, final int batchSize, final Consumer<GroupRepresentation> authorityProcessor)
|
||||||
|
{
|
||||||
|
// TODO Evaluate other iteration approaches, e.g. crawling from a configured root group
|
||||||
|
// How to count totals in advance though?
|
||||||
|
return KeycloakUserRegistry.this.idmClient.processGroups(offset, batchSize, group -> {
|
||||||
|
this.processGroupsRecursively(group, authorityProcessor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void processGroupsRecursively(final GroupRepresentation group, final Consumer<GroupRepresentation> authorityProcessor)
|
||||||
|
{
|
||||||
|
final boolean skipSync = KeycloakUserRegistry.this.groupFilters.stream().anyMatch(filter -> !filter.shouldIncludeGroup(group));
|
||||||
|
if (!skipSync)
|
||||||
|
{
|
||||||
|
authorityProcessor.accept(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
// any filtering applied above does not apply here as any sub-group will be individually checked for filtering by recursive
|
||||||
|
// processing
|
||||||
|
group.getSubGroups().forEach(subGroup -> this.processGroupsRecursively(subGroup, authorityProcessor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class perform simple mappings from Keycloak group attributes to authority container node description properties.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class SimpleGroupAttributeProcessor extends BaseAttributeProcessor
|
||||||
|
implements GroupProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void mapGroup(final GroupRepresentation group, final NodeDescription groupNode)
|
||||||
|
{
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
this.map(group.getAttributes(), groupNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class perform simple mappings from Keycloak user attributes to person node description properties.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class SimpleUserAttributeProcessor extends BaseAttributeProcessor
|
||||||
|
implements UserProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
protected boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param enabled
|
||||||
|
* the enabled to set
|
||||||
|
*/
|
||||||
|
public void setEnabled(final boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void mapUser(final UserRepresentation user, final NodeDescription person)
|
||||||
|
{
|
||||||
|
if (this.enabled)
|
||||||
|
{
|
||||||
|
this.map(user.getAttributes(), person);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<QName> getMappedProperties()
|
||||||
|
{
|
||||||
|
return this.enabled ? new HashSet<>(this.attributePropertyQNameMappings.values()) : Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface are used to determine which users should be synchronised. All instances of this interface in the Keycloak
|
||||||
|
* authentication subsystem will be consulted and only users for which every filter returns {@code true} will be synchronised. If no filter
|
||||||
|
* instances have been defined, users will always be synchronised without any filtering.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface UserFilter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether this user should be included in the synchronisation.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* the user to consider
|
||||||
|
* @return {@code true} if the user should be synchronised, {@code false} if not
|
||||||
|
*/
|
||||||
|
boolean shouldIncludeUser(UserRepresentation user);
|
||||||
|
}
|
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.sync;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.sync.NodeDescription;
|
||||||
|
import org.alfresco.service.namespace.QName;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface are used to map data from Keycloak users to the Alfresco person node description. All instances of this
|
||||||
|
* interface in the Keycloak authentication subsystem will be consulted in the order the beans are defined in the Spring application
|
||||||
|
* context, resulting in an aggregated person node description.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface UserProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps data from a Keycloak user representation to a description of an Alfresco node for the person.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* the Keycloak user representation
|
||||||
|
* @param personNodeDescription
|
||||||
|
* the Alfresco node description
|
||||||
|
*/
|
||||||
|
void mapUser(UserRepresentation user, NodeDescription personNodeDescription);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the set of properties mapped by this instance.
|
||||||
|
*
|
||||||
|
* @return the set of person node properties mapped by this instance
|
||||||
|
*/
|
||||||
|
Collection<QName> getMappedProperties();
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.util;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class bundles static utility operations meant to bridge slightly different APIs of Alfresco versions, so that the Keycloak module
|
||||||
|
* can run on
|
||||||
|
* in as many Alfresco versions as possible.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AlfrescoCompatibilityUtil
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AlfrescoCompatibilityUtil.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Masks the user name except for the first two characters. This method has been ported from 6.1+ Alfresco AuthenticationUtil for use in
|
||||||
|
* any Alfresco version.
|
||||||
|
*
|
||||||
|
* @param userName
|
||||||
|
* the user name to mask
|
||||||
|
* @return the masked user name
|
||||||
|
*/
|
||||||
|
public static String maskUsername(final String userName)
|
||||||
|
{
|
||||||
|
if (userName != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (userName.length() >= 2)
|
||||||
|
{
|
||||||
|
return userName.substring(0, 2) + new String(new char[(userName.length() - 2)]).replace("\0", "*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception e)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Failed to mask the username", e);
|
||||||
|
}
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package de.acosix.alfresco.keycloak.repo.util;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.alfresco.util.ParameterCheck;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.common.util.Time;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessToken;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.AccessTokenResponse;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.deps.keycloak.representations.IDToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class encapsulate an access token with its associated refresh data.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class RefreshableAccessTokenHolder implements Serializable
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -3230026569734591820L;
|
||||||
|
|
||||||
|
protected final AccessToken accessToken;
|
||||||
|
|
||||||
|
protected final IDToken idToken;
|
||||||
|
|
||||||
|
protected final String token;
|
||||||
|
|
||||||
|
protected final String refreshToken;
|
||||||
|
|
||||||
|
protected final int refreshExpiration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class from an access token response, typically from an initial authentication or token refresh
|
||||||
|
*
|
||||||
|
* @param tokenResponse
|
||||||
|
* the response to a request for an access token
|
||||||
|
* @param verifiedTokens
|
||||||
|
* the token wrapper from the response verification step any client should do before constructing a new instance of this
|
||||||
|
* class
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder(final AccessTokenResponse tokenResponse, final VerifiedTokens verifiedTokens)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("tokenResponse", tokenResponse);
|
||||||
|
ParameterCheck.mandatory("verifiedTokens", verifiedTokens);
|
||||||
|
|
||||||
|
this.accessToken = verifiedTokens.getAccessToken();
|
||||||
|
this.idToken = verifiedTokens.getIdToken();
|
||||||
|
|
||||||
|
this.token = tokenResponse.getToken();
|
||||||
|
this.refreshToken = tokenResponse.getRefreshToken();
|
||||||
|
this.refreshExpiration = Time.currentTime() + (int) tokenResponse.getRefreshExpiresIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class from details exposed by Keycloak servlet adapter APIs. Since these APIs do not provide some
|
||||||
|
* access to token response details, this constructor assumes that the refresh token is valid for at least 1/100th the duration of the
|
||||||
|
* overall access token.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token
|
||||||
|
* @param idToken
|
||||||
|
* the ID token
|
||||||
|
* @param token
|
||||||
|
* the textual representation of the access token
|
||||||
|
* @param refreshToken
|
||||||
|
* the textual representation of the refresh token
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder(final AccessToken accessToken, final IDToken idToken, final String token, final String refreshToken)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("accessToken", accessToken);
|
||||||
|
ParameterCheck.mandatory("idToken", idToken);
|
||||||
|
ParameterCheck.mandatoryString("token", token);
|
||||||
|
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.idToken = idToken;
|
||||||
|
|
||||||
|
this.token = token;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
// no explicit refresh expiration, so assume validity period is 1/100th
|
||||||
|
this.refreshExpiration = Time.currentTime() - (accessToken.getExpiration() - Time.currentTime()) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token has expired.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the access token as expired, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean isExpired()
|
||||||
|
{
|
||||||
|
final boolean isExpired = this.accessToken.getExpiration() < Time.currentTime();
|
||||||
|
return isExpired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token can be refreshed.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the token can be refreshed, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean canRefresh()
|
||||||
|
{
|
||||||
|
final boolean canRefresh = this.refreshToken != null && this.refreshExpiration > Time.currentTime();
|
||||||
|
return canRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the encapsulated access token should be refreshed.
|
||||||
|
*
|
||||||
|
* @param minTokenTTL
|
||||||
|
* the minimum time-to-live remaining before a token needs to be refreshed
|
||||||
|
*
|
||||||
|
* @return {@code true} if the token should be refreshed, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean shouldRefresh(final int minTokenTTL)
|
||||||
|
{
|
||||||
|
final boolean shouldRefresh = this.refreshToken != null && this.accessToken.getExpiration() - minTokenTTL < Time.currentTime();
|
||||||
|
return shouldRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the token
|
||||||
|
*/
|
||||||
|
public String getToken()
|
||||||
|
{
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the refreshToken
|
||||||
|
*/
|
||||||
|
public String getRefreshToken()
|
||||||
|
{
|
||||||
|
return this.refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the access token
|
||||||
|
*/
|
||||||
|
public AccessToken getAccessToken()
|
||||||
|
{
|
||||||
|
return this.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the idToken
|
||||||
|
*/
|
||||||
|
public IDToken getIdToken()
|
||||||
|
{
|
||||||
|
return this.idToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -5,6 +5,7 @@ COPY maven ${docker.tests.repositoryWebappPath}
|
|||||||
RUN echo "" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
RUN echo "" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||||
&& echo "#MergeGlobalProperties" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
&& echo "#MergeGlobalProperties" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||||
&& sed -i '/#MergeGlobalProperties/r ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/alfresco-global.addition.properties' ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
&& sed -i '/#MergeGlobalProperties/r ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/alfresco-global.addition.properties' ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||||
|
&& sed -i 's/<secure>true<\/secure>/<secure>false<\/secure>/' $CATALINA_HOME/conf/web.xml \
|
||||||
&& mv ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/entrypoint.sh $CATALINA_HOME/bin/ \
|
&& mv ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/entrypoint.sh $CATALINA_HOME/bin/ \
|
||||||
&& chmod +x $CATALINA_HOME/bin/entrypoint.sh
|
&& chmod +x $CATALINA_HOME/bin/entrypoint.sh
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright 2019 Acosix GmbH
|
# Copyright 2019 - 2020 Acosix GmbH
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,4 +22,7 @@ keycloak.adapter.auth-server-url=http://${docker.tests.host.name}:${docker.tests
|
|||||||
keycloak.adapter.realm=test
|
keycloak.adapter.realm=test
|
||||||
keycloak.adapter.resource=alfresco
|
keycloak.adapter.resource=alfresco
|
||||||
keycloak.adapter.credentials.provider=secret
|
keycloak.adapter.credentials.provider=secret
|
||||||
keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
|
keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
|
||||||
|
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright 2019 Acosix GmbH
|
# Copyright 2019 - 2020 Acosix GmbH
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -75,8 +75,7 @@
|
|||||||
<dependencySet>
|
<dependencySet>
|
||||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
<!-- everything else from Keycloak should already be bundled in Alfresco 6.x -->
|
<include>${project.groupId}:${project.artifactId}.deps:*</include>
|
||||||
<include>org.keycloak:keycloak-servlet-filter-adapter:*</include>
|
|
||||||
</includes>
|
</includes>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependencySet>
|
</dependencySet>
|
||||||
|
@@ -1,98 +1,195 @@
|
|||||||
{
|
{
|
||||||
"id": "test",
|
"id": "test",
|
||||||
"realm": "test",
|
"realm": "test",
|
||||||
"users": [
|
"groups": [{
|
||||||
{
|
"name": "Test A",
|
||||||
"id": "mustermann",
|
"subGroups": [{
|
||||||
"username": "mmustermann",
|
"name": "Test AA"
|
||||||
"enabled": true,
|
}, {
|
||||||
"email": "max.mustermann@muster.com",
|
"name": "Test AB"
|
||||||
"firstName": "Max",
|
}]
|
||||||
"lastName": "Mustermann",
|
}, {
|
||||||
"credentials": [
|
"name": "Test B",
|
||||||
{
|
"subGroups": [{
|
||||||
"type": "password",
|
"name": "Test BA"
|
||||||
"value": "mmustermann"
|
}]
|
||||||
}
|
}],
|
||||||
|
"users": [{
|
||||||
|
"id": "service-account-alfresco",
|
||||||
|
"serviceAccountClientId": "alfresco",
|
||||||
|
"username": "service-account-alfresco",
|
||||||
|
"enabled": true,
|
||||||
|
"email": "service-account-alfresco@muster.com",
|
||||||
|
"realmRoles": [
|
||||||
|
"offline_access",
|
||||||
|
"uma_authorization"
|
||||||
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account"
|
||||||
],
|
],
|
||||||
"realmRoles": [
|
"realm-management": [
|
||||||
"user"
|
"view-users"
|
||||||
],
|
]
|
||||||
"clientRoles": {
|
|
||||||
"account": [
|
|
||||||
"view-profile",
|
|
||||||
"manage-account"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
}, {
|
||||||
"clients": [
|
"id": "mmustermann",
|
||||||
{
|
"username": "mmustermann",
|
||||||
"clientId": "alfresco",
|
"enabled": true,
|
||||||
"name": "Alfresco Repository",
|
"email": "max.mustermann@muster.com",
|
||||||
"rootUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco",
|
"firstName": "Max",
|
||||||
"adminUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco/keycloak",
|
"lastName": "Mustermann",
|
||||||
"baseUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco",
|
"credentials": [{
|
||||||
"surrogateAuthRequired": false,
|
"type": "password",
|
||||||
"enabled": true,
|
"value": "mmustermann"
|
||||||
"clientAuthenticatorType": "client-secret",
|
}],
|
||||||
"secret" : "6f70a28f-98cd-41ca-8f2f-368a8797d708",
|
"realmRoles": [
|
||||||
"redirectUris": [
|
"user"
|
||||||
"http://localhost:${docker.tests.repositoryPort}/alfresco/*"
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"groups": [
|
||||||
|
"/Test A/Test AB",
|
||||||
|
"/Test B/Test BA"
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"id": "jdoe",
|
||||||
|
"username": "jdoe",
|
||||||
|
"enabled": true,
|
||||||
|
"email": "john.doe@muster.com",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"credentials": [{
|
||||||
|
"type": "password",
|
||||||
|
"value": "jdoe"
|
||||||
|
}],
|
||||||
|
"realmRoles": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"id": "ssuper",
|
||||||
|
"username": "ssuper",
|
||||||
|
"enabled": true,
|
||||||
|
"email": "suzy.super@muster.com",
|
||||||
|
"firstName": "Suzy",
|
||||||
|
"lastName": "Super",
|
||||||
|
"credentials": [{
|
||||||
|
"type": "password",
|
||||||
|
"value": "ssuper"
|
||||||
|
}],
|
||||||
|
"realmRoles": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"clientRoles": {
|
||||||
|
"account": [
|
||||||
|
"view-profile",
|
||||||
|
"manage-account"
|
||||||
],
|
],
|
||||||
"webOrigins": [
|
"alfresco": [
|
||||||
"http://localhost:${docker.tests.repositoryPort}"
|
"admin"
|
||||||
],
|
]
|
||||||
"notBefore": 0,
|
}
|
||||||
"bearerOnly": false,
|
}],
|
||||||
"consentRequired": false,
|
"roles": {
|
||||||
"standardFlowEnabled": true,
|
"client": {
|
||||||
"implicitFlowEnabled": false,
|
"alfresco": [{
|
||||||
"directAccessGrantsEnabled": true,
|
"id": "57944d14-7240-464b-925d-7778fa9b78e6",
|
||||||
"serviceAccountsEnabled": false,
|
"name": "admin",
|
||||||
"publicClient": false,
|
"composite": false,
|
||||||
"frontchannelLogout": false,
|
"clientRole": true,
|
||||||
|
"attributes": {}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clients": [{
|
||||||
|
"clientId": "alfresco",
|
||||||
|
"name": "Alfresco Repository",
|
||||||
|
"rootUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco",
|
||||||
|
"adminUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco/keycloak",
|
||||||
|
"baseUrl": "http://localhost:${docker.tests.repositoryPort}/alfresco",
|
||||||
|
"surrogateAuthRequired": false,
|
||||||
|
"enabled": true,
|
||||||
|
"clientAuthenticatorType": "client-secret",
|
||||||
|
"secret": "6f70a28f-98cd-41ca-8f2f-368a8797d708",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:${docker.tests.repositoryPort}/alfresco/*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"http://localhost:${docker.tests.repositoryPort}"
|
||||||
|
],
|
||||||
|
"notBefore": 0,
|
||||||
|
"bearerOnly": false,
|
||||||
|
"consentRequired": false,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"serviceAccountsEnabled": true,
|
||||||
|
"publicClient": false,
|
||||||
|
"frontchannelLogout": false,
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"saml.assertion.signature": "false",
|
||||||
|
"saml.force.post.binding": "false",
|
||||||
|
"saml.multivalued.roles": "false",
|
||||||
|
"saml.encrypt": "false",
|
||||||
|
"saml.server.signature": "false",
|
||||||
|
"saml.server.signature.keyinfo.ext": "false",
|
||||||
|
"exclude.session.state.from.auth.response": "false",
|
||||||
|
"saml_force_name_id_format": "false",
|
||||||
|
"saml.client.signature": "false",
|
||||||
|
"tls.client.certificate.bound.access.tokens": "false",
|
||||||
|
"saml.authnstatement": "false",
|
||||||
|
"display.on.consent.screen": "false",
|
||||||
|
"saml.onetimeuse.condition": "false"
|
||||||
|
},
|
||||||
|
"authenticationFlowBindingOverrides": {
|
||||||
|
|
||||||
|
},
|
||||||
|
"fullScopeAllowed": true,
|
||||||
|
"nodeReRegistrationTimeout": -1,
|
||||||
|
"protocolMappers": [{
|
||||||
|
"name": "groups",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"attributes": {
|
"protocolMapper": "oidc-group-membership-mapper",
|
||||||
"saml.assertion.signature": "false",
|
"consentRequired": false,
|
||||||
"saml.force.post.binding": "false",
|
"config": {
|
||||||
"saml.multivalued.roles": "false",
|
"full.path": "true",
|
||||||
"saml.encrypt": "false",
|
"id.token.claim": "true",
|
||||||
"saml.server.signature": "false",
|
"access.token.claim": "true",
|
||||||
"saml.server.signature.keyinfo.ext": "false",
|
"claim.name": "groups",
|
||||||
"exclude.session.state.from.auth.response": "false",
|
"userinfo.token.claim": "true"
|
||||||
"saml_force_name_id_format": "false",
|
|
||||||
"saml.client.signature": "false",
|
|
||||||
"tls.client.certificate.bound.access.tokens": "false",
|
|
||||||
"saml.authnstatement": "false",
|
|
||||||
"display.on.consent.screen": "false",
|
|
||||||
"saml.onetimeuse.condition": "false"
|
|
||||||
},
|
|
||||||
"authenticationFlowBindingOverrides": {
|
|
||||||
|
|
||||||
},
|
|
||||||
"fullScopeAllowed": true,
|
|
||||||
"nodeReRegistrationTimeout": -1,
|
|
||||||
"defaultClientScopes": [
|
|
||||||
"web-origins",
|
|
||||||
"role_list",
|
|
||||||
"roles",
|
|
||||||
"profile",
|
|
||||||
"email"
|
|
||||||
],
|
|
||||||
"optionalClientScopes": [
|
|
||||||
"address",
|
|
||||||
"phone",
|
|
||||||
"offline_access",
|
|
||||||
"microprofile-jwt"
|
|
||||||
],
|
|
||||||
"access": {
|
|
||||||
"view": true,
|
|
||||||
"configure": true,
|
|
||||||
"manage": true
|
|
||||||
}
|
}
|
||||||
|
}],
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"web-origins",
|
||||||
|
"role_list",
|
||||||
|
"roles",
|
||||||
|
"profile",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"address",
|
||||||
|
"phone",
|
||||||
|
"offline_access",
|
||||||
|
"microprofile-jwt"
|
||||||
|
],
|
||||||
|
"access": {
|
||||||
|
"view": true,
|
||||||
|
"configure": true,
|
||||||
|
"manage": true
|
||||||
}
|
}
|
||||||
],
|
}],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"revokeRefreshToken": false,
|
"revokeRefreshToken": false,
|
||||||
"refreshTokenMaxReuse": 0,
|
"refreshTokenMaxReuse": 0,
|
||||||
@@ -145,16 +242,13 @@
|
|||||||
"FreeOTP",
|
"FreeOTP",
|
||||||
"Google Authenticator"
|
"Google Authenticator"
|
||||||
],
|
],
|
||||||
"scopeMappings": [
|
"scopeMappings": [{
|
||||||
{
|
"clientScope": "offline_access",
|
||||||
"clientScope": "offline_access",
|
"roles": [
|
||||||
"roles": [
|
"offline_access"
|
||||||
"offline_access"
|
]
|
||||||
]
|
}],
|
||||||
}
|
"clientScopes": [{
|
||||||
],
|
|
||||||
"clientScopes": [
|
|
||||||
{
|
|
||||||
"id": "0ee41513-079a-4156-9eab-5709b18be2a6",
|
"id": "0ee41513-079a-4156-9eab-5709b18be2a6",
|
||||||
"name": "address",
|
"name": "address",
|
||||||
"description": "OpenID Connect built-in scope: address",
|
"description": "OpenID Connect built-in scope: address",
|
||||||
@@ -164,26 +258,24 @@
|
|||||||
"display.on.consent.screen": "true",
|
"display.on.consent.screen": "true",
|
||||||
"consent.screen.text": "${addressScopeConsentText}"
|
"consent.screen.text": "${addressScopeConsentText}"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
"id": "1218801d-c159-4ddd-901c-2fc5afa06170",
|
||||||
"id": "1218801d-c159-4ddd-901c-2fc5afa06170",
|
"name": "address",
|
||||||
"name": "address",
|
"protocol": "openid-connect",
|
||||||
"protocol": "openid-connect",
|
"protocolMapper": "oidc-address-mapper",
|
||||||
"protocolMapper": "oidc-address-mapper",
|
"consentRequired": false,
|
||||||
"consentRequired": false,
|
"config": {
|
||||||
"config": {
|
"user.attribute.formatted": "formatted",
|
||||||
"user.attribute.formatted": "formatted",
|
"user.attribute.country": "country",
|
||||||
"user.attribute.country": "country",
|
"user.attribute.postal_code": "postal_code",
|
||||||
"user.attribute.postal_code": "postal_code",
|
"userinfo.token.claim": "true",
|
||||||
"userinfo.token.claim": "true",
|
"user.attribute.street": "street",
|
||||||
"user.attribute.street": "street",
|
"id.token.claim": "true",
|
||||||
"id.token.claim": "true",
|
"user.attribute.region": "region",
|
||||||
"user.attribute.region": "region",
|
"access.token.claim": "true",
|
||||||
"access.token.claim": "true",
|
"user.attribute.locality": "locality"
|
||||||
"user.attribute.locality": "locality"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bac1ffb3-92bf-481d-b6f8-8dec9bbe7291",
|
"id": "bac1ffb3-92bf-481d-b6f8-8dec9bbe7291",
|
||||||
@@ -195,8 +287,7 @@
|
|||||||
"display.on.consent.screen": "true",
|
"display.on.consent.screen": "true",
|
||||||
"consent.screen.text": "${emailScopeConsentText}"
|
"consent.screen.text": "${emailScopeConsentText}"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
|
||||||
"id": "4f28826f-46c6-4fe9-b499-611b66d9bc6f",
|
"id": "4f28826f-46c6-4fe9-b499-611b66d9bc6f",
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
@@ -237,8 +328,7 @@
|
|||||||
"include.in.token.scope": "true",
|
"include.in.token.scope": "true",
|
||||||
"display.on.consent.screen": "false"
|
"display.on.consent.screen": "false"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
|
||||||
"id": "a69c70ca-70c0-465b-8faf-fffd427f90d9",
|
"id": "a69c70ca-70c0-465b-8faf-fffd427f90d9",
|
||||||
"name": "groups",
|
"name": "groups",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
@@ -291,8 +381,7 @@
|
|||||||
"display.on.consent.screen": "true",
|
"display.on.consent.screen": "true",
|
||||||
"consent.screen.text": "${phoneScopeConsentText}"
|
"consent.screen.text": "${phoneScopeConsentText}"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
|
||||||
"id": "850fe82e-ea0b-4cda-a0bc-dcc9d355c5a8",
|
"id": "850fe82e-ea0b-4cda-a0bc-dcc9d355c5a8",
|
||||||
"name": "phone number verified",
|
"name": "phone number verified",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
@@ -334,8 +423,7 @@
|
|||||||
"display.on.consent.screen": "true",
|
"display.on.consent.screen": "true",
|
||||||
"consent.screen.text": "${profileScopeConsentText}"
|
"consent.screen.text": "${profileScopeConsentText}"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
|
||||||
"id": "da5905e2-00ee-4ed0-ab7d-cdac7ead6206",
|
"id": "da5905e2-00ee-4ed0-ab7d-cdac7ead6206",
|
||||||
"name": "zoneinfo",
|
"name": "zoneinfo",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
@@ -553,20 +641,18 @@
|
|||||||
"consent.screen.text": "${samlRoleListScopeConsentText}",
|
"consent.screen.text": "${samlRoleListScopeConsentText}",
|
||||||
"display.on.consent.screen": "true"
|
"display.on.consent.screen": "true"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
"id": "51899037-a3df-4924-bd73-81f07cfb3aa9",
|
||||||
"id": "51899037-a3df-4924-bd73-81f07cfb3aa9",
|
"name": "role list",
|
||||||
"name": "role list",
|
"protocol": "saml",
|
||||||
"protocol": "saml",
|
"protocolMapper": "saml-role-list-mapper",
|
||||||
"protocolMapper": "saml-role-list-mapper",
|
"consentRequired": false,
|
||||||
"consentRequired": false,
|
"config": {
|
||||||
"config": {
|
"single": "false",
|
||||||
"single": "false",
|
"attribute.nameformat": "Basic",
|
||||||
"attribute.nameformat": "Basic",
|
"attribute.name": "Role"
|
||||||
"attribute.name": "Role"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4c15f94e-f490-4541-8c14-7b4734d6999b",
|
"id": "4c15f94e-f490-4541-8c14-7b4734d6999b",
|
||||||
@@ -578,15 +664,14 @@
|
|||||||
"display.on.consent.screen": "true",
|
"display.on.consent.screen": "true",
|
||||||
"consent.screen.text": "${rolesScopeConsentText}"
|
"consent.screen.text": "${rolesScopeConsentText}"
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
|
||||||
"id": "1908b63f-1be6-4f87-a947-30ee66721d05",
|
"id": "1908b63f-1be6-4f87-a947-30ee66721d05",
|
||||||
"name": "audience resolve",
|
"name": "audience resolve",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"protocolMapper": "oidc-audience-resolve-mapper",
|
"protocolMapper": "oidc-audience-resolve-mapper",
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -629,18 +714,16 @@
|
|||||||
"display.on.consent.screen": "false",
|
"display.on.consent.screen": "false",
|
||||||
"consent.screen.text": ""
|
"consent.screen.text": ""
|
||||||
},
|
},
|
||||||
"protocolMappers": [
|
"protocolMappers": [{
|
||||||
{
|
"id": "4652b835-1693-45ae-bad5-b61b534610de",
|
||||||
"id": "4652b835-1693-45ae-bad5-b61b534610de",
|
"name": "allowed web origins",
|
||||||
"name": "allowed web origins",
|
"protocol": "openid-connect",
|
||||||
"protocol": "openid-connect",
|
"protocolMapper": "oidc-allowed-origins-mapper",
|
||||||
"protocolMapper": "oidc-allowed-origins-mapper",
|
"consentRequired": false,
|
||||||
"consentRequired": false,
|
"config": {
|
||||||
"config": {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultDefaultClientScopes": [
|
"defaultDefaultClientScopes": [
|
||||||
@@ -666,7 +749,7 @@
|
|||||||
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
||||||
},
|
},
|
||||||
"smtpServer": {
|
"smtpServer": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"eventsEnabled": false,
|
"eventsEnabled": false,
|
||||||
"eventsListeners": [
|
"eventsListeners": [
|
||||||
@@ -676,14 +759,13 @@
|
|||||||
"adminEventsEnabled": false,
|
"adminEventsEnabled": false,
|
||||||
"adminEventsDetailsEnabled": false,
|
"adminEventsDetailsEnabled": false,
|
||||||
"components": {
|
"components": {
|
||||||
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
|
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [{
|
||||||
{
|
|
||||||
"id": "43a864c4-c4fa-4057-bf87-2ca409cde736",
|
"id": "43a864c4-c4fa-4057-bf87-2ca409cde736",
|
||||||
"name": "Max Clients Limit",
|
"name": "Max Clients Limit",
|
||||||
"providerId": "max-clients",
|
"providerId": "max-clients",
|
||||||
"subType": "anonymous",
|
"subType": "anonymous",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"max-clients": [
|
"max-clients": [
|
||||||
@@ -697,10 +779,10 @@
|
|||||||
"providerId": "consent-required",
|
"providerId": "consent-required",
|
||||||
"subType": "anonymous",
|
"subType": "anonymous",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -709,7 +791,7 @@
|
|||||||
"providerId": "allowed-client-templates",
|
"providerId": "allowed-client-templates",
|
||||||
"subType": "authenticated",
|
"subType": "authenticated",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-default-scopes": [
|
"allow-default-scopes": [
|
||||||
@@ -723,7 +805,7 @@
|
|||||||
"providerId": "allowed-protocol-mappers",
|
"providerId": "allowed-protocol-mappers",
|
||||||
"subType": "authenticated",
|
"subType": "authenticated",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
@@ -744,10 +826,10 @@
|
|||||||
"providerId": "scope",
|
"providerId": "scope",
|
||||||
"subType": "anonymous",
|
"subType": "anonymous",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -756,7 +838,7 @@
|
|||||||
"providerId": "allowed-protocol-mappers",
|
"providerId": "allowed-protocol-mappers",
|
||||||
"subType": "anonymous",
|
"subType": "anonymous",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allowed-protocol-mapper-types": [
|
"allowed-protocol-mapper-types": [
|
||||||
@@ -777,7 +859,7 @@
|
|||||||
"providerId": "trusted-hosts",
|
"providerId": "trusted-hosts",
|
||||||
"subType": "anonymous",
|
"subType": "anonymous",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"host-sending-registration-request-must-match": [
|
"host-sending-registration-request-must-match": [
|
||||||
@@ -794,7 +876,7 @@
|
|||||||
"providerId": "allowed-client-templates",
|
"providerId": "allowed-client-templates",
|
||||||
"subType": "anonymous",
|
"subType": "anonymous",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-default-scopes": [
|
"allow-default-scopes": [
|
||||||
@@ -803,13 +885,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"org.keycloak.keys.KeyProvider": [
|
"org.keycloak.keys.KeyProvider": [{
|
||||||
{
|
|
||||||
"id": "005993b6-dcb3-4ebf-b19c-7287c740bb79",
|
"id": "005993b6-dcb3-4ebf-b19c-7287c740bb79",
|
||||||
"name": "rsa-generated",
|
"name": "rsa-generated",
|
||||||
"providerId": "rsa-generated",
|
"providerId": "rsa-generated",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"priority": [
|
"priority": [
|
||||||
@@ -822,7 +903,7 @@
|
|||||||
"name": "aes-generated",
|
"name": "aes-generated",
|
||||||
"providerId": "aes-generated",
|
"providerId": "aes-generated",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"priority": [
|
"priority": [
|
||||||
@@ -835,7 +916,7 @@
|
|||||||
"name": "hmac-generated",
|
"name": "hmac-generated",
|
||||||
"providerId": "hmac-generated",
|
"providerId": "hmac-generated",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"priority": [
|
"priority": [
|
||||||
@@ -850,16 +931,14 @@
|
|||||||
},
|
},
|
||||||
"internationalizationEnabled": false,
|
"internationalizationEnabled": false,
|
||||||
"supportedLocales": [],
|
"supportedLocales": [],
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [{
|
||||||
{
|
|
||||||
"id": "50eda798-0ed6-4fd1-9071-977ca22b032f",
|
"id": "50eda798-0ed6-4fd1-9071-977ca22b032f",
|
||||||
"alias": "Handle Existing Account",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": false,
|
"topLevel": false,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "idp-confirm-link",
|
"authenticator": "idp-confirm-link",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -889,8 +968,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": false,
|
"topLevel": false,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "idp-username-password-form",
|
"authenticator": "idp-username-password-form",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -913,8 +991,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "auth-cookie",
|
"authenticator": "auth-cookie",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -951,8 +1028,7 @@
|
|||||||
"providerId": "client-flow",
|
"providerId": "client-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "client-secret",
|
"authenticator": "client-secret",
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -989,8 +1065,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "direct-grant-validate-username",
|
"authenticator": "direct-grant-validate-username",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -1020,15 +1095,13 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
"authenticator": "docker-http-basic-authenticator",
|
||||||
"authenticator": "docker-http-basic-authenticator",
|
"requirement": "REQUIRED",
|
||||||
"requirement": "REQUIRED",
|
"priority": 10,
|
||||||
"priority": 10,
|
"userSetupAllowed": false,
|
||||||
"userSetupAllowed": false,
|
"autheticatorFlow": false
|
||||||
"autheticatorFlow": false
|
}]
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "eb15d55b-a601-493c-9f49-069695624ada",
|
"id": "eb15d55b-a601-493c-9f49-069695624ada",
|
||||||
@@ -1037,8 +1110,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticatorConfig": "review profile config",
|
"authenticatorConfig": "review profile config",
|
||||||
"authenticator": "idp-review-profile",
|
"authenticator": "idp-review-profile",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
@@ -1070,8 +1142,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": false,
|
"topLevel": false,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "auth-username-password-form",
|
"authenticator": "auth-username-password-form",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -1094,8 +1165,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "no-cookie-redirect",
|
"authenticator": "no-cookie-redirect",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -1132,16 +1202,14 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
"authenticator": "registration-page-form",
|
||||||
"authenticator": "registration-page-form",
|
"requirement": "REQUIRED",
|
||||||
"requirement": "REQUIRED",
|
"priority": 10,
|
||||||
"priority": 10,
|
"flowAlias": "registration form",
|
||||||
"flowAlias": "registration form",
|
"userSetupAllowed": false,
|
||||||
"userSetupAllowed": false,
|
"autheticatorFlow": true
|
||||||
"autheticatorFlow": true
|
}]
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "088ede2e-2dce-466d-b25f-62cc07c12bee",
|
"id": "088ede2e-2dce-466d-b25f-62cc07c12bee",
|
||||||
@@ -1150,8 +1218,7 @@
|
|||||||
"providerId": "form-flow",
|
"providerId": "form-flow",
|
||||||
"topLevel": false,
|
"topLevel": false,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "registration-user-creation",
|
"authenticator": "registration-user-creation",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
@@ -1188,8 +1255,7 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
|
||||||
"authenticator": "reset-credentials-choose-user",
|
"authenticator": "reset-credentials-choose-user",
|
||||||
"requirement": "REQUIRED",
|
"requirement": "REQUIRED",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
@@ -1226,19 +1292,16 @@
|
|||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
"authenticationExecutions": [
|
"authenticationExecutions": [{
|
||||||
{
|
"authenticator": "http-basic-authenticator",
|
||||||
"authenticator": "http-basic-authenticator",
|
"requirement": "REQUIRED",
|
||||||
"requirement": "REQUIRED",
|
"priority": 10,
|
||||||
"priority": 10,
|
"userSetupAllowed": false,
|
||||||
"userSetupAllowed": false,
|
"autheticatorFlow": false
|
||||||
"autheticatorFlow": false
|
}]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"authenticatorConfig": [
|
"authenticatorConfig": [{
|
||||||
{
|
|
||||||
"id": "1593e111-7e5e-4ee2-b33d-f459834e4e3b",
|
"id": "1593e111-7e5e-4ee2-b33d-f459834e4e3b",
|
||||||
"alias": "create unique user config",
|
"alias": "create unique user config",
|
||||||
"config": {
|
"config": {
|
||||||
@@ -1253,8 +1316,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requiredActions": [
|
"requiredActions": [{
|
||||||
{
|
|
||||||
"alias": "CONFIGURE_TOTP",
|
"alias": "CONFIGURE_TOTP",
|
||||||
"name": "Configure OTP",
|
"name": "Configure OTP",
|
||||||
"providerId": "CONFIGURE_TOTP",
|
"providerId": "CONFIGURE_TOTP",
|
||||||
@@ -1262,7 +1324,7 @@
|
|||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1273,7 +1335,7 @@
|
|||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1284,7 +1346,7 @@
|
|||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1295,7 +1357,7 @@
|
|||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 40,
|
"priority": 40,
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1306,7 +1368,7 @@
|
|||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 50,
|
"priority": 50,
|
||||||
"config": {
|
"config": {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
53
share-dependencies/dependency-reduced-pom.xml
Normal file
53
share-dependencies/dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?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">
|
||||||
|
<parent>
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.share.deps</artifactId>
|
||||||
|
<name>Acosix Alfresco Keycloak - Share Dependencies Module</name>
|
||||||
|
<description>Aggregate (Uber-)JAR of all dependencies for the Acosix Alfresco Keycloak Share Module</description>
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.keycloak</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.share.deps.keycloak</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.jboss.logging</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.share.deps.jboss.logging</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<transformers>
|
||||||
|
<transformer />
|
||||||
|
<transformer />
|
||||||
|
<transformer>
|
||||||
|
<addHeader>false</addHeader>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
128
share-dependencies/pom.xml
Normal file
128
share-dependencies/pom.xml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!--
|
||||||
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<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/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>de.acosix.alfresco.keycloak</groupId>
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
|
||||||
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>de.acosix.alfresco.keycloak.share.deps</artifactId>
|
||||||
|
<name>Acosix Alfresco Keycloak - Share Dependencies Module</name>
|
||||||
|
<description>Aggregate (Uber-)JAR of all dependencies for the Acosix Alfresco Keycloak Share Module</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-adapter-core</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-servlet-adapter-spi</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-servlet-filter-adapter</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-authz-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.keycloak</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.share.deps.keycloak</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.jboss.logging</pattern>
|
||||||
|
<shadedPattern>de.acosix.alfresco.keycloak.share.deps.jboss.logging</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
<transformers>
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
|
||||||
|
<addHeader>false</addHeader>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -127,11 +127,92 @@
|
|||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
|
||||||
|
<!-- some image customisations -->
|
||||||
|
<!-- Maven + docker-maven-plugin result in somewhat weird inheritance handling -->
|
||||||
|
<!-- (relying on positional order of images for overrides) -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>io.fabric8</groupId>
|
||||||
|
<artifactId>docker-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<images>
|
||||||
|
<image>
|
||||||
|
<!-- no change to postgres image (first image in parent POM) -->
|
||||||
|
</image>
|
||||||
|
<image>
|
||||||
|
<!-- customise repository image (second image in parent POM) -->
|
||||||
|
<run>
|
||||||
|
<env>
|
||||||
|
<DOCKER_HOST_NAME>${docker.tests.host.name}</DOCKER_HOST_NAME>
|
||||||
|
</env>
|
||||||
|
<!-- add log directory mount to just the contentstore -->
|
||||||
|
<!-- (cannot be done in parent POM due to hard requirement on specific project structure -->
|
||||||
|
<!-- for tests to easily check contentstore files, we also mount alf_data locally, not in a volume -->
|
||||||
|
<volumes>
|
||||||
|
<bind>
|
||||||
|
<volume>${moduleId}-repository-test-contentstore:/usr/local/tomcat/alf_data</volume>
|
||||||
|
<volume>${project.build.directory}/docker/repository-logs:/usr/local/tomcat/logs</volume>
|
||||||
|
</bind>
|
||||||
|
</volumes>
|
||||||
|
<dependsOn>
|
||||||
|
<container>postgres</container>
|
||||||
|
<container>keycloak</container>
|
||||||
|
</dependsOn>
|
||||||
|
</run>
|
||||||
|
</image>
|
||||||
|
<image>
|
||||||
|
<!-- no change to Share image -->
|
||||||
|
</image>
|
||||||
|
<image>
|
||||||
|
<!-- no change to Search image -->
|
||||||
|
</image>
|
||||||
|
<image>
|
||||||
|
<name>jboss/keycloak</name>
|
||||||
|
<alias>keycloak</alias>
|
||||||
|
<run>
|
||||||
|
<hostname>keycloak</hostname>
|
||||||
|
<env>
|
||||||
|
<KEYCLOAK_USER>admin</KEYCLOAK_USER>
|
||||||
|
<KEYCLOAK_PASSWORD>admin</KEYCLOAK_PASSWORD>
|
||||||
|
<KEYCLOAK_IMPORT>/tmp/test-realm.json</KEYCLOAK_IMPORT>
|
||||||
|
<DB_VENDOR>h2</DB_VENDOR>
|
||||||
|
</env>
|
||||||
|
<ports>
|
||||||
|
<port>${docker.tests.keycloakPort}:8080</port>
|
||||||
|
</ports>
|
||||||
|
<network>
|
||||||
|
<mode>custom</mode>
|
||||||
|
<name>${moduleId}-test</name>
|
||||||
|
<alias>keycloak</alias>
|
||||||
|
</network>
|
||||||
|
<volumes>
|
||||||
|
<bind>
|
||||||
|
<volume>${project.build.directory}/docker/test-realm.json:/tmp/test-realm.json</volume>
|
||||||
|
</bind>
|
||||||
|
</volumes>
|
||||||
|
</run>
|
||||||
|
</image>
|
||||||
|
</images>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>net.alchim31.maven</groupId>
|
<groupId>net.alchim31.maven</groupId>
|
||||||
<artifactId>yuicompressor-maven-plugin</artifactId>
|
<artifactId>yuicompressor-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>io.fabric8</groupId>
|
||||||
|
<artifactId>docker-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8' ?>
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8' ?>
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8' ?>
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<#--
|
<#--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
12
share/src/test/docker/Repository-Dockerfile
Normal file
12
share/src/test/docker/Repository-Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM ${docker.tests.repositoryBaseImage}
|
||||||
|
COPY maven ${docker.tests.repositoryWebappPath}
|
||||||
|
|
||||||
|
# merge additions to alfresco-global.properties
|
||||||
|
RUN echo "" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||||
|
&& echo "#MergeGlobalProperties" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||||
|
&& sed -i '/#MergeGlobalProperties/r ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/alfresco-global.addition.properties' ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||||
|
&& sed -i 's/<secure>true<\/secure>/<secure>false<\/secure>/' $CATALINA_HOME/conf/web.xml \
|
||||||
|
&& mv ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/entrypoint.sh $CATALINA_HOME/bin/ \
|
||||||
|
&& chmod +x $CATALINA_HOME/bin/entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["entrypoint.sh", "catalina.sh run -security"]
|
@@ -0,0 +1,28 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# note: this file is not named alfresco-global.properties to not override the default file in the image
|
||||||
|
# instead it relies on Dockerfile post-processing to merge with the default file
|
||||||
|
|
||||||
|
authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm
|
||||||
|
|
||||||
|
keycloak.adapter.auth-server-url=http://${docker.tests.host.name}:${docker.tests.keycloakPort}/auth
|
||||||
|
keycloak.adapter.realm=test
|
||||||
|
keycloak.adapter.resource=alfresco
|
||||||
|
keycloak.adapter.credentials.provider=secret
|
||||||
|
keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
|
||||||
|
|
||||||
|
keycloak.synchronization.userFilter.containedInGroup.property.groupPaths=/Test A
|
||||||
|
keycloak.synchronization.groupFilter.containedInGroup.property.groupPaths=/Test A
|
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
log4j.rootLogger=error, File
|
||||||
|
|
||||||
|
log4j.appender.File=org.apache.log4j.DailyRollingFileAppender
|
||||||
|
log4j.appender.File.File=\${catalina.base}/logs/alfresco.log
|
||||||
|
log4j.appender.File.Append=true
|
||||||
|
log4j.appender.File.DatePattern='.'yyyy-MM-dd
|
||||||
|
log4j.appender.File.layout=org.apache.log4j.PatternLayout
|
||||||
|
log4j.appender.File.layout.ConversionPattern=%d{ISO8601} %-5p [%c] [%t] %m%n
|
||||||
|
|
||||||
|
log4j.logger.${project.artifactId}=DEBUG
|
10
share/src/test/docker/alfresco/extension/entrypoint.sh
Normal file
10
share/src/test/docker/alfresco/extension/entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ip=`hostname -I | awk '{print $1}'`
|
||||||
|
hostip=`echo "${ip}" | sed -E 's/([0-9]+\.[0-9]+)\.0\.[0-9]+/\1.0.1/'`
|
||||||
|
hostname="${DOCKER_HOST_NAME}"
|
||||||
|
echo "${hostip} ${hostname}" >> /etc/hosts
|
||||||
|
|
||||||
|
bash -c "$@"
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8' ?>
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -23,18 +23,43 @@
|
|||||||
<format>dir</format>
|
<format>dir</format>
|
||||||
</formats>
|
</formats>
|
||||||
<includeBaseDirectory>false</includeBaseDirectory>
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
<dependencySets>
|
<fileSets>
|
||||||
<!--
|
<fileSet>
|
||||||
<dependencySet>
|
<directory>${project.basedir}/src/test/docker/alfresco</directory>
|
||||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
<outputDirectory>WEB-INF/classes/alfresco</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
|
<include>*</include>
|
||||||
|
<include>**/*</include>
|
||||||
</includes>
|
</includes>
|
||||||
<scope>compile</scope>
|
<excludes>
|
||||||
</dependencySet>
|
<exclude>*.js</exclude>
|
||||||
-->
|
<exclude>**/*.js</exclude>
|
||||||
|
<exclude>*.ftl</exclude>
|
||||||
|
<exclude>**/*.ftl</exclude>
|
||||||
|
<exclude>*.keystore</exclude>
|
||||||
|
<exclude>**/*.keystore</exclude>
|
||||||
|
</excludes>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
<lineEnding>lf</lineEnding>
|
||||||
|
</fileSet>
|
||||||
|
<fileSet>
|
||||||
|
<directory>${project.basedir}/src/test/docker/alfresco</directory>
|
||||||
|
<outputDirectory>WEB-INF/classes/alfresco</outputDirectory>
|
||||||
|
<includes>
|
||||||
|
<include>*.js</include>
|
||||||
|
<include>**/*.js</include>
|
||||||
|
<include>*.ftl</include>
|
||||||
|
<include>**/*.ftl</include>
|
||||||
|
<include>*.keystore</include>
|
||||||
|
<include>**/*.keystore</include>
|
||||||
|
</includes>
|
||||||
|
</fileSet>
|
||||||
|
</fileSets>
|
||||||
|
<dependencySets>
|
||||||
<dependencySet>
|
<dependencySet>
|
||||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
|
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo.deps:*</include>
|
||||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.common:*</include>
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.common:*</include>
|
||||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz1:*</include>
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz1:*</include>
|
||||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
|
||||||
|
1
share/src/test/docker/repository-logs/dummy.properties
Normal file
1
share/src/test/docker/repository-logs/dummy.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# only exists to ensure Maven creates path in project ./target
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -23,6 +23,13 @@
|
|||||||
<format>dir</format>
|
<format>dir</format>
|
||||||
</formats>
|
</formats>
|
||||||
<includeBaseDirectory>false</includeBaseDirectory>
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
|
<files>
|
||||||
|
<file>
|
||||||
|
<source>${project.basedir}/src/test/docker/share-log4j.properties</source>
|
||||||
|
<outputDirectory>WEB-INF/classes</outputDirectory>
|
||||||
|
<destName>log4j.properties</destName>
|
||||||
|
</file>
|
||||||
|
</files>
|
||||||
<fileSets>
|
<fileSets>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>${project.build.directory}</directory>
|
<directory>${project.build.directory}</directory>
|
||||||
@@ -32,24 +39,41 @@
|
|||||||
</includes>
|
</includes>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>${project.basedir}/src/test/resources</directory>
|
<directory>${project.basedir}/src/test/docker/alfresco</directory>
|
||||||
<outputDirectory>WEB-INF/classes</outputDirectory>
|
<outputDirectory>WEB-INF/classes/alfresco</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>**/*.properties</include>
|
<include>*</include>
|
||||||
<include>**/*.xml</include>
|
<include>**/*</include>
|
||||||
</includes>
|
</includes>
|
||||||
|
<excludes>
|
||||||
|
<exclude>*.js</exclude>
|
||||||
|
<exclude>**/*.js</exclude>
|
||||||
|
<exclude>*.ftl</exclude>
|
||||||
|
<exclude>**/*.ftl</exclude>
|
||||||
|
<exclude>*.keystore</exclude>
|
||||||
|
<exclude>**/*.keystore</exclude>
|
||||||
|
</excludes>
|
||||||
<filtered>true</filtered>
|
<filtered>true</filtered>
|
||||||
<lineEnding>lf</lineEnding>
|
<lineEnding>lf</lineEnding>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
|
<fileSet>
|
||||||
|
<directory>${project.basedir}/src/test/docker/alfresco</directory>
|
||||||
|
<outputDirectory>WEB-INF/classes/alfresco</outputDirectory>
|
||||||
|
<includes>
|
||||||
|
<include>*.js</include>
|
||||||
|
<include>**/*.js</include>
|
||||||
|
<include>*.ftl</include>
|
||||||
|
<include>**/*.ftl</include>
|
||||||
|
<include>*.keystore</include>
|
||||||
|
<include>**/*.keystore</include>
|
||||||
|
</includes>
|
||||||
|
</fileSet>
|
||||||
</fileSets>
|
</fileSets>
|
||||||
<dependencySets>
|
<dependencySets>
|
||||||
<dependencySet>
|
<dependencySet>
|
||||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>org.keycloak:*</include>
|
<include>${project.groupId}:de.acosix.alfresco.keycloak.share.deps:*</include>
|
||||||
<include>org.jboss.logging:*</include>
|
|
||||||
<include>org.bouncycastle:*</include>
|
|
||||||
<include>com.fasterxml.jackson.core:*</include>
|
|
||||||
</includes>
|
</includes>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependencySet>
|
</dependencySet>
|
||||||
|
1404
share/src/test/docker/test-realm.json
Normal file
1404
share/src/test/docker/test-realm.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Acosix GmbH
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8' ?>
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8' ?>
|
<?xml version='1.0' encoding='UTF-8' ?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2019 Acosix GmbH
|
Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
Reference in New Issue
Block a user