Initial version

This commit is contained in:
AFaust 2019-08-21 00:01:40 +02:00
commit 2db4aaddbe
49 changed files with 4316 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.settings/
bin/
.project
.classpath
target/

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
sudo: false
language: java
jdk:
- openjdk8
cache:
directories:
- $HOME/.m2

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2019 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.

99
README.md Normal file
View File

@ -0,0 +1,99 @@
[![Build Status](https://travis-ci.org/Acosix/alfresco-keycloak.svg?branch=master)](https://travis-ci.org/Acosix/alfresco-keycloak)
# About
This addon aims to provide a Keycloak-related extensions / customisations to the out-of-the-box Alfresco authentication and authorization functionalities for the Alfresco Repository and Share tiers.
## Compatbility
This module is built to be compatible with Alfresco 6.0 and above. It may be used on either Community or Enterprise Edition.
## Features
At this time, only the Share sub-module of this project provides a feature enhancement.
The Share sub-module provides a Keycloak-based filter and customisations that support:
- enhancement of the login dialog to allow users to perform an external authentication via a Keycloak server as an alternative to the password-based login
- enforcement of an external authentication via a Keycloak server for all users via a SSO filter enhancement
- back-channel logout and other operations actively initiated by a Keycloak server, e.g. invalidating all authentication tokens issued after a specific point in time
- active logout which also logs out the user centrally on the Keycloak server
# Build
This project uses a Maven build using templates from the [Acosix Alfresco Maven](https://github.com/Acosix/alfresco-maven) project and produces module AMPs, regular Java *classes* JARs, JavaDoc and source attachment JARs, as well as installable (Simple Alfresco Module) JAR artifacts for the Alfresco Content Services and Share extensions. If the installable JAR artifacts are used for installing this module, developers / users are advised to consult the 'Dependencies' section of this README.
## Maven toolchains
By inheritance from the Acosix Alfresco Maven framework, this project uses the [Maven Toolchains plugin](http://maven.apache.org/plugins/maven-toolchains-plugin/) to allow potential cross-compilation against different Java versions. This plugin is used to avoid potentially inconsistent compiler and library versions compared to when only the source/target compiler options of the Maven compiler plugin are set, which (as an example) has caused issues with some Alfresco releases in the past where Alfresco compiled for Java 7 using the Java 8 libraries.
In order to build the project it is necessary to provide a basic toolchain configuration via the user specific Maven configuration home (usually ~/.m2/). That file (toolchains.xml) only needs to list the path to a compatible JDK for the Java version required by this project. The following is a sample file defining a Java 7 and 8 development kit.
```xml
<?xml version='1.0' encoding='UTF-8'?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.8</version>
<vendor>oracle</vendor>
</provides>
<configuration>
<jdkHome>C:\Program Files\Java\jdk1.8.0_112</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>1.7</version>
<vendor>oracle</vendor>
</provides>
<configuration>
<jdkHome>C:\Program Files\Java\jdk1.7.0_80</jdkHome>
</configuration>
</toolchain>
</toolchains>
```
The master branch requires Java 8.
## Docker-based integration tests
In a default build using ```mvn clean install```, this project will build the extension for Alfresco Content Services, executing regular unit-tests without running integration tests. The integration tests of this project are based on Docker and require a Docker engine to run the necessary components (PostgreSQL database as well as Alfresco Content Services). Since a Docker engine may not be available in all environments of interested community members / collaborators, the integration tests have been made optional. A full build, including integration tests, can be run by executing
```
mvn clean install -Ddocker.tests.enabled=true
```
This project currently does not contain any integration tests, as a proper setup which includes Keycloak has not yet been achieved.
## Dependencies
This module depends on the following projects / libraries:
- various [Keycloak]https://github.com/keycloak/keycloak) adapter and client libraries (Apache License, Version 2.0)
- keycloak-adapter-core
- keycloak-servlet-adapter-spi
- keycloak-servlet-filter-adapter
- keycloak-authz-client
- Acosix Alfresco Utility (Apache License, Version 2.0) - core extension
The Share AMP of this project includes all the Keycloak library JARs that the extension depends on. The Acosix Alfresco Utility project provides the core extension for Alfresco Content Services as a separate artifact from the full module, which needs to be installed in Alfresco Content Services before the AMP of this project can be installed.
When the installable JAR produced by the build of this project is used for installation, the developer / user is responsible to either manually install all the required components / libraries provided by the listed projects, or use a build system to collect all relevant direct / transitive dependencies.
**Note**: The Acosix Alfresco Utility project is also built using templates from the Acosix Alfresco Maven project, and as such produces similar artifacts. Automatic resolution and collection of (transitive) dependencies using Maven / Gradle will resolve the Java *classes* JAR as a dependency, and **not** the installable (Simple Alfresco Module) variant. It is recommended to exclude Acosix Alfresco Utility from transitive resolution and instead include it directly / explicitly.
## Using SNAPSHOT builds
In order to use a pre-built SNAPSHOT artifact published to the Open Source Sonatype Repository Hosting site, the artifact repository may need to be added to the POM, global settings.xml or an artifact repository proxy server. The following is the XML snippet for inclusion in a POM file.
```xml
<repositories>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
```

200
pom.xml Normal file
View File

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2019 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.maven</groupId>
<artifactId>de.acosix.alfresco.maven.project.parent-6.1.2</artifactId>
<version>1.2.0</version>
</parent>
<groupId>de.acosix.alfresco.keycloak</groupId>
<artifactId>de.acosix.alfresco.keycloak.parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<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>
<url>https://github.com/Acosix/alfresco-keycloak</url>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<scm>
<connection>scm:git:git@github.com:Acosix/alfresco-keycloak.git</connection>
<developerConnection>scm:git:git@github.com:Acosix/alfresco-keycloak.git</developerConnection>
<url>git@github.com:Acosix/alfresco-keycloak.git</url>
</scm>
<developers>
<developer>
<id>AFaust</id>
<name>Axel Faust</name>
<email>axel.faust@acosix.de</email>
<organization>Acosix GmbH</organization>
<roles>
<role>Founder</role>
<role>Architect</role>
<role>Developer</role>
</roles>
<properties>
<twitter>twitter.com/ReluctantBird83</twitter>
</properties>
</developer>
</developers>
<properties>
<messages.path>acosix/keycloak</messages.path>
<messages.packageId>acosix.keycloak</messages.packageId>
<moduleId>acosix-keycloak</moduleId>
<!-- Java 7 is out of support -->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<keycloak.version>6.0.1</keycloak.version>
<!-- lowest common denominator of Repository / Share in 6.1 -->
<apache.httpclient.version>4.5.1</apache.httpclient.version>
<apache.httpcore.version>4.4.3</apache.httpcore.version>
<acosix.utility.version>1.0.7.0</acosix.utility.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-adapter-spi</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>${keycloak.version}</version>
</dependency>
<!-- HttpClient already bundled by both Repository and Share web apps -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${apache.httpclient.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>${apache.httpcore.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.common</artifactId>
<version>${acosix.utility.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.repo</artifactId>
<version>${acosix.utility.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.repo</artifactId>
<version>${acosix.utility.version}</version>
<classifier>installable</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
<version>${acosix.utility.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
<version>${acosix.utility.version}</version>
<classifier>installable</classifier>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<build>
</build>
<modules>
<module>repository</module>
<module>share</module>
</modules>
</project>

View File

@ -0,0 +1,2 @@
include.default=true
/web=/

View File

@ -0,0 +1,6 @@
module.id=${moduleId}
module.title=${project.name}
module.description=${project.description}
module.version=${noSnapshotVersion}
module.repo.version.min=5

49
repository/pom.xml Normal file
View File

@ -0,0 +1,49 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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.0.0</version>
</parent>
<artifactId>de.acosix.alfresco.keycloak.repo</artifactId>
<name>Acosix Alfresco Keycloak - Repository Module</name>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-repository</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1 @@
log4j.logger.${project.artifactId}=INFO

View File

@ -0,0 +1,24 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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.
-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
</beans>

View File

View File

View File

View File

View File

@ -0,0 +1,2 @@
include.default=true
/web=/

4
share/module.properties Normal file
View File

@ -0,0 +1,4 @@
module.id=${moduleId}
module.title=${project.name}
module.description=${project.description}
module.version=${noSnapshotVersion}

113
share/pom.xml Normal file
View File

@ -0,0 +1,113 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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.0.0</version>
</parent>
<artifactId>de.acosix.alfresco.keycloak.share</artifactId>
<name>Acosix Alfresco Keycloak - Share Module</name>
<dependencies>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>share</artifactId>
<classifier>classes</classifier>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-adapter-spi</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.repo</artifactId>
<classifier>installable</classifier>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
</dependency>
<dependency>
<groupId>de.acosix.alfresco.utility</groupId>
<artifactId>de.acosix.alfresco.utility.core.share</artifactId>
<classifier>installable</classifier>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>de.acosix.alfresco.keycloak.repo</artifactId>
<version>${project.version}</version>
<classifier>installable</classifier>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>de.thetaphi</groupId>
<artifactId>forbiddenapis</artifactId>
<configuration>
<excludes>
<exclude>**/KeycloakAdapterConfigElement.class</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>yuicompressor-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,55 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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>org.keycloak:*</include>
<include>org.jboss.logging:*</include>
<include>org.bouncycastle:*</include>
<include>com.fasterxml.jackson.core:*</include>
</includes>
</dependencySet>
</dependencySets>
</assembly>

View File

@ -0,0 +1,60 @@
<?xml version='1.0' encoding='UTF-8' ?>
<!--
Copyright 2019 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.
-->
<alfresco-config>
<plug-ins>
<element-readers>
<element-reader element-name="keycloak-auth-config"
class="${project.artifactId}.config.KeycloakAuthenticationConfigElementReader" />
<element-reader element-name="keycloak-adapter-config"
class="${project.artifactId}.config.KeycloakAdapterConfigElementReader" />
</element-readers>
</plug-ins>
<!-- sensible default configuration (similar to Repository identity-service-authentication.properties -->
<config evaluator="string-compare" condition="Keycloak">
<keycloak-auth-config>
<enhance-login-form>true</enhance-login-form>
<enable-sso-filter>true</enable-sso-filter>
<force-keycloak-sso>false</force-keycloak-sso>
<!-- use 8443 as default SSL redirect based on Tomcat default server.xml configuration -->
<ssl-redirect-port>8443</ssl-redirect-port>
<body-buffer-limit>10485760</body-buffer-limit>
<session-mapper-limit>1000</session-mapper-limit>
</keycloak-auth-config>
<keycloak-adapter-config>
<auth-server-url>http://localhost:8180/auth</auth-server-url>
<realm>alfresco</realm>
<resource>alfresco</resource>
<ssl-required>none</ssl-required>
<!-- other than content-app / Identity Service, Share must/should be a confidential client to exchange code for access token + refresh -->
<public-client>false</public-client>
<credentials>
<provider>secret</provider>
</credentials>
</keycloak-adapter-config>
</config>
<!-- add to the global configuration -->
<config evaluator="string-compare">
<user>
<!-- make sure groups of a user are kept up-to-date in at least 60 seconds intervals (lazily refreshed on next request) -->
<cached-user-groups-timeout>60000</cached-user-groups-timeout>
</user>
</config>
</alfresco-config>

View File

@ -0,0 +1 @@
log4j.logger.${project.artifactId}=INFO

View File

@ -0,0 +1,20 @@
<?xml version='1.0' encoding='UTF-8' ?>
<!--
Copyright 2019 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.
-->
<alfresco-config>
</alfresco-config>

View File

@ -0,0 +1,77 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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.
-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean class="de.acosix.alfresco.utility.share.spring.WebFrameworkConfigSourceInserter">
<property name="afterConfigSources">
<list>
<value>classpath:alfresco/share-config.xml</value>
</list>
</property>
<property name="beforeConfigSources">
<list>
<value>classpath:alfresco/web-extension/share-config-custom.xml</value>
<value>jar:*!/META-INF/share-config-custom.xml</value>
</list>
</property>
<property name="configSources">
<list>
<value>classpath:alfresco/module/${moduleId}/default-config.xml</value>
</list>
</property>
</bean>
<bean class="org.springframework.extensions.config.ConfigBootstrap" init-method="register">
<property name="configService" ref="web.config" />
<property name="configs">
<list>
<value>classpath:alfresco/module/${moduleId}/module-config.xml</value>
</list>
</property>
</bean>
<bean id="${moduleId}.SessionIdMapper" class="${project.artifactId}.web.DefaultSessionIdMapper">
<property name="configService" ref="web.config" />
</bean>
<bean id="${moduleId}.KeycloakAuthenticationFilter" abstract="true" class="${project.artifactId}.web.KeycloakAuthenticationFilter">
<property name="configService" ref="web.config" />
<property name="connectorService" ref="connector.service" />
<property name="pageViewResolver" ref="pageViewResolver" />
<property name="sessionIdMapper" ref="${moduleId}.SessionIdMapper" />
<property name="primaryEndpoint" value="alfresco" />
<property name="secondaryEndpoints">
<list>
<value>alfresco-api</value>
<value>alfresco-feed</value>
</list>
</property>
</bean>
<bean id="${moduleId}.UserGroupsLoadFilter" class="${project.artifactId}.web.UserGroupsLoadFilter">
<property name="configService" ref="web.config" />
<property name="connectorService" ref="connector.service" />
</bean>
<bean class="${project.artifactId}.spring.KeycloakAuthenticationFilterActivation">
<property name="moduleId" value="${moduleId}" />
</bean>
</beans>

View File

@ -0,0 +1,41 @@
<?xml version='1.0' encoding='UTF-8' ?>
<!--
Copyright 2019 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.
-->
<extension>
<modules>
<module>
<id>${moduleId} - Base Extensions</id>
<description>${project.name} - Base Extensions</description>
<version>${noSnapshotVersion}</version>
<auto-deploy>true</auto-deploy>
<customizations>
<customization>
<targetPackageRoot>org.alfresco</targetPackageRoot>
<sourcePackageRoot>de.acosix.keycloak.customisations</sourcePackageRoot>
</customization>
</customizations>
<customization>
<targetPackageRoot>org.alfresco.share.pages</targetPackageRoot>
<sourcePackageRoot>de.acosix.keycloak.customisations.share.header</sourcePackageRoot>
<alwaysApply>
<webscript>share-header</webscript>
</alwaysApply>
</customization>
</module>
</modules>
</extension>

View File

@ -0,0 +1,457 @@
/*
* Copyright 2019 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.share.config;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.ParameterCheck;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.extensions.config.ConfigElement;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.acosix.alfresco.utility.share.config.BaseCustomConfigElement;
import de.acosix.alfresco.utility.share.config.ConfigValueHolder;
/**
* @author Axel Faust
*/
public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
{
public static final String NAME = "keycloak-adapter-config";
private static final long serialVersionUID = -7211927327179092723L;
private static final Map<String, Field> FIELD_BY_CONFIG_NAME;
private static final Map<String, Class<?>> VALUE_TYPE_BY_CONFIG_NAME;
private static final List<String> CONFIG_NAMES;
static
{
final Map<String, Field> fieldByConfigName = new HashMap<>();
final Map<String, Class<?>> valueTypeByConfigName = new HashMap<>();
final List<String> configNames = new ArrayList<>();
final Set<Class<?>> supportedValueTypes = new HashSet<>(Arrays.asList(String.class, Map.class));
final Map<Class<?>, Class<?>> primitiveWrapperTypeMap = new HashMap<>();
final Class<?>[] wrapperTypes = { Integer.class, Long.class, Boolean.class, Short.class, Byte.class, Character.class, Float.class,
Double.class };
final Class<?>[] primitiveTypes = { int.class, long.class, boolean.class, short.class, byte.class, char.class, float.class,
double.class };
for (int i = 0; i < primitiveTypes.length; i++)
{
supportedValueTypes.add(primitiveTypes[i]);
supportedValueTypes.add(wrapperTypes[i]);
primitiveWrapperTypeMap.put(primitiveTypes[i], wrapperTypes[i]);
}
Class<?> cls = AdapterConfig.class;
while (cls != null && !Object.class.equals(cls))
{
final Field[] fields = cls.getDeclaredFields();
for (final Field field : fields)
{
final JsonProperty annotation = field.getAnnotation(JsonProperty.class);
if (annotation != null)
{
final String configName = annotation.value();
Class<?> valueType = field.getType();
if (valueType.isPrimitive())
{
valueType = primitiveWrapperTypeMap.get(valueType);
}
if (supportedValueTypes.contains(valueType))
{
fieldByConfigName.put(configName, field);
valueTypeByConfigName.put(configName, valueType);
configNames.add(configName);
}
}
}
cls = cls.getSuperclass();
}
FIELD_BY_CONFIG_NAME = Collections.unmodifiableMap(fieldByConfigName);
VALUE_TYPE_BY_CONFIG_NAME = Collections.unmodifiableMap(valueTypeByConfigName);
CONFIG_NAMES = Collections.unmodifiableList(configNames);
}
protected final Map<String, Object> configValueByField = new HashMap<>();
protected final Set<String> markedAsUnset = new HashSet<>();
protected final ConfigValueHolder<Long> connectionTimeout = new ConfigValueHolder<>();
protected final ConfigValueHolder<Long> socketTimeout = new ConfigValueHolder<>();
/**
* Creates a new instance of this class.
*/
public KeycloakAdapterConfigElement()
{
super(NAME);
}
/**
* @return the connectionTimeout
*/
public Long getConnectionTimeout()
{
return this.connectionTimeout.getValue();
}
/**
* @param connectionTimeout
* the connectionTimeout to set
*/
public void setConnectionTimeout(final Long connectionTimeout)
{
this.connectionTimeout.setValue(connectionTimeout);
}
/**
* @return the socketTimeout
*/
public Long getSocketTimeout()
{
return this.socketTimeout.getValue();
}
/**
* @param socketTimeout
* the socketTimeout to set
*/
public void setSocketTimeout(final Long socketTimeout)
{
this.socketTimeout.setValue(socketTimeout);
}
/**
* Checks if a specific field is supported by this config element.
*
* @param fieldName
* the name of the field to check
* @return {@code true} if the field is supported, {@code false} otherwise
*/
public boolean isFieldSupported(final String fieldName)
{
ParameterCheck.mandatoryString("fieldName", fieldName);
final boolean supported = CONFIG_NAMES.contains(fieldName);
return supported;
}
/**
* Retrieves the expected type of value for a specific field. This operation will never return a class object for primitive types,
* instead replacing those with the class object for the corresponding wrapper type.
*
* @param fieldName
* the name of the field for which to retrieve the type of value
* @return the type of value for the field
*/
public Class<?> getFieldValueType(final String fieldName)
{
if (!this.isFieldSupported(fieldName))
{
throw new IllegalArgumentException(fieldName + " is not a supported field");
}
final Class<?> valueType = VALUE_TYPE_BY_CONFIG_NAME.get(fieldName);
return valueType;
}
/**
* Retrieves the configured value for a specific field. Default values inherent in the {@link AdapterConfig Keycloak classes} are not
* considered by this operation.
*
* @param fieldName
* the name of the field for which to retrieve the value
* @return the currently configured value for the field, or {@code null} if no value has been configured
*/
public Object getFieldValue(final String fieldName)
{
final Object value = this.configValueByField.get(fieldName);
return value;
}
/**
* Sets the configured value for a specific field.
*
* @param fieldName
* the name of the field for which to set the value
* @param value
* the value of the field to set
* @throws IllegalArgumentException
* if the field is {@link #isFieldSupported(String) not supported} or the type of value does not match the required type
*/
public void setFieldValue(final String fieldName, final Object value)
{
if (!this.isFieldSupported(fieldName))
{
throw new IllegalArgumentException(fieldName + " is not a supported field");
}
ParameterCheck.mandatory("value", value);
final Class<?> valueType = VALUE_TYPE_BY_CONFIG_NAME.get(fieldName);
if (!valueType.isInstance(value))
{
throw new IllegalArgumentException("Value is not an instance of " + valueType);
}
this.configValueByField.put(fieldName, value);
this.markedAsUnset.remove(fieldName);
}
/**
* Removes the configured value for a specific field.
*
* @param fieldName
* the name of the field for which to set the value
* @param markAsUnset
* {@code true} if the field should be marked as explicitly unset for the purpose of {@link #combine(ConfigElement) merging
* with other config elements}, {@code false} otherwise
* @throws IllegalArgumentException
* if the field is {@link #isFieldSupported(String) not supported}
*/
public void removeFieldValue(final String fieldName, final boolean markAsUnset)
{
if (!this.isFieldSupported(fieldName))
{
throw new IllegalArgumentException(fieldName + " is not a supported field");
}
this.configValueByField.remove(fieldName);
if (markAsUnset)
{
this.markedAsUnset.add(fieldName);
}
}
/**
* Retrieves the explicit {@code unset} flag of a field that may have been set via {@link #removeFieldValue(String, boolean) value
* removal}.
*
* @param fieldName
* the name of the field for which to retrieve the flag
* @return the value of the flag
*/
public boolean isFieldMarkedAsUnset(final String fieldName)
{
if (!this.isFieldSupported(fieldName))
{
throw new IllegalArgumentException(fieldName + " is not a supported field");
}
final boolean unset = this.markedAsUnset.contains(fieldName);
return unset;
}
/**
* Builds an instance of a Keycloak adapter configuration based on the configured values managed by this config element.
*
* @return the adapter configuration instance
*/
public AdapterConfig buildAdapterConfiguration()
{
final AdapterConfig config = new AdapterConfig();
try
{
for (final String configName : CONFIG_NAMES)
{
final Field field = FIELD_BY_CONFIG_NAME.get(configName);
final Object value = this.configValueByField.get(configName);
if (value != null)
{
// TODO Refactor towards use of setter to avoid setAccessible
field.setAccessible(true);
field.set(config, value);
}
}
}
catch (final IllegalAccessException ex)
{
throw new AlfrescoRuntimeException("Error building adapter configuration", ex);
}
return config;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public ConfigElement combine(final ConfigElement configElement)
{
if (!(configElement instanceof KeycloakAdapterConfigElement))
{
throw new IllegalArgumentException("Cannot combine with " + configElement);
}
final KeycloakAdapterConfigElement combined = new KeycloakAdapterConfigElement();
final KeycloakAdapterConfigElement otherConfigElement = (KeycloakAdapterConfigElement) configElement;
for (final String configName : CONFIG_NAMES)
{
final Object thisValue = this.getFieldValue(configName);
final Object otherValue = otherConfigElement.getFieldValue(configName);
if (otherValue != null)
{
Object valueToSet = otherValue;
if (thisValue instanceof Map<?, ?> && otherValue instanceof Map<?, ?>)
{
valueToSet = new HashMap<>((Map<?, ?>) thisValue);
((Map) valueToSet).putAll((Map) otherValue);
}
else if (otherValue instanceof Map<?, ?>)
{
valueToSet = new HashMap<>((Map<?, ?>) otherValue);
}
combined.setFieldValue(configName, valueToSet);
}
else if (otherConfigElement.isFieldMarkedAsUnset(configName) || this.isFieldMarkedAsUnset(configName))
{
combined.removeFieldValue(configName, true);
}
else if (thisValue != null)
{
combined.setFieldValue(configName, thisValue instanceof Map<?, ?> ? new HashMap<>((Map<?, ?>) thisValue) : thisValue);
}
}
if (otherConfigElement.connectionTimeout.isUnset())
{
combined.connectionTimeout.unset();
}
else
{
combined.setConnectionTimeout(otherConfigElement.getConnectionTimeout() != null ? otherConfigElement.getConnectionTimeout()
: this.getConnectionTimeout());
}
if (otherConfigElement.socketTimeout.isUnset())
{
combined.socketTimeout.unset();
}
else
{
combined.setSocketTimeout(
otherConfigElement.getSocketTimeout() != null ? otherConfigElement.getSocketTimeout() : this.getSocketTimeout());
}
return combined;
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
final StringBuilder builder = new StringBuilder();
builder.append("KeycloakAdapterConfigElement [");
builder.append("configValueByField=");
builder.append(this.configValueByField);
builder.append(",markedAsUnset=");
builder.append(this.markedAsUnset);
if (this.connectionTimeout != null)
{
builder.append(",connectionTimeout=");
builder.append(this.connectionTimeout);
}
if (this.connectionTimeout != null)
{
builder.append(",socketTimeout=");
builder.append(this.socketTimeout);
}
builder.append("]");
return builder.toString();
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = super.hashCode();
// use class-consistent order for actual config values
for (final String configName : CONFIG_NAMES)
{
final Object value = this.configValueByField.get(configName);
final int valueHash = value == null ? (this.markedAsUnset.contains(configName) ? -1 : 0) : value.hashCode();
result = prime * result + valueHash;
}
result = prime * result + (this.connectionTimeout != null ? this.connectionTimeout.hashCode() : 0);
result = prime * result + (this.socketTimeout != null ? this.socketTimeout.hashCode() : 0);
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(final Object obj)
{
if (this == obj)
{
return true;
}
if (!super.equals(obj))
{
return false;
}
if (!(obj instanceof KeycloakAdapterConfigElement))
{
return false;
}
final KeycloakAdapterConfigElement other = (KeycloakAdapterConfigElement) obj;
if (!EqualsHelper.nullSafeEquals(this.configValueByField, other.configValueByField))
{
return false;
}
if (!EqualsHelper.nullSafeEquals(this.markedAsUnset, other.markedAsUnset))
{
return false;
}
if (!EqualsHelper.nullSafeEquals(this.connectionTimeout, other.connectionTimeout))
{
return false;
}
if (!EqualsHelper.nullSafeEquals(this.socketTimeout, other.socketTimeout))
{
return false;
}
return true;
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright 2019 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.share.config;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.config.ConfigElement;
import org.springframework.extensions.config.xml.elementreader.ConfigElementReader;
/**
* @author Axel Faust
*/
public class KeycloakAdapterConfigElementReader implements ConfigElementReader
{
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAdapterConfigElementReader.class);
/**
* {@inheritDoc}
*/
@Override
public ConfigElement parse(final Element element)
{
final KeycloakAdapterConfigElement configElement = new KeycloakAdapterConfigElement();
@SuppressWarnings("unchecked")
final Iterator<Element> subElementIterator = element.elementIterator();
while (subElementIterator.hasNext())
{
final Element subElement = subElementIterator.next();
final String subElementName = subElement.getName();
if (configElement.isFieldSupported(subElementName))
{
final Class<?> valueType = configElement.getFieldValueType(subElementName);
if (Map.class.equals(valueType))
{
final Map<String, Object> configMap = new HashMap<>();
@SuppressWarnings("unchecked")
final Iterator<Element> mapElementIterator = subElement.elementIterator();
while (mapElementIterator.hasNext())
{
final Element mapElement = mapElementIterator.next();
final String key = mapElement.getName();
final String value = mapElement.getTextTrim();
configMap.put(key, value);
}
configElement.setFieldValue(subElementName, configMap);
}
else
{
final String textTrim = subElement.getTextTrim();
if (textTrim.isEmpty())
{
configElement.removeFieldValue(subElementName, true);
}
else if (Number.class.isAssignableFrom(valueType))
{
try
{
configElement.setFieldValue(subElementName,
valueType.getMethod("valueOf", String.class).invoke(null, textTrim));
}
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex)
{
LOGGER.error(
"Number-based value type {} does not provide a publicly accessible, static valueOf to handle conversion of value {}",
valueType, textTrim);
throw new AlfrescoRuntimeException("Failed to convert configuration value " + textTrim, ex);
}
}
else if (Boolean.class.equals(valueType))
{
configElement.setFieldValue(subElementName, Boolean.valueOf(textTrim));
}
else if (Character.class.equals(valueType))
{
if (textTrim.length() > 1)
{
throw new IllegalStateException("Value " + textTrim + " has more than one character");
}
configElement.setFieldValue(subElementName, new Character(textTrim.charAt(0)));
}
else if (String.class.equals(valueType))
{
configElement.setFieldValue(subElementName, textTrim);
}
else
{
throw new UnsupportedOperationException("Unsupported value type " + valueType);
}
}
}
else
{
switch (subElementName)
{
// use -1 as dummy value for empty value to signify that empty value has explicitly been set (relevant for merge/combine
// of config)
case "connectionTimeout":
final String prospectiveConnectionTimeout = subElement.getTextTrim();
configElement.setConnectionTimeout(
prospectiveConnectionTimeout.isEmpty() ? null : Long.valueOf(prospectiveConnectionTimeout));
break;
case "socketTimeout":
final String prospectiveSocketTimeout = subElement.getTextTrim();
configElement.setSocketTimeout(prospectiveSocketTimeout.isEmpty() ? null : Long.valueOf(prospectiveSocketTimeout));
break;
default:
LOGGER.warn("Encountered unsupported Keycloak Adapter config element {}", subElementName);
}
}
}
LOGGER.debug("Read configuration element {} from XML section", configElement);
return configElement;
}
}

View File

@ -0,0 +1,263 @@
/*
* Copyright 2019 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.share.config;
import org.springframework.extensions.config.ConfigElement;
import de.acosix.alfresco.utility.share.config.BaseCustomConfigElement;
import de.acosix.alfresco.utility.share.config.ConfigValueHolder;
/**
*
* @author Axel Faust
*/
public class KeycloakAuthenticationConfigElement extends BaseCustomConfigElement
{
private static final long serialVersionUID = 8587583775593697136L;
public static final String NAME = "keycloak-auth-config";
protected final ConfigValueHolder<Boolean> enhanceLoginForm = new ConfigValueHolder<>();
protected final ConfigValueHolder<Boolean> enableSsoFilter = new ConfigValueHolder<>();
protected final ConfigValueHolder<Boolean> forceKeycloakSso = new ConfigValueHolder<>();
protected final ConfigValueHolder<Integer> bodyBufferLimit = new ConfigValueHolder<>();
protected final ConfigValueHolder<Integer> sslRedirectPort = new ConfigValueHolder<>();
protected final ConfigValueHolder<Integer> sessionMapperLimit = new ConfigValueHolder<>();
/**
* Creates a new instance of this class.
*/
public KeycloakAuthenticationConfigElement()
{
super(NAME);
}
/**
* @param enhanceLoginForm
* the enhanceLoginForm to set
*/
public void setEnhanceLoginForm(final Boolean enhanceLoginForm)
{
this.enhanceLoginForm.setValue(enhanceLoginForm);
}
/**
* @return the enhanceLoginForm
*/
public Boolean getEnhanceLoginForm()
{
return this.enhanceLoginForm.getValue();
}
/**
* @param enableSsoFilter
* the enableSsoFilter to set
*/
public void setEnableSsoFilter(final Boolean enableSsoFilter)
{
this.enableSsoFilter.setValue(enableSsoFilter);
}
/**
* @return the enhanceSsoFilter
*/
public Boolean getEnableSsoFilter()
{
return this.enableSsoFilter.getValue();
}
/**
* @param forceKeycloakSso
* the forceKeycloakSso to set
*/
public void setForceKeycloakSso(final Boolean forceKeycloakSso)
{
this.forceKeycloakSso.setValue(forceKeycloakSso);
}
/**
* @return the forceKeycloakSso
*/
public Boolean getForceKeycloakSso()
{
return this.forceKeycloakSso.getValue();
}
/**
* @param bodyBufferLimit
* the bodyBufferLimit to set
*/
public void setBodyBufferLimit(final Integer bodyBufferLimit)
{
this.bodyBufferLimit.setValue(bodyBufferLimit);
}
/**
* @return the bodyBufferLimit
*/
public Integer getBodyBufferLimit()
{
return this.bodyBufferLimit.getValue();
}
/**
* @param sslRedirectPort
* the sslRedirectPort to set
*/
public void setSslRedirectPort(final Integer sslRedirectPort)
{
this.sslRedirectPort.setValue(sslRedirectPort);
}
/**
* @return the sslRedirectPort
*/
public Integer getSslRedirectPort()
{
return this.sslRedirectPort.getValue();
}
/**
* @param sessionMapperLimit
* the sessionMapperLimit to set
*/
public void setSessionMapperLimit(final Integer sessionMapperLimit)
{
this.sessionMapperLimit.setValue(sessionMapperLimit);
}
/**
* @return the sessionMapperLimit
*/
public Integer getSessionMapperLimit()
{
return this.sessionMapperLimit.getValue();
}
/**
*
* {@inheritDoc}
*/
@Override
public ConfigElement combine(final ConfigElement configElement)
{
if (!(configElement instanceof KeycloakAuthenticationConfigElement))
{
throw new IllegalArgumentException("Cannot combine with " + configElement);
}
final KeycloakAuthenticationConfigElement combined = new KeycloakAuthenticationConfigElement();
final KeycloakAuthenticationConfigElement otherConfigElement = (KeycloakAuthenticationConfigElement) configElement;
if (otherConfigElement.enhanceLoginForm.isUnset())
{
combined.enhanceLoginForm.unset();
}
else
{
combined.setEnhanceLoginForm(otherConfigElement.getEnhanceLoginForm() != null ? otherConfigElement.getEnhanceLoginForm()
: this.getEnhanceLoginForm());
}
if (otherConfigElement.enhanceLoginForm.isUnset())
{
combined.enhanceLoginForm.unset();
}
else
{
combined.setEnableSsoFilter(
otherConfigElement.getEnableSsoFilter() != null ? otherConfigElement.getEnableSsoFilter() : this.getEnableSsoFilter());
}
if (otherConfigElement.forceKeycloakSso.isUnset())
{
combined.forceKeycloakSso.unset();
}
else
{
combined.setForceKeycloakSso(otherConfigElement.getForceKeycloakSso() != null ? otherConfigElement.getForceKeycloakSso()
: this.getForceKeycloakSso());
}
if (otherConfigElement.bodyBufferLimit.isUnset())
{
combined.bodyBufferLimit.unset();
}
else
{
combined.setBodyBufferLimit(
otherConfigElement.getBodyBufferLimit() != null ? otherConfigElement.getBodyBufferLimit() : this.getBodyBufferLimit());
}
if (otherConfigElement.sslRedirectPort.isUnset())
{
combined.sslRedirectPort.unset();
}
else
{
combined.setSslRedirectPort(
otherConfigElement.getSslRedirectPort() != null ? otherConfigElement.getSslRedirectPort() : this.getSslRedirectPort());
}
if (otherConfigElement.sessionMapperLimit.isUnset())
{
combined.sessionMapperLimit.unset();
}
else
{
combined.setSessionMapperLimit(otherConfigElement.getSessionMapperLimit() != null ? otherConfigElement.getSessionMapperLimit()
: this.getSessionMapperLimit());
}
return combined;
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
final StringBuilder builder = new StringBuilder();
builder.append("KeycloakAuthenticationConfigElement [");
builder.append("enhanceLoginForm=");
builder.append(this.enhanceLoginForm);
builder.append(", ");
builder.append("enableSsoFilter=");
builder.append(this.enableSsoFilter);
builder.append(", ");
builder.append("forceKeycloakSso=");
builder.append(this.forceKeycloakSso);
builder.append(", ");
builder.append("bodyBufferLimit=");
builder.append(this.bodyBufferLimit);
builder.append(", ");
builder.append("sslRedirectPort=");
builder.append(this.sslRedirectPort);
builder.append(", ");
builder.append("sessionMapperLimit=");
builder.append(this.sessionMapperLimit);
builder.append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright 2019 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.share.config;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.config.ConfigElement;
import org.springframework.extensions.config.xml.elementreader.ConfigElementReader;
/**
* @author Axel Faust
*/
public class KeycloakAuthenticationConfigElementReader implements ConfigElementReader
{
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationConfigElementReader.class);
/**
* {@inheritDoc}
*/
@Override
public ConfigElement parse(final Element element)
{
final KeycloakAuthenticationConfigElement configElement = new KeycloakAuthenticationConfigElement();
final Element enhanceLoginForm = element.element("enhance-login-form");
if (enhanceLoginForm != null)
{
final String value = enhanceLoginForm.getTextTrim();
configElement.setEnhanceLoginForm(value.isEmpty() ? null : Boolean.valueOf(value));
}
final Element enableSsoFilter = element.element("enable-sso-filter");
if (enableSsoFilter != null)
{
final String value = enableSsoFilter.getTextTrim();
configElement.setEnableSsoFilter(value.isEmpty() ? null : Boolean.valueOf(value));
}
final Element forceKeycloakSso = element.element("force-keycloak-sso");
if (forceKeycloakSso != null)
{
final String value = forceKeycloakSso.getTextTrim();
configElement.setForceKeycloakSso(value.isEmpty() ? null : Boolean.valueOf(value));
}
final Element bodyBufferLimit = element.element("body-buffer-limit");
if (bodyBufferLimit != null)
{
final String value = bodyBufferLimit.getTextTrim();
configElement.setBodyBufferLimit(value.isEmpty() ? null : Integer.valueOf(value));
}
final Element sslRedirectPort = element.element("ssl-redirect-port");
if (sslRedirectPort != null)
{
final String value = sslRedirectPort.getTextTrim();
configElement.setSslRedirectPort(value.isEmpty() ? null : Integer.valueOf(value));
}
final Element sessionMapperLimit = element.element("session-mapper-limit");
if (sessionMapperLimit != null)
{
final String value = sessionMapperLimit.getTextTrim();
configElement.setSessionMapperLimit(value.isEmpty() ? null : Integer.valueOf(value));
}
LOGGER.debug("Read configuration element {} from XML section", configElement);
return configElement;
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2019 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.share.remote;
import java.util.Collections;
import org.alfresco.web.site.servlet.SlingshotAlfrescoConnector;
import org.springframework.extensions.config.RemoteConfigElement.ConnectorDescriptor;
import org.springframework.extensions.webscripts.connector.ConnectorContext;
import org.springframework.extensions.webscripts.connector.ConnectorSession;
import org.springframework.extensions.webscripts.connector.RemoteClient;
/**
* @author Axel Faust
*/
public class BearerTokenAwareSlingshotAlfrescoConnector extends SlingshotAlfrescoConnector
{
public static final String CS_PARAM_BEARER_TOKEN = "bearerToken";
/**
* Constructs a new instance of this class.
*
* @param descriptor
* the descriptor / configuration of this connector
* @param endpoint
* the endpoint with which this connector instance should connect
*/
public BearerTokenAwareSlingshotAlfrescoConnector(final ConnectorDescriptor descriptor, final String endpoint)
{
super(descriptor, endpoint);
}
/**
*
* {@inheritDoc}
*/
@Override
protected void applyRequestHeaders(final RemoteClient remoteClient, final ConnectorContext context)
{
// apply default mapping of headers
super.applyRequestHeaders(remoteClient, context);
final ConnectorSession connectorSession = this.getConnectorSession();
if (connectorSession != null)
{
final String bearerToken = connectorSession.getParameter(CS_PARAM_BEARER_TOKEN);
if (bearerToken != null && !bearerToken.trim().isEmpty())
{
remoteClient.setRequestProperties(Collections.singletonMap("Authorization", "Bearer " + bearerToken));
}
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2019 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.share.spring;
import org.alfresco.util.PropertyCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import de.acosix.alfresco.keycloak.share.web.KeycloakAuthenticationFilter;
/**
* @author Axel Faust
*/
public class KeycloakAuthenticationFilterActivation implements BeanDefinitionRegistryPostProcessor, InitializingBean
{
private static final String DEFAULT_SSO_AUTHENTICATION_FILTER_NAME = "SSOAuthenticationFilter";
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationFilterActivation.class);
protected String moduleId;
/**
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "moduleId", this.moduleId);
}
/**
* @param moduleId
* the moduleId to set
*/
public void setModuleId(final String moduleId)
{
this.moduleId = moduleId;
}
/**
* {@inheritDoc}
*/
@Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException
{
// NO-OP
}
/**
* {@inheritDoc}
*/
@Override
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) throws BeansException
{
final String keycloakFilterBeanName = this.moduleId + "." + KeycloakAuthenticationFilter.class.getSimpleName();
if (registry.containsBeanDefinition(keycloakFilterBeanName))
{
// re-register default filter under different name
final BeanDefinition defaultSsoAuthenticationFilter = registry.getBeanDefinition(DEFAULT_SSO_AUTHENTICATION_FILTER_NAME);
registry.removeBeanDefinition(DEFAULT_SSO_AUTHENTICATION_FILTER_NAME);
final String defaultSsoAuthenticationFilterReplacementName = this.moduleId + ".default"
+ DEFAULT_SSO_AUTHENTICATION_FILTER_NAME;
registry.registerBeanDefinition(defaultSsoAuthenticationFilterReplacementName, defaultSsoAuthenticationFilter);
// re-register our filter under default name
final BeanDefinition keycloakSsoAuthenticationFilter = registry.getBeanDefinition(keycloakFilterBeanName);
registry.removeBeanDefinition(keycloakFilterBeanName);
((GenericBeanDefinition) keycloakSsoAuthenticationFilter).setAbstract(false);
keycloakSsoAuthenticationFilter.getPropertyValues().add("defaultSsoFilter",
new RuntimeBeanReference(defaultSsoAuthenticationFilterReplacementName));
registry.registerBeanDefinition(DEFAULT_SSO_AUTHENTICATION_FILTER_NAME, keycloakSsoAuthenticationFilter);
}
else
{
LOGGER.error("Cannot activate KeycloakAuthenticationFilter bean as abstract bean {} was not found in Spring context",
keycloakFilterBeanName);
}
}
}

View File

@ -0,0 +1,316 @@
/*
* Copyright 2019 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.share.web;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.extensions.config.ConfigService;
import de.acosix.alfresco.keycloak.share.config.KeycloakAuthenticationConfigElement;
/**
* This implementation of a {@link SessionIdMapper Keycloak session ID mapper} is based on the {@link InMemorySessionIdMapper in-memory
* mapper}, but uses a better model for synchronization and respects configured size limits, ejecting least-recently active sessions first.
* Activity of session with regards to being determined the "least-recently active" session is based upon validation calls to
* {@link #hasSession(String) hasSession}.
*
* @author Axel Faust
*/
public class DefaultSessionIdMapper implements SessionIdMapper, InitializingBean
{
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSessionIdMapper.class);
private static final int DEFAULT_SESSION_COUNT_LIMIT = 1000;
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
protected final Map<String, String> ssoToSession = new HashMap<>();
protected final Map<String, String> sessionToSso = new HashMap<>();
protected final Map<String, Set<String>> principalToSession = new HashMap<>();
protected final Map<String, String> sessionToPrincipal = new HashMap<>();
protected ConfigService configService;
protected int sessionCountLimit = DEFAULT_SESSION_COUNT_LIMIT;
protected Set<String> sessionUsedOrder;
/**
*
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
if (this.configService != null)
{
final KeycloakAuthenticationConfigElement keycloakAuthConfig = (KeycloakAuthenticationConfigElement) this.configService
.getConfig("Keycloak").getConfigElement(KeycloakAuthenticationConfigElement.NAME);
final Integer sessionMapperLimit = keycloakAuthConfig.getSessionMapperLimit();
if (sessionMapperLimit != null)
{
this.sessionCountLimit = sessionMapperLimit.intValue();
}
}
if (this.sessionCountLimit <= 0)
{
LOGGER.warn(
"Session count limit is set to {} - session ID mapper will not restrict size of internal data structures (this can cause OOMEs)",
this.sessionCountLimit);
}
else
{
this.sessionUsedOrder = new LinkedHashSet<>();
}
}
/**
* @param configService
* the configService to set
*/
public void setConfigService(final ConfigService configService)
{
this.configService = configService;
}
/**
* @param sessionCountLimit
* the sessionCountLimit to set
*/
public void setSessionCountLimit(final int sessionCountLimit)
{
this.sessionCountLimit = sessionCountLimit;
}
@Override
public boolean hasSession(final String id)
{
this.lock.readLock().lock();
try
{
LOGGER.debug("Checking hasSession for {}", id);
final boolean hasSession = this.sessionToSso.containsKey(id) || this.sessionToPrincipal.containsKey(id);
LOGGER.debug("Session {}", hasSession ? "is mapped" : "is not mapped");
if (hasSession && this.sessionCountLimit > 0)
{
synchronized (this.sessionUsedOrder)
{
this.sessionUsedOrder.remove(id);
this.sessionUsedOrder.add(id);
}
}
return hasSession;
}
finally
{
this.lock.readLock().unlock();
}
}
/**
*
* {@inheritDoc}
*/
@Override
public void clear()
{
this.lock.writeLock().lock();
try
{
LOGGER.info("Clearing all mappings");
this.ssoToSession.clear();
this.sessionToSso.clear();
this.principalToSession.clear();
this.sessionToPrincipal.clear();
}
finally
{
this.lock.writeLock().unlock();
}
}
/**
*
* {@inheritDoc}
*/
@Override
public Set<String> getUserSessions(final String principal)
{
Set<String> userSessions;
this.lock.readLock().lock();
try
{
LOGGER.debug("Retrieving user sessions for {}", principal);
final Set<String> lookup = this.principalToSession.get(principal);
if (lookup != null)
{
userSessions = new HashSet<>();
userSessions.addAll(lookup);
}
else
{
userSessions = Collections.emptySet();
}
}
finally
{
this.lock.readLock().unlock();
}
LOGGER.debug("Principal {} is mapped to sessions {}", principal, userSessions);
return userSessions;
}
/**
*
* {@inheritDoc}
*/
@Override
public String getSessionFromSSO(final String sso)
{
this.lock.readLock().lock();
try
{
return this.ssoToSession.get(sso);
}
finally
{
this.lock.readLock().unlock();
}
}
/**
*
* {@inheritDoc}
*/
@Override
public void map(final String sso, final String principal, final String session)
{
this.lock.writeLock().lock();
try
{
LOGGER.debug("Adding mapping ({}, {}, {})", sso, principal, session);
if (sso != null)
{
this.ssoToSession.put(sso, session);
this.sessionToSso.put(session, sso);
}
if (principal != null)
{
this.principalToSession.compute(principal, (key, value) -> {
if (value == null)
{
value = new HashSet<>();
}
value.add(session);
return value;
});
this.sessionToPrincipal.put(session, principal);
}
if (this.sessionCountLimit > 0 && sso != null && principal != null)
{
synchronized (this.sessionUsedOrder)
{
this.sessionUsedOrder.add(session);
final int sessionsToRemove = this.sessionUsedOrder.size() - this.sessionCountLimit;
if (sessionsToRemove == 1)
{
final String sessionToRemove = this.sessionUsedOrder.iterator().next();
this.removeSession(sessionToRemove);
}
// should really not happen, but in place should we ever switch to a more bulk-handling
else if (sessionsToRemove > 0)
{
final List<String> sessionsForRemoval = new ArrayList<>(this.sessionUsedOrder).subList(0, sessionsToRemove);
sessionsForRemoval.forEach(this::removeSession);
}
}
}
}
finally
{
this.lock.writeLock().unlock();
}
}
/**
*
* {@inheritDoc}
*/
@Override
public void removeSession(final String session)
{
this.lock.writeLock().lock();
try
{
LOGGER.debug("Removing session {}", session);
final String sso = this.sessionToSso.remove(session);
if (sso != null)
{
this.ssoToSession.remove(sso);
}
final String principal = this.sessionToPrincipal.remove(session);
if (principal != null)
{
this.principalToSession.computeIfPresent(principal, (key, value) -> {
value.remove(session);
if (value.isEmpty())
{
value = null;
}
return value;
});
}
if (this.sessionCountLimit > 0)
{
synchronized (this.sessionUsedOrder)
{
this.sessionUsedOrder.remove(session);
}
}
}
finally
{
this.lock.writeLock().unlock();
}
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright 2019 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.share.web;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.alfresco.util.Pair;
import org.keycloak.adapters.servlet.ServletHttpFacade;
import org.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
* authenticators are captured, and otherwise no output is written to the servlet response. This is required for some scenarios in which a
* redirect action should be included in the login form.
*
* @author Axel Faust
*/
public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
{
protected final Map<Pair<String, String>, javax.servlet.http.Cookie> cookies = new HashMap<>();
protected final Map<String, List<String>> headers = new HashMap<>();
/**
* Creates a new instance of this class for the provided servlet request.
*
* @param request
* the servlet request to facade
*/
public RedirectCaptureServletHttpFacade(final HttpServletRequest request)
{
super(request, null);
}
/**
*
* {@inheritDoc}
*/
@Override
public Response getResponse()
{
return new ResponseCaptureFacade();
}
/**
* @return the cookies
*/
public List<javax.servlet.http.Cookie> getCookies()
{
return new ArrayList<>(this.cookies.values());
}
/**
* @return the headers
*/
public Map<String, List<String>> getHeaders()
{
final Map<String, List<String>> headers = new HashMap<>();
this.headers.forEach((headerName, values) -> headers.put(headerName, new ArrayList<>(values)));
return headers;
}
/**
*
* @author Axel Faust
*/
private class ResponseCaptureFacade implements Response
{
/**
*
* {@inheritDoc}
*/
@Override
public void setStatus(final int status)
{
// NO-OP
}
/**
*
* {@inheritDoc}
*/
@Override
public void addHeader(final String name, final String value)
{
RedirectCaptureServletHttpFacade.this.headers.computeIfAbsent(name, key -> new ArrayList<>()).add(value);
}
/**
*
* {@inheritDoc}
*/
@Override
public void setHeader(final String name, final String value)
{
RedirectCaptureServletHttpFacade.this.headers.put(name, new ArrayList<>(Collections.singleton(value)));
}
/**
*
* {@inheritDoc}
*/
@Override
public void resetCookie(final String name, final String path)
{
RedirectCaptureServletHttpFacade.this.cookies.remove(new Pair<>(name, path));
}
/**
*
* {@inheritDoc}
*/
@Override
public void setCookie(final String name, final String value, final String path, final String domain, final int maxAge,
final boolean secure, final boolean httpOnly)
{
final javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(name, value);
cookie.setPath(path);
if (domain != null)
{
cookie.setDomain(domain);
}
cookie.setMaxAge(maxAge);
cookie.setSecure(secure);
cookie.setHttpOnly(httpOnly);
RedirectCaptureServletHttpFacade.this.cookies.put(new Pair<>(name, path), cookie);
}
/**
*
* {@inheritDoc}
*/
@Override
public OutputStream getOutputStream()
{
return new ByteArrayOutputStream();
}
/**
*
* {@inheritDoc}
*/
@Override
public void sendError(final int code)
{
// NO-OP
}
/**
*
* {@inheritDoc}
*/
@Override
public void sendError(final int code, final String message)
{
// NO-OP
}
/**
*
* {@inheritDoc}
*/
@Override
public void end()
{
// NO-OP
}
}
}

View File

@ -0,0 +1,261 @@
/*
* Copyright 2019 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.share.web;
import java.io.IOException;
import java.util.Date;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.PropertyCheck;
import org.alfresco.web.site.servlet.SlingshotLoginController;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
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 org.springframework.extensions.config.ConfigElement;
import org.springframework.extensions.config.ConfigService;
import org.springframework.extensions.surf.RequestContext;
import org.springframework.extensions.surf.RequestContextUtil;
import org.springframework.extensions.surf.exception.ConnectorServiceException;
import org.springframework.extensions.surf.exception.RequestContextException;
import org.springframework.extensions.surf.site.AuthenticationUtil;
import org.springframework.extensions.surf.support.AlfrescoUserFactory;
import org.springframework.extensions.surf.support.ThreadLocalRequestContext;
import org.springframework.extensions.surf.util.URLEncoder;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.connector.Connector;
import org.springframework.extensions.webscripts.connector.ConnectorContext;
import org.springframework.extensions.webscripts.connector.ConnectorService;
import org.springframework.extensions.webscripts.connector.HttpMethod;
import org.springframework.extensions.webscripts.connector.Response;
import org.springframework.extensions.webscripts.servlet.DependencyInjectedFilter;
/**
* This filter performs the initial load of user groups for any user authenticated by a filter preceeding it in the filter chain, and
* transparently refreshes the user groups after a configurable amount of time has past, in order to avoid Share user groups to become stale
* / inconsistent with actual group memberships in the Alfresco Repository. This filter is necessary since the default logic for the simple
* initialisation inside {@link SlingshotLoginController} is inaccessible to custom authentication filters, and there is actually no refresh
* functionality in default Alfresco at all, which can be problematic for SSO-authenticated sessions that may be active for a long time.
*
* @author Axel Faust
*/
public class UserGroupsLoadFilter implements DependencyInjectedFilter, InitializingBean, ApplicationContextAware
{
public static final String SESSION_ATTRIBUTE_KEY_USER_GROUPS_LAST_LOADED = SlingshotLoginController.SESSION_ATTRIBUTE_KEY_USER_GROUPS
+ "_lastLoaded";
private static final Logger LOGGER = LoggerFactory.getLogger(UserGroupsLoadFilter.class);
private static final long DEFAULT_CACHED_USER_GROUPS_TIMEOUT = 60000;
protected ApplicationContext applicationContext;
protected ConfigService configService;
protected ConnectorService connectorService;
/**
* {@inheritDoc}
*/
@Override
public void setApplicationContext(final ApplicationContext applicationContext)
{
this.applicationContext = applicationContext;
}
/**
*
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "applicationContext", this.applicationContext);
PropertyCheck.mandatory(this, "configService", this.configService);
PropertyCheck.mandatory(this, "connectorService", this.connectorService);
}
/**
* @param configService
* the configService to set
*/
public void setConfigService(final ConfigService configService)
{
this.configService = configService;
}
/**
* @param connectorService
* the connectorService to set
*/
public void setConnectorService(final ConnectorService connectorService)
{
this.connectorService = connectorService;
}
/**
*
* {@inheritDoc}
*/
@Override
public void doFilter(final ServletContext context, final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException
{
if (request instanceof HttpServletRequest)
{
final HttpSession session = ((HttpServletRequest) request).getSession(false);
if (session != null)
{
final String userId = AuthenticationUtil.getUserId((HttpServletRequest) request);
final String userGroupsCSVList = (String) session.getAttribute(SlingshotLoginController.SESSION_ATTRIBUTE_KEY_USER_GROUPS);
final Date lastLoaded = (Date) session.getAttribute(SESSION_ATTRIBUTE_KEY_USER_GROUPS_LAST_LOADED);
long cachedUserGroupsTimeout = DEFAULT_CACHED_USER_GROUPS_TIMEOUT;
final ConfigElement userConfig = this.configService.getGlobalConfig().getConfigElement("user");
if (userConfig != null)
{
final String timeoutConfig = userConfig.getChildValue("cached-user-groups-timeout");
if (timeoutConfig != null)
{
cachedUserGroupsTimeout = Long.parseLong(timeoutConfig, 10);
}
}
if (userId != null)
{
if (userGroupsCSVList == null
|| (lastLoaded != null && lastLoaded.getTime() + cachedUserGroupsTimeout < System.currentTimeMillis()))
{
session.setAttribute(SlingshotLoginController.SESSION_ATTRIBUTE_KEY_USER_GROUPS,
this.loadUserGroupsCSVList((HttpServletRequest) request, session, userId));
session.setAttribute(SESSION_ATTRIBUTE_KEY_USER_GROUPS_LAST_LOADED, new Date());
}
else if (lastLoaded == null)
{
// might have just been loaded by an authentication filter on initial login
session.setAttribute(SESSION_ATTRIBUTE_KEY_USER_GROUPS_LAST_LOADED, new Date());
}
}
}
}
chain.doFilter(request, response);
}
/**
* Loads the groups a user is a member of as a comma-separated list from the default Alfresco backend.
*
* @param request
* the HTTP servlet request
* @param session
* the current session
* @param userId
* the ID of the user for which to load the group memberships
* @return the list of groups the user is a member of as a comma-separated list of names
*/
protected String loadUserGroupsCSVList(final HttpServletRequest request, final HttpSession session, final String userId)
{
String userGroupsCSVList;
try
{
// logic nearly identical to SlingshotLoginController
final Connector connector = this.connectorService.getConnector(AlfrescoUserFactory.ALFRESCO_ENDPOINT_ID, userId, session);
// bug in default Alfresco RequestCachingConnector: with ConnectorContext having HttpMethod.GET, null check of
// ThreadLocalRequestContext.getRequestContext() is short-circuited, causing NPE on access
final RequestContext requestContext = ThreadLocalRequestContext.getRequestContext();
if (requestContext == null)
{
try
{
RequestContextUtil.initRequestContext(this.applicationContext, request, true);
}
catch (final RequestContextException e)
{
LOGGER.error("Failed to initialise request context", e);
throw new AlfrescoRuntimeException("Failed to initialise request context", e);
}
}
final ConnectorContext c = new ConnectorContext(HttpMethod.GET);
c.setContentType("application/json");
final Response res = connector.call("/api/people/" + URLEncoder.encode(userId) + "?groups=true", c);
if (res.getStatus().getCode() == Status.STATUS_OK)
{
final String responseText = res.getResponse();
final JSONParser jsonParser = new JSONParser();
final Object userData = jsonParser.parse(responseText.toString());
final StringBuilder groups = new StringBuilder(512);
if (userData instanceof JSONObject)
{
final Object groupsArray = ((JSONObject) userData).get("groups");
if (groupsArray instanceof JSONArray)
{
for (final Object groupData : (JSONArray) groupsArray)
{
if (groupData instanceof JSONObject)
{
final Object groupName = ((JSONObject) groupData).get("itemName");
if (groupName != null)
{
if (groups.length() > 0)
{
groups.append(',');
}
groups.append(groupName.toString());
}
}
}
}
}
userGroupsCSVList = groups.toString();
LOGGER.debug("Retrieved group memberships for user {}: {}", userId, userGroupsCSVList);
}
else
{
LOGGER.warn("Failed to load user groups for {} with backend call resulting in HTTP {} response and message {}", userId,
res.getStatus().getCode(), res.getStatus().getMessage());
userGroupsCSVList = "";
}
}
catch (final ConnectorServiceException | ParseException ex)
{
LOGGER.error("Failed to load user groups for {}", userId, ex);
userGroupsCSVList = "";
}
return userGroupsCSVList;
}
}

View File

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2019 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.
-->
<web-fragment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd" id="WebAppFragment_ID" version="3.0">
<!-- Servlet 3.0 Spec does not support ordering filter/filter-mapping before those from web.xml -->
<!-- So this only includes filters that do not rely on ordering -->
<name>${moduleId}.AddonFilters</name>
<ordering>
<after>
<others />
</after>
</ordering>
<filter>
<filter-name>>${moduleId}.UserGroupsLoadFilter</filter-name>
<filter-class>org.springframework.extensions.webscripts.servlet.BeanProxyFilter</filter-class>
<init-param>
<param-name>beanName</param-name>
<param-value>${moduleId}.UserGroupsLoadFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>>${moduleId}.UserGroupsLoadFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-fragment>

View File

@ -0,0 +1,43 @@
<#--
Copyright 2019 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.
-->
<#function showRedirectForm>
<#local showRedirect = false />
<#local authConfig = config.scoped['Keycloak']['keycloak-auth-config'] />
<#if authConfig?? && authConfig.enhanceLoginForm??>
<#local showRedirect = authConfig.enhanceLoginForm />
</#if>
<#return showRedirect />
</#function>
<#if keycloakRedirectUriModel?? && showRedirectForm()>
<@markup id="oidc-redirect-button" target="form" action="after">
<#assign el = args.htmlid?html>
<#-- reuse CSS for consistent look&feel -->
<form action="${keycloakRedirectUriModel.baseUrl}" method="get" class="form-fields login">
<#list keycloakRedirectUriModel.parameters as parameter>
<input type="hidden" name="${parameter.name?html}" value="${parameter.value?html}" />
</#list>
<div class="form-field">
<#-- YUI style button without extra client-side JS code -->
<span class="yui-button yui-submit-button">
<span class="first-child">
<button type="submit" tabindex="0" id="${el}-oidcRedirect">${msg('button.oidc-sso')?html}</button>
</span>
</span>
</div>
</form>
</@markup>
</#if>

View File

@ -0,0 +1,62 @@
/*
* Copyright 2019 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.
*/
function main()
{
var keycloakRedirectUrl, keycloakRedirectUriModel, parameters, idx, parameter, parameterModel;
// redirect URL is already pre-constructed for a simple <a> redirect
// we want to support a form-based redirect for UI consistency, so need to deconstruct and even decode (parts of) URL
keycloakRedirectUrl = Packages.de.acosix.alfresco.keycloak.share.web.KeycloakAuthenticationFilter.getLoginRedirectUrl();
if (keycloakRedirectUrl !== null)
{
// make sure it is a JS string, not Java string
keycloakRedirectUrl = String(keycloakRedirectUrl);
keycloakRedirectUriModel = {
baseUrl : keycloakRedirectUrl.substring(0, keycloakRedirectUrl.indexOf('?')),
parameters : []
};
parameters = keycloakRedirectUrl.substring(keycloakRedirectUrl.indexOf('?') + 1).split(/&/);
for (idx = 0; idx < parameters.length; idx++)
{
parameter = parameters[idx];
if (parameter !== '' && parameter.indexOf('=') !== -1)
{
parameterModel = {
name : parameter.substring(0, parameter.indexOf('=')),
value : null
};
if (parameterModel.name === 'redirect_uri')
{
parameterModel.value = decodeURIComponent(parameter.substring(parameter.indexOf('=') + 1));
if (parameterModel.value.indexOf('?') !== -1)
{
parameterModel.value = parameterModel.value.substring(0, parameterModel.value.indexOf('?'));
}
}
else
{
parameterModel.value = parameter.substring(parameter.indexOf('=') + 1);
}
keycloakRedirectUriModel.parameters.push(parameterModel);
}
}
model.keycloakRedirectUriModel = keycloakRedirectUriModel;
}
}
main();

View File

@ -0,0 +1 @@
button.oidc-sso=Login via SSO

View File

@ -0,0 +1 @@
button.oidc-sso=\u00dcber SSO anmelden

View File

@ -0,0 +1,42 @@
var userMenu, otherMenuGroup, logoutItem;
if (model.jsonModel && model.jsonModel.widgets
&& Packages.de.acosix.alfresco.keycloak.share.web.KeycloakAuthenticationFilter.isAuthenticatedByKeycloak())
{
// default Share does not show logout action for externally authenticated users
// but with Keycloak we can actually support it, so add it (if missing and user menu has not been removed)
userMenu = widgetUtils.findObject(model.jsonModel.widgets, 'id', 'HEADER_USER_MENU');
if (userMenu)
{
otherMenuGroup = widgetUtils.findObject(model.jsonModel.widgets, 'id', 'HEADER_USER_MENU_OTHER_GROUP');
if (!otherMenuGroup)
{
otherMenuGroup = {
id : 'HEADER_USER_MENU_OTHER_GROUP',
name : 'alfresco/menus/AlfMenuGroup',
config : {
label : 'group.other.label',
widgets : [],
additionalCssClasses : 'alf-menu-group-no-label'
}
};
userMenu.config.widgets.push(otherMenuGroup);
}
logoutItem = widgetUtils.findObject(model.jsonModel.widgets, 'id', 'HEADER_USER_MENU_LOGOUT');
if (!logoutItem)
{
otherMenuGroup.config.widgets.push({
id : 'HEADER_USER_MENU_LOGOUT',
name : 'alfresco/header/AlfMenuItem',
config : {
id : 'HEADER_USER_MENU_LOGOUT',
label : 'logout.label',
iconClass : 'alf-user-logout-icon',
publishTopic : 'ALF_DOLOGOUT'
}
});
}
}
}

View File

View File

View File

@ -0,0 +1,47 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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>repository-it-docker</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<!--
<dependencySet>
<outputDirectory>WEB-INF/lib</outputDirectory>
<includes>
</includes>
<scope>compile</scope>
</dependencySet>
-->
<dependencySet>
<outputDirectory>WEB-INF/lib</outputDirectory>
<includes>
<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.quartz2:*</include>
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:*</include>
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*</include>
</includes>
<scope>test</scope>
</dependencySet>
</dependencySets>
</assembly>

View File

@ -0,0 +1,65 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Copyright 2019 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>share-it-docker</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>WEB-INF/lib</outputDirectory>
<includes>
<include>${project.artifactId}-${project.version}-installable.jar</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/test/resources</directory>
<outputDirectory>WEB-INF/classes</outputDirectory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtered>true</filtered>
<lineEnding>lf</lineEnding>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>WEB-INF/lib</outputDirectory>
<includes>
<include>org.keycloak:*</include>
<include>org.jboss.logging:*</include>
<include>org.bouncycastle:*</include>
<include>com.fasterxml.jackson.core:*</include>
</includes>
<scope>compile</scope>
</dependencySet>
<dependencySet>
<outputDirectory>WEB-INF/lib</outputDirectory>
<includes>
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.common:*</include>
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.share:jar:installable:*</include>
</includes>
<scope>test</scope>
</dependencySet>
</dependencySets>
</assembly>

View File

@ -0,0 +1,29 @@
<?xml version='1.0' encoding='UTF-8' ?>
<!--
Copyright 2019 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.
-->
<alfresco-config>
<config evaluator="string-compare" condition="Test">
<keycloak-adapter-config>
<credentials>
<secret>secret</secret>
<provider>test</provider>
</credentials>
<allow-any-hostname>true</allow-any-hostname>
</keycloak-adapter-config>
</config>
</alfresco-config>