Compare commits

..

68 Commits

Author SHA1 Message Date
alfresco-build
d885c47df7 [maven-release-plugin][skip ci] prepare release 20.113 2023-03-22 13:30:31 +00:00
Piotr Żurek
a1ccc14a93 ACS- 4752 keycloak migration - resource server role (#1818) 2023-03-22 12:58:13 +01:00
alfresco-build
0023612623 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-22 10:54:12 +00:00
alfresco-build
abf9bf8d71 [maven-release-plugin][skip ci] prepare release 20.112 2023-03-22 10:54:08 +00:00
Maciej Pichura
162e164a0c ACS-4755: Bump snakeyaml to 2.0 (#1791)
* ACS-4755: Bump snakeyaml to 2.0

* ACS-4755: Bump jackson to 2.15.0-rc1
2023-03-22 11:02:31 +01:00
alfresco-build
55b0044965 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-22 08:37:22 +00:00
alfresco-build
cfe212d52f [maven-release-plugin][skip ci] prepare release 20.111 2023-03-22 08:37:19 +00:00
MohinishSah
a3cafb7c4c updated surf-webscripts version to 8.38 2023-03-22 13:07:41 +05:30
alfresco-build
e42e2b2c8e [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-21 15:42:06 +00:00
alfresco-build
9090dea75b [maven-release-plugin][skip ci] prepare release 20.110 2023-03-21 15:42:03 +00:00
Krystian Dabrowski
a26ac1f778 ACS-4857: Lookup for root tag in DB and NOT using search engine (#1810)
* ACS-4857: Lookup for root tag in DB and NOT using search engine
Also:
- removing commons-collections exclusion within test scope due to:
"Caused by: java.lang.ClassNotFoundException: org.apache.commons.collections.CollectionUtils"
2023-03-21 15:37:19 +01:00
alfresco-build
8e1d4782b4 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-21 09:49:51 +00:00
alfresco-build
4c409c925b [maven-release-plugin][skip ci] prepare release 20.109 2023-03-21 09:49:48 +00:00
Suneet Gupta
7cbfab81ce [ACS-3916][ACS-3917] Added TagId Validation for Get and Delete methods (#1815)
* [ACS-3916][ACS-3917] Added TagId Validation for Get and Delete methods

* Addressed review comments -
1. Moved duplicate code from ValidateTag methods to a private method
2. Updated the test case
2023-03-21 14:03:27 +05:30
suneet-gupta
addc2c202b [ACS-4886] Changed valdateTag method to validateNode in getTags() method in TagsImpl class 2023-03-21 07:12:21 +00:00
alfresco-build
7b523e5ad9 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-20 16:36:45 +00:00
alfresco-build
7567e83955 [maven-release-plugin][skip ci] prepare release 20.108 2023-03-20 16:36:41 +00:00
Damian Ujma
d6082f84ac ACS-4842 Upgrade Camel and Netty stack (#1814)
* ACS-4842 Update camel to 3.20.2

* ACS-4842 Update gytheio to 0.18
2023-03-20 16:07:19 +01:00
alfresco-build
4f58031178 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-19 00:08:08 +00:00
alfresco-build
7aff9366ce [maven-release-plugin][skip ci] prepare release 20.107 2023-03-19 00:08:05 +00:00
Alfresco CI User
58235fd891 [force] Force release for 2023-03-19. 2023-03-19 00:03:20 +00:00
alfresco-build
bb204c4cea [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-16 18:42:27 +00:00
alfresco-build
ae29ae8581 [maven-release-plugin][skip ci] prepare release 20.106 2023-03-16 18:42:24 +00:00
Jared Ottley
4b351fbfdc [MNT-23553] REST API call fails when using Oracle database
- Remove semicolon from sql
2023-03-16 11:56:00 -06:00
alfresco-build
3e9964d53f [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-16 08:22:17 +00:00
alfresco-build
23761f6c56 [maven-release-plugin][skip ci] prepare release 20.105 2023-03-16 08:22:13 +00:00
Piotr Żurek
2f8c283ada ACS-4844 Use matching json-smart version (#1805) 2023-03-16 07:58:25 +01:00
alfresco-build
5232b8c3fe [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-15 16:35:10 +00:00
alfresco-build
b90938bb99 [maven-release-plugin][skip ci] prepare release 20.104 2023-03-15 16:35:07 +00:00
Piotr Żurek
d243ac04c6 ACS-4844 json-path should be provided by the repo to avoid conflicts with AMPs (#1803) 2023-03-15 16:49:17 +01:00
alfresco-build
e651f6e104 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-15 14:55:19 +00:00
alfresco-build
e4776e2594 [maven-release-plugin][skip ci] prepare release 20.103 2023-03-15 14:55:15 +00:00
Tom Page
acc5425d68 ACS-4759 Replace default password hashing algorithm. (#1789)
ACS-4759 Replace default password hashing algorithm with bcrypt10.

Update tests to use sha256 hashing and fix tests that relied on md4 being the default hashing algorithm.

Remove test for default password hashing algorithm since this is being overridden for the tests anyway.
2023-03-15 14:11:25 +00:00
alfresco-build
a96e805d52 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-15 09:37:38 +00:00
alfresco-build
45a9a1ae49 [maven-release-plugin][skip ci] prepare release 20.102 2023-03-15 09:37:34 +00:00
Piotr Żurek
bf18c6b419 ACS-4751 Keycloak Migration - OAuth2 Client Role (#1798) 2023-03-15 09:53:17 +01:00
dependabot[bot]
e728489b69 Bump groovy-json from 3.0.12 to 3.0.16 (#1800)
Bumps [groovy-json](https://github.com/apache/groovy) from 3.0.12 to 3.0.16.
- [Release notes](https://github.com/apache/groovy/releases)
- [Commits](https://github.com/apache/groovy/commits)

---
updated-dependencies:
- dependency-name: org.codehaus.groovy:groovy-json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 09:52:13 +01:00
Krystian Dabrowski
f2fdf958f2 ACS-4023: Update GET /tags to support getting tags by name (#1766)
* ACS-4023: Update GET /tags to support getting tags by name
2023-03-15 09:46:58 +01:00
alfresco-build
0cb03c2a38 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-14 09:23:52 +00:00
alfresco-build
b13ca1f68b [maven-release-plugin][skip ci] prepare release 20.101 2023-03-14 09:23:49 +00:00
dependabot[bot]
484699b266 Bump groovy from 3.0.12 to 3.0.16 (#1801)
Bumps [groovy](https://github.com/apache/groovy) from 3.0.12 to 3.0.16.
- [Release notes](https://github.com/apache/groovy/releases)
- [Commits](https://github.com/apache/groovy/commits)

---
updated-dependencies:
- dependency-name: org.codehaus.groovy:groovy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-14 09:55:22 +01:00
alfresco-build
a15161c872 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-13 21:34:16 +00:00
alfresco-build
b3a6150655 [maven-release-plugin][skip ci] prepare release 20.100 2023-03-13 21:34:13 +00:00
Damian Ujma
42e06da4f8 ACS-4137 Change the 'build-multiarch-docker-image' profile phase to package (#1799)
* ACS-4137 Change the build-multiarch-docker-image profile phase to package

* ACS-4137 Change the build-multiarch-docker-image profile phase to package
2023-03-13 22:05:22 +01:00
alfresco-build
4ac30c2173 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-13 15:30:38 +00:00
alfresco-build
d1d84d849e [maven-release-plugin][skip ci] prepare release 20.99 2023-03-13 15:30:35 +00:00
dependabot[bot]
40af1799fe Bump alfresco/alfresco-base-tomcat from tomcat9-jre17-rockylinux8-202209261711 to tomcat9-jre17-rockylinux8-202303081618 in /packaging/docker-alfresco (#1792)
* Bump alfresco/alfresco-base-tomcat in /packaging/docker-alfresco

Bumps alfresco/alfresco-base-tomcat from tomcat9-jre17-rockylinux8-202209261711 to tomcat9-jre17-rockylinux8-202303081618.

---
updated-dependencies:
- dependency-name: alfresco/alfresco-base-tomcat
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* ACS-4768 Update freetype package for Rocky Linux 8.7

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Damian.Ujma@hyland.com <Damian.Ujma@hyland.com>
2023-03-13 16:03:51 +01:00
alfresco-build
f59e4a044a [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-13 10:24:01 +00:00
alfresco-build
5924263c18 [maven-release-plugin][skip ci] prepare release 20.98 2023-03-13 10:23:58 +00:00
Damian Ujma
082e38692f ACS-4137 Produce Multi-Arch Docker images (#1797)
* Revert "Revert "ACS-4137 Produce Multi-Arch Docker images (#1656)" (#1796)"

This reverts commit ddbaabb713.

* ACS-4137 Remove default multi-arch docker-maven-plugin configuration
2023-03-13 10:44:06 +01:00
alfresco-build
f5c4112e65 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-12 00:06:40 +00:00
alfresco-build
ad8251c054 [maven-release-plugin][skip ci] prepare release 20.97 2023-03-12 00:06:37 +00:00
Alfresco CI User
32d0182096 [force] Force release for 2023-03-12. 2023-03-12 00:03:26 +00:00
alfresco-build
2ac9779a3b [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-10 10:45:43 +00:00
alfresco-build
af999fb0fd [maven-release-plugin][skip ci] prepare release 20.96 2023-03-10 10:45:40 +00:00
Damian Ujma
ddbaabb713 Revert "ACS-4137 Produce Multi-Arch Docker images (#1656)" (#1796)
This reverts commit 212fa9b362.
2023-03-10 11:20:43 +01:00
alfresco-build
b93136f2c0 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-10 09:06:46 +00:00
alfresco-build
923aadb12a [maven-release-plugin][skip ci] prepare release 20.95 2023-03-10 09:06:42 +00:00
Wojtek Świętoń
212fa9b362 ACS-4137 Produce Multi-Arch Docker images (#1656)
* ACS-4137 Update pom.xml to use buildx

* ACS-4137 Update travis.yml to use buildx

* ACS-4137 Update travis.yml to use buildx, newest docker version

* ACS-4137 reverted unnecessary changes in travis.yml file

* ACS-4137 Change deprecated <dockerFileDir> property to <contextDir>. Moved <build> section from subpoms to global pom.

* ACS-4137 POM indentation fixes

* ACS-4137 Fix building a multiarch image (#1697)

* ACS-4137 Test wih GHA [ags][tas]

* ACS-4137 Test wih GHA [ags][tas]

* ACS-4137 Fix setting network [ags][tas]

* ACS-4137 Fix creating builder [ags][tas]

* ACS-4137 Change image tag [ags][tas]

* ACS-4137 Fix starting the registry [ags][tas]

* ACS-4137 Refactor code [ags][tas]

* ACS-4137 Uncomment all jobs [tas][ags]

* ACS-4137 Improve prepare_buildx.sh [tas][ags]

* ACS-4137 Implement timeout + remove hardcoded base image tag [tas][ags]

* ACS-4137 Added exec-maven-plugin to build-push-image for alfresco-governance-repository-community-base image

* ACS-4137 Generalize prepare_buildx.sh + increase registry timeout

* ACS-4137 merged local.registry.host and local.registry.port. Builder name changed to entitled-builder. In prepare_builder script added localhost registry checking

* ACS-4137 added build-multiarch-docker-images maven profile

* ACS-4137 added <BASE_IMAGE> arg to build-docker-images profile

* ACS-4137 added combine.self="override" attribute to configuration in build-docker-images profile to not use buildx. Delete redundant base.image.tag

* ACS-4137 Push docker images to local repository

* ACS-4137 Remove useless scripts

* ACS-4137 Move builder.name and local.registry properties to main pom.xml

* ACS-4137 Remove useless properties definitions

---------

Co-authored-by: Damian.Ujma@hyland.com <Damian.Ujma@hyland.com>
Co-authored-by: Damian Ujma <92095156+damianujma@users.noreply.github.com>
2023-03-10 09:20:22 +01:00
alfresco-build
c3e37b96b4 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-08 10:53:06 +00:00
alfresco-build
517f40e150 [maven-release-plugin][skip ci] prepare release 20.94 2023-03-08 10:53:02 +00:00
dependabot[bot]
f86d0f1fd2 Bump alfresco-jlan-embed from 7.2 to 7.4 (#1773)
Bumps alfresco-jlan-embed from 7.2 to 7.4.

---
updated-dependencies:
- dependency-name: org.alfresco:alfresco-jlan-embed
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 10:38:40 +01:00
alfresco-build
09da8640a0 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-08 09:14:04 +00:00
alfresco-build
6bd9bf768e [maven-release-plugin][skip ci] prepare release 20.93 2023-03-08 09:14:00 +00:00
pzurek
f75c0c8f9e Just to check if the recent problems are related to Dependabot commits 2023-03-08 09:14:54 +01:00
dependabot[bot]
ff9364a3b1 Bump utility from 3.0.58 to 3.0.61 (#1776) 2023-03-07 19:22:57 +00:00
dependabot[bot]
db832663b4 Bump dependency.activemq.version from 5.17.1 to 5.17.4 (#1778) 2023-03-07 11:39:30 +00:00
alfresco-build
890e1173c5 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-07 11:36:43 +00:00
65 changed files with 4070 additions and 2079 deletions

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-automation-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<build>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<modules>

View File

@@ -1,3 +1,4 @@
ARG BASE_IMAGE
# BUILD STAGE AGS
FROM debian:11-slim AS AGSBUILDER
@@ -12,7 +13,7 @@ RUN unzip -q /build/gs-api-explorer-*.war -d /build/gs-api-explorer && \
chmod -R g-w,o= /build
# ACTUAL IMAGE
FROM alfresco/alfresco-community-repo-base:${image.tag}
FROM ${BASE_IMAGE}
# Alfresco user does not have permissions to modify webapps or configuration. Switch to root.
# The access will be fixed after all operations are done.

View File

@@ -8,13 +8,15 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<properties>
<app.amp.client.war.folder>${project.build.directory}/${project.build.finalName}-war</app.amp.client.war.folder>
<image.name>alfresco/alfresco-governance-repository-community-base</image.name>
<base.image>alfresco/alfresco-community-repo-base</base.image>
<scripts.directory>${project.parent.parent.parent.parent.basedir}/scripts</scripts.directory>
</properties>
<dependencies>
@@ -537,9 +539,43 @@
</build>
</profile>
<profile>
<id>build-docker-images</id>
<!-- builds "image:latest" locally -->
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<image>
<name>${image.name}:${image.tag}</name>
<build>
<args>
<BASE_IMAGE>${base.image}:${image.tag}</BASE_IMAGE>
</args>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>build-docker-images</id>
<!-- builds "image:latest" locally -->
<id>build-multiarch-docker-images</id>
<build>
<plugins>
<plugin>
@@ -548,20 +584,56 @@
<configuration>
<images>
<image>
<name>${image.name}:${image.tag}</name>
<name>${local.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<contextDir>${project.basedir}</contextDir>
<args>
<BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
</args>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-image</id>
<id>build-push-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>prepare-buildx</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${scripts.directory}/prepare_buildx.sh</executable>
<arguments>
<argument>${builder.name}</argument>
<argument>${image.registry}</argument>
<argument>${image.name}</argument>
<argument>${image.tag}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
@@ -578,12 +650,37 @@
<images>
<image>
<!-- Quay image -->
<name>${image.name}:${image.tag}</name>
<registry>${image.registry}</registry>
<name>${image.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<args>
<BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
</args>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
<image>
<!-- DockerHub image -->
<name>${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<args>
<BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
</args>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
@@ -598,6 +695,28 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>prepare-buildx</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${scripts.directory}/prepare_buildx.sh</executable>
<arguments>
<argument>${builder.name}</argument>
<argument>${image.registry}</argument>
<argument>${image.name}</argument>
<argument>${image.tag}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<build>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<modules>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<properties>
@@ -121,12 +121,6 @@
<version>${dependency.webscripts.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<dependencies>

View File

@@ -45,6 +45,13 @@ public class ListBackedPagingResults<R> implements PagingResults<R>
size = list.size();
hasMore = false;
}
public ListBackedPagingResults(List<R> list, boolean hasMore)
{
this(list);
this.hasMore = hasMore;
}
public ListBackedPagingResults(List<R> list, PagingRequest paging)
{
// Excerpt

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<dependencies>

View File

@@ -9,6 +9,6 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
</project>

View File

@@ -1,6 +1,6 @@
# Fetch image based on Tomcat 9.0, Java 17 and Rocky Linux 8
# More infos about this image: https://github.com/Alfresco/alfresco-docker-base-tomcat
FROM alfresco/alfresco-base-tomcat:tomcat9-jre17-rockylinux8-202209261711
FROM alfresco/alfresco-base-tomcat:tomcat9-jre17-rockylinux8-202303081618
# Set default docker_context.
ARG resource_path=target
@@ -65,7 +65,7 @@ RUN sed -i -e "s_appender.rolling.fileName\=alfresco.log_appender.rolling.fileNa
RUN yum install -y fontconfig-2.13.1-4.el8 \
dejavu-fonts-common-2.35-7.el8 \
fontpackages-filesystem-1.44-22.el8 \
freetype-2.9.1-4.el8_3.1 \
freetype-2.9.1-9.el8 \
libpng-1.6.34-5.el8 \
dejavu-sans-fonts-2.35-7.el8 && \
yum clean all

View File

@@ -7,11 +7,12 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<properties>
<image.name>alfresco/alfresco-community-repo-base</image.name>
<scripts.directory>${project.parent.parent.basedir}/scripts</scripts.directory>
</properties>
<build>
@@ -156,6 +157,67 @@
</build>
</profile>
<profile>
<id>build-multiarch-docker-images</id>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<image>
<name>${local.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-push-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>prepare-buildx</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${scripts.directory}/prepare_buildx.sh</executable>
<arguments>
<argument>${builder.name}</argument>
<argument>${image.registry}</argument>
<argument>${image.name}</argument>
<argument>${image.tag}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>push-docker-images</id>
<!-- publishes "image:latest" on Quay & DockerHub -->
@@ -168,12 +230,29 @@
<images>
<!-- Quay image -->
<image>
<name>${image.name}:${image.tag}</name>
<registry>${image.registry}</registry>
<name>${image.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
</buildx>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
<!-- DockerHub image -->
<image>
<name>${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
</buildx>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<modules>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<organization>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<developers>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<developers>
@@ -95,7 +95,6 @@
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${dependency.jakarta-json-path.version}</version>
</dependency>
</dependencies>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<properties>
@@ -165,14 +165,14 @@
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.12</version>
<version>3.0.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-json-->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-json</artifactId>
<version>3.0.12</version>
<version>3.0.16</version>
</dependency>
<dependency>

View File

@@ -30,7 +30,10 @@ import static org.alfresco.utility.report.log.Step.STEP;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.alfresco.rest.core.IRestModelsCollection;
import org.alfresco.utility.exception.TestConfigurationException;
@@ -117,7 +120,7 @@ public class ModelsCollectionAssertion<C>
return (C) modelCollection;
}
@SuppressWarnings("unchecked")
@SuppressWarnings("unchecked")
public C entriesListDoesNotContain(String key, String value)
{
boolean exist = false;
@@ -143,6 +146,53 @@ public class ModelsCollectionAssertion<C>
return (C) modelCollection;
}
public C entrySetContains(String key, String... expectedValues)
{
return entrySetContains(key, Arrays.stream(expectedValues).collect(Collectors.toSet()));
}
@SuppressWarnings("unchecked")
public C entrySetContains(String key, Collection<String> expectedValues)
{
Collection<String> actualValues = ((List<Model>) modelCollection.getEntries()).stream()
.map(model -> extractValueAsString(model, key))
.collect(Collectors.toSet());
Assert.assertTrue(actualValues.containsAll(expectedValues), String.format("Entry with key: \"%s\" is expected to contain values: %s, but actual values are: %s",
key, expectedValues, actualValues));
return (C) modelCollection;
}
@SuppressWarnings("unchecked")
public C entrySetMatches(String key, Collection<String> expectedValues)
{
Collection<String> actualValues = ((List<Model>) modelCollection.getEntries()).stream()
.map(model -> extractValueAsString(model, key))
.collect(Collectors.toSet());
Assert.assertEqualsNoOrder(actualValues, expectedValues, String.format("Entry with key: \"%s\" is expected to match values: %s, but actual values are: %s",
key, expectedValues, actualValues));
return (C) modelCollection;
}
private String extractValueAsString(Model model, String key)
{
String fieldValue;
Object modelObject = loadModel(model);
try {
ObjectMapper mapper = new ObjectMapper();
String jsonInString = mapper.writeValueAsString(modelObject);
fieldValue = JsonPath.with(jsonInString).get(key);
} catch (Exception e) {
throw new TestConfigurationException(String.format(
"You try to assert field [%s] that doesn't exist in class: [%s]. Exception: %s, Please check your code!",
key, getClass().getCanonicalName(), e.getMessage()));
}
return fieldValue;
}
@SuppressWarnings("unchecked")
public C entriesListDoesNotContain(String key)
{

View File

@@ -1,5 +1,9 @@
package org.alfresco.rest.tags;
import static org.alfresco.utility.report.log.Step.STEP;
import java.util.Set;
import org.alfresco.rest.model.RestErrorModel;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.rest.model.RestTagModelsCollection;
@@ -9,19 +13,11 @@ import org.alfresco.utility.model.TestGroup;
import org.alfresco.utility.testrail.ExecutionType;
import org.alfresco.utility.testrail.annotation.TestRail;
import org.springframework.http.HttpStatus;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@Test(groups = {TestGroup.REQUIRE_SOLR})
public class GetTagsTests extends TagsDataPrep
{
@BeforeClass(alwaysRun = true)
public void dataPreparation() throws Exception
{
init();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.SANITY, description = "Verify user with Manager role gets tags using REST API and status code is OK (200)")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.SANITY })
public void getTagsWithManagerRole() throws Exception
@@ -192,7 +188,7 @@ public class GetTagsTests extends TagsDataPrep
.and().field("hasMoreItems").is("false")
.and().field("count").is("0")
.and().field("skipCount").is(20000)
.and().field("totalItems").isNull();
.and().field("totalItems").is(0);
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -219,11 +215,128 @@ public class GetTagsTests extends TagsDataPrep
RestTagModel deletedTag = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager))
.withCoreAPI().usingResource(document).addTag(removedTag);
restClient.withCoreAPI().usingResource(document).deleteTag(deletedTag);
restClient.authenticateUser(adminUserModel).withCoreAPI().usingTag(deletedTag).deleteTag();
restClient.assertStatusCodeIs(HttpStatus.NO_CONTENT);
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListDoesNotContain("tag", removedTag.toLowerCase());
}
/**
* Verify if exact name filter can be applied.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withSingleNameFilter()
{
STEP("Get tags with names filter using EQUALS and expect one item in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag='" + documentTag.getTag() + "')")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetMatches("tag", Set.of(documentTagValue.toLowerCase()));
}
/**
* Verify if multiple names can be applied as a filter.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withTwoNameFilters()
{
STEP("Get tags with names filter using IN and expect two items in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag IN ('" + documentTag.getTag() + "', '" + folderTag.getTag() + "'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetMatches("tag", Set.of(documentTagValue.toLowerCase(), folderTagValue.toLowerCase()));
}
/**
* Verify if alike name filter can be applied.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_whichNamesStartsWithOrphan()
{
STEP("Get tags with names filter using MATCHES and expect one item in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag MATCHES ('orphan*'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetContains("tag", orphanTag.getTag().toLowerCase());
}
/**
* Verify that tags can be filtered by exact name and alike name at the same time.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withExactNameAndAlikeFilters()
{
STEP("Get tags with names filter using EQUALS and MATCHES and expect four items in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag='" + orphanTag.getTag() + "' OR tag MATCHES ('*tag*'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetContains("tag", documentTagValue.toLowerCase(), documentTagValue2.toLowerCase(), folderTagValue.toLowerCase(), orphanTag.getTag().toLowerCase());
}
/**
* Verify if multiple alike filters can be applied.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withTwoAlikeFilters()
{
STEP("Get tags applying names filter using MATCHES twice and expect four items in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag MATCHES ('orphan*') OR tag MATCHES ('tag*'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetContains("tag", documentTagValue.toLowerCase(), documentTagValue2.toLowerCase(), folderTagValue.toLowerCase(), orphanTag.getTag().toLowerCase());
}
/**
* Verify that providing incorrect field name in where query will result with 400 (Bad Request).
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withWrongWherePropertyNameAndExpect400()
{
STEP("Try to get tags with names filter using EQUALS and wrong property name and expect 400");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(name=gat)")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST)
.assertLastError().containsSummary("Where query error: property with name: name is not expected");
}
/**
* Verify tht AND operator is not supported in where query and expect 400 (Bad Request).
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_queryAndOperatorNotSupported()
{
STEP("Try to get tags applying names filter using AND operator and expect 400");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(name=tag AND name IN ('tag-', 'gat'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST)
.assertLastError().containsSummary("An invalid WHERE query was received. Unsupported Predicate");
}
}

View File

@@ -24,7 +24,7 @@ public class TagsDataPrep extends RestTest
protected static FileModel document;
protected static FolderModel folder;
protected static String documentTagValue, documentTagValue2, folderTagValue;
protected static RestTagModel documentTag, documentTag2, folderTag, returnedModel;
protected static RestTagModel documentTag, documentTag2, folderTag, orphanTag, returnedModel;
protected static RestTagModelsCollection returnedCollection;
@BeforeClass
@@ -47,16 +47,17 @@ public class TagsDataPrep extends RestTest
documentTag = restClient.withCoreAPI().usingResource(document).addTag(documentTagValue);
documentTag2 = restClient.withCoreAPI().usingResource(document).addTag(documentTagValue2);
folderTag = restClient.withCoreAPI().usingResource(folder).addTag(folderTagValue);
orphanTag = restClient.withCoreAPI().createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("orphan-tag")).create());
// Allow indexing to complete.
Utility.sleep(500, 60000, () ->
{
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
returnedCollection.assertThat().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase())
.and().entriesListContains("tag", folderTagValue.toLowerCase());
});
{
returnedCollection = restClient.withParams("maxItems=10000", "where=(tag MATCHES ('*tag*'))")
.withCoreAPI().getTags();
returnedCollection.assertThat().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase())
.and().entriesListContains("tag", folderTagValue.toLowerCase());
});
}
protected RestTagModel createTagForDocument(FileModel document)

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<developers>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<properties>

51
pom.xml
View File

@@ -2,7 +2,7 @@
<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>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
<packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name>
@@ -35,6 +35,8 @@
<build-number>local</build-number>
<image.tag>latest</image.tag>
<image.registry>quay.io</image.registry>
<builder.name>entitled-builder</builder.name>
<local.registry>127.0.0.1:5000</local.registry>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -45,7 +47,7 @@
<dependency.alfresco-hb-data-sender.version>1.0.12</dependency.alfresco-hb-data-sender.version>
<dependency.alfresco-trashcan-cleaner.version>2.4.1</dependency.alfresco-trashcan-cleaner.version>
<dependency.alfresco-jlan.version>7.2</dependency.alfresco-jlan.version>
<dependency.alfresco-jlan.version>7.4</dependency.alfresco-jlan.version>
<dependency.alfresco-server-root.version>6.0.1</dependency.alfresco-server-root.version>
<dependency.alfresco-messaging-repo.version>1.2.20</dependency.alfresco-messaging-repo.version>
<dependency.activiti-engine.version>5.23.0</dependency.activiti-engine.version>
@@ -57,10 +59,10 @@
<dependency.spring.version>5.3.25</dependency.spring.version>
<dependency.antlr.version>3.5.3</dependency.antlr.version>
<dependency.jackson.version>2.14.0</dependency.jackson.version>
<dependency.jackson.version>2.15.0-rc1</dependency.jackson.version>
<dependency.cxf.version>3.5.5</dependency.cxf.version>
<dependency.opencmis.version>1.0.0</dependency.opencmis.version>
<dependency.webscripts.version>8.33</dependency.webscripts.version>
<dependency.webscripts.version>8.38</dependency.webscripts.version>
<dependency.bouncycastle.version>1.70</dependency.bouncycastle.version>
<dependency.mockito-core.version>4.9.0</dependency.mockito-core.version>
<dependency.assertj.version>3.24.2</dependency.assertj.version>
@@ -74,20 +76,20 @@
<dependency.xercesImpl.version>2.12.2</dependency.xercesImpl.version>
<dependency.slf4j.version>2.0.3</dependency.slf4j.version>
<dependency.log4j.version>2.19.0</dependency.log4j.version>
<dependency.gytheio.version>0.17</dependency.gytheio.version>
<dependency.groovy.version>3.0.12</dependency.groovy.version>
<dependency.gytheio.version>0.18</dependency.gytheio.version>
<dependency.groovy.version>3.0.16</dependency.groovy.version>
<dependency.tika.version>2.4.1</dependency.tika.version>
<dependency.spring-security.version>5.7.5</dependency.spring-security.version>
<dependency.spring-security.version>5.8.2</dependency.spring-security.version>
<dependency.truezip.version>7.7.10</dependency.truezip.version>
<dependency.poi.version>5.2.2</dependency.poi.version>
<dependency.poi-ooxml-lite.version>5.2.3</dependency.poi-ooxml-lite.version>
<dependency.keycloak.version>18.0.0</dependency.keycloak.version>
<dependency.jboss.logging.version>3.5.0.Final</dependency.jboss.logging.version>
<dependency.camel.version>3.18.2</dependency.camel.version> <!-- when bumping this version, please keep track/sync with included netty.io dependencies -->
<dependency.netty.version>4.1.79.Final</dependency.netty.version> <!-- must be in sync with camels transitive dependencies, e.g.: netty-common -->
<dependency.netty.qpid.version>4.1.72.Final</dependency.netty.qpid.version> <!-- must be in sync with camels transitive dependencies: native-unix-common/native-epoll/native-kqueue -->
<dependency.netty-tcnative.version>2.0.53.Final</dependency.netty-tcnative.version> <!-- must be in sync with camels transitive dependencies -->
<dependency.activemq.version>5.17.1</dependency.activemq.version>
<dependency.camel.version>3.20.2</dependency.camel.version> <!-- when bumping this version, please keep track/sync with included netty.io dependencies -->
<dependency.netty.version>4.1.87.Final</dependency.netty.version> <!-- must be in sync with camels transitive dependencies, e.g.: netty-common -->
<dependency.netty.qpid.version>4.1.82.Final</dependency.netty.qpid.version> <!-- must be in sync with camels transitive dependencies: native-unix-common/native-epoll/native-kqueue -->
<dependency.netty-tcnative.version>2.0.56.Final</dependency.netty-tcnative.version> <!-- must be in sync with camels transitive dependencies -->
<dependency.activemq.version>5.17.4</dependency.activemq.version>
<dependency.apache-compress.version>1.22</dependency.apache-compress.version>
<dependency.apache.taglibs.version>1.2.5</dependency.apache.taglibs.version>
<dependency.awaitility.version>4.2.0</dependency.awaitility.version>
@@ -107,6 +109,7 @@
<dependency.jakarta-mail-api.version>1.6.5</dependency.jakarta-mail-api.version>
<dependency.jakarta-json-api.version>1.1.6</dependency.jakarta-json-api.version>
<dependency.jakarta-json-path.version>2.7.0</dependency.jakarta-json-path.version>
<dependency.json-smart.version>2.4.8</dependency.json-smart.version>
<dependency.jakarta-rpc-api.version>1.1.4</dependency.jakarta-rpc-api.version>
<alfresco.googledrive.version>3.4.0-M1</alfresco.googledrive.version>
@@ -120,7 +123,7 @@
<dependency.mysql.version>8.0.30</dependency.mysql.version>
<dependency.mysql-image.version>8</dependency.mysql-image.version>
<dependency.mariadb.version>2.7.4</dependency.mariadb.version>
<dependency.tas-utility.version>3.0.58</dependency.tas-utility.version>
<dependency.tas-utility.version>3.0.61</dependency.tas-utility.version>
<dependency.rest-assured.version>5.2.0</dependency.rest-assured.version>
<dependency.tas-email.version>1.11</dependency.tas-email.version>
<dependency.tas-webdav.version>1.7</dependency.tas-webdav.version>
@@ -148,7 +151,7 @@
<connection>scm:git:https://github.com/Alfresco/alfresco-community-repo.git</connection>
<developerConnection>scm:git:https://github.com/Alfresco/alfresco-community-repo.git</developerConnection>
<url>https://github.com/Alfresco/alfresco-community-repo</url>
<tag>20.92</tag>
<tag>20.113</tag>
</scm>
<distributionManagement>
@@ -239,6 +242,17 @@
<version>${dependency.jakarta-json-api.version}</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${dependency.jakarta-json-path.version}</version>
</dependency>
<dependency>
<groupId> net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>${dependency.json-smart.version}</version>
</dependency>
<dependency>
<groupId>jakarta.xml.rpc</groupId>
<artifactId>jakarta.xml.rpc-api</artifactId>
@@ -516,7 +530,7 @@
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.32</version>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -899,6 +913,13 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>${dependency.spring-security.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<dependencies>

View File

@@ -25,10 +25,16 @@
*/
package org.alfresco.rest.api.impl;
import static org.alfresco.rest.antlr.WhereClauseParser.EQUALS;
import static org.alfresco.rest.antlr.WhereClauseParser.IN;
import static org.alfresco.rest.antlr.WhereClauseParser.MATCHES;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -51,8 +57,12 @@ import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationE
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.resource.parameters.where.Query;
import org.alfresco.rest.framework.resource.parameters.where.QueryHelper;
import org.alfresco.rest.framework.resource.parameters.where.QueryImpl;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
@@ -68,11 +78,14 @@ import org.apache.commons.collections.CollectionUtils;
*/
public class TagsImpl implements Tags
{
private static final Object PARAM_INCLUDE_COUNT = "count";
private static final String PARAM_INCLUDE_COUNT = "count";
private static final String PARAM_WHERE_TAG = "tag";
static final String NOT_A_VALID_TAG = "An invalid parameter has been supplied";
static final String NO_PERMISSION_TO_MANAGE_A_TAG = "Current user does not have permission to manage a tag";
private final NodeRef tagParentNodeRef = new NodeRef("workspace://SpacesStore/tag:tag-root");
private Nodes nodes;
private NodeService nodeService;
private TaggingService taggingService;
private TypeConstraint typeConstraint;
private AuthorityService authorityService;
@@ -86,6 +99,10 @@ public class TagsImpl implements Tags
{
this.nodes = nodes;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
public void setTaggingService(TaggingService taggingService)
{
@@ -154,17 +171,18 @@ public class TagsImpl implements Tags
taggingService.deleteTag(storeRef, tagValue);
}
@Override
public CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params)
{
Paging paging = params.getPaging();
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging));
taggingService.getPagedTags(storeRef, 0, paging.getMaxItems());
Paging paging = params.getPaging();
Map<Integer, Collection<String>> namesFilters = resolveTagNamesQuery(params.getQuery());
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging), namesFilters.get(EQUALS), namesFilters.get(MATCHES));
Integer totalItems = results.getTotalResultCount().getFirst();
List<Pair<NodeRef, String>> page = results.getPage();
List<Tag> tags = new ArrayList<Tag>(page.size());
List<Pair<String, Integer>> tagsByCount = null;
Map<String, Integer> tagsByCountMap = new HashMap<String, Integer>();
List<Tag> tags = new ArrayList<>(page.size());
List<Pair<String, Integer>> tagsByCount;
Map<String, Integer> tagsByCountMap = new HashMap<>();
if (params.getInclude().contains(PARAM_INCLUDE_COUNT))
{
tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef);
@@ -183,27 +201,19 @@ public class TagsImpl implements Tags
tags.add(selectedTag);
}
return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), totalItems);
}
public NodeRef validateTag(String tagId)
{
NodeRef tagNodeRef = nodes.validateNode(tagId);
if(tagNodeRef == null)
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
return checkTagRootAsNodePrimaryParent(tagId, tagNodeRef);
}
public NodeRef validateTag(StoreRef storeRef, String tagId)
{
NodeRef tagNodeRef = nodes.validateNode(storeRef, tagId);
if(tagNodeRef == null)
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
return checkTagRootAsNodePrimaryParent(tagId, tagNodeRef);
}
public Tag changeTag(StoreRef storeRef, String tagId, Tag tag)
@@ -244,8 +254,7 @@ public class TagsImpl implements Tags
public CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params)
{
NodeRef nodeRef = validateTag(nodeId);
NodeRef nodeRef = nodes.validateNode(nodeId);
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(nodeRef, Util.getPagingRequest(params.getPaging()));
Integer totalItems = results.getTotalResultCount().getFirst();
List<Pair<NodeRef, String>> page = results.getPage();
@@ -291,4 +300,47 @@ public class TagsImpl implements Tags
throw new PermissionDeniedException(NO_PERMISSION_TO_MANAGE_A_TAG);
}
}
/**
* Method resolves where query looking for clauses: EQUALS, IN or MATCHES.
* Expected values for EQUALS and IN will be merged under EQUALS clause.
* @param namesQuery Where query with expected tag name(s).
* @return Map of expected exact and alike tag names.
*/
private Map<Integer, Collection<String>> resolveTagNamesQuery(final Query namesQuery)
{
if (namesQuery == null || namesQuery == QueryImpl.EMPTY)
{
return Collections.emptyMap();
}
final Map<Integer, Collection<String>> properties = QueryHelper
.resolve(namesQuery)
.usingOrOperator()
.withoutNegations()
.getProperty(PARAM_WHERE_TAG)
.getExpectedValuesForAnyOf(EQUALS, IN, MATCHES)
.skipNegated();
return properties.entrySet().stream()
.collect(Collectors.groupingBy((entry) -> {
if (entry.getKey() == EQUALS || entry.getKey() == IN)
{
return EQUALS;
}
else
{
return MATCHES;
}
}, Collectors.flatMapping((entry) -> entry.getValue().stream().map(String::toLowerCase), Collectors.toCollection(HashSet::new))));
}
private NodeRef checkTagRootAsNodePrimaryParent(String tagId, NodeRef tagNodeRef)
{
if ( tagNodeRef == null || !nodeService.getPrimaryParent(tagNodeRef).getParentRef().equals(tagParentNodeRef))
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
}
}

View File

@@ -101,36 +101,20 @@ public class Tag implements Comparable<Tag>
}
@Override
public int hashCode()
public boolean equals(Object o)
{
final int prime = 31;
int result = 1;
result = prime * result + ((nodeRef == null) ? 0 : nodeRef.hashCode());
return result;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Tag tag1 = (Tag) o;
return Objects.equals(nodeRef, tag1.nodeRef) && Objects.equals(tag, tag1.tag) && Objects.equals(count, tag1.count);
}
/*
* Tags are equal if they have the same NodeRef
*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
public int hashCode()
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Tag other = (Tag) obj;
if (nodeRef == null) {
if (other.nodeRef != null)
return false;
} else if (!nodeRef.equals(other.nodeRef))
return false;
return true;
return Objects.hash(nodeRef, tag, count);
}
@Override

View File

@@ -59,9 +59,8 @@ public class TagsEntityResource implements EntityResourceAction.Read<Tag>,
}
/**
*
* Returns a paged list of all currently used tags in the store workspace://SpacesStore for the current tenant.
*
* GET /tags
*/
@Override
@WebApiDescription(title="A paged list of all tags in the network.")

View File

@@ -0,0 +1,259 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.framework.resource.parameters.where;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
/**
* Basic implementation of {@link QueryHelper.WalkerCallbackAdapter} providing universal handling of Where query clauses.
* This implementation supports AND operator and all clause types.
* Be default, walker verifies strictly if expected or unexpected properties, and it's comparison types are present in query
* and throws {@link InvalidQueryException} if they are missing.
*/
public class BasicQueryWalker extends QueryHelper.WalkerCallbackAdapter
{
private static final String EQUALS_AND_IN_NOT_ALLOWED_TOGETHER = "Where query error: cannot use '=' (EQUALS) AND 'IN' clauses with same property: %s";
private static final String MISSING_PROPERTY = "Where query error: property with name: %s not present";
static final String MISSING_CLAUSE_TYPE = "Where query error: property with name: %s expects clause: %s";
static final String MISSING_ANY_CLAUSE_OF_TYPE = "Where query error: property with name: %s expects at least one of clauses: %s";
private static final String PROPERTY_NOT_EXPECTED = "Where query error: property with name: %s is not expected";
private static final String PROPERTY_NOT_NEGATABLE = "Where query error: property with name: %s cannot be negated";
private static final String PROPERTY_NAMES_EMPTY = "Cannot verify WHERE query without expected property names";
private Collection<String> expectedPropertyNames;
private final Map<String, WhereProperty> properties;
protected boolean clausesNegatable = true;
protected boolean validateStrictly = true;
public BasicQueryWalker()
{
this.properties = new HashMap<>();
}
public BasicQueryWalker(final String... expectedPropertyNames)
{
this();
this.expectedPropertyNames = Set.of(expectedPropertyNames);
}
public BasicQueryWalker(final Collection<String> expectedPropertyNames)
{
this();
this.expectedPropertyNames = expectedPropertyNames;
}
public void setClausesNegatable(final boolean clausesNegatable)
{
this.clausesNegatable = clausesNegatable;
}
public void setValidateStrictly(boolean validateStrictly)
{
this.validateStrictly = validateStrictly;
}
@Override
public void exists(String propertyName, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
addProperties(propertyName, WhereClauseParser.EXISTS, negated);
}
@Override
public void between(String propertyName, String firstValue, String secondValue, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
addProperties(propertyName, WhereClauseParser.BETWEEN, negated, firstValue, secondValue);
}
@Override
public void comparison(int type, String propertyName, String propertyValue, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
if (WhereClauseParser.EQUALS == type && isAndSupported() && containsProperty(propertyName, WhereClauseParser.IN, negated))
{
throw new InvalidQueryException(String.format(EQUALS_AND_IN_NOT_ALLOWED_TOGETHER, propertyName));
}
addProperties(propertyName, type, negated, propertyValue);
}
@Override
public void in(String propertyName, boolean negated, String... propertyValues)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
if (isAndSupported() && containsProperty(propertyName, WhereClauseParser.EQUALS, negated))
{
throw new InvalidQueryException(String.format(EQUALS_AND_IN_NOT_ALLOWED_TOGETHER, propertyName));
}
addProperties(propertyName, WhereClauseParser.IN, negated, propertyValues);
}
@Override
public void matches(final String propertyName, String propertyValue, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
addProperties(propertyName, WhereClauseParser.MATCHES, negated, propertyValue);
}
@Override
public void and()
{
// Don't need to do anything here - it's enough to enable AND operator.
// OR is not supported at the same time.
}
/**
* Verify if property is expected, if not throws {@link InvalidQueryException}.
*/
protected void verifyPropertyExpectedness(final String propertyName)
{
if (validateStrictly && CollectionUtils.isNotEmpty(expectedPropertyNames) && !this.expectedPropertyNames.contains(propertyName))
{
throw new InvalidQueryException(String.format(PROPERTY_NOT_EXPECTED, propertyName));
}
else if (validateStrictly && CollectionUtils.isEmpty(expectedPropertyNames))
{
throw new IllegalStateException(PROPERTY_NAMES_EMPTY);
}
}
/**
* Verify if clause negations are allowed, if not throws {@link InvalidQueryException}.
*/
protected void verifyClausesNegatability(final boolean negated, final String propertyName)
{
if (!clausesNegatable && negated)
{
throw new InvalidQueryException(String.format(PROPERTY_NOT_NEGATABLE, propertyName));
}
}
protected boolean isAndSupported()
{
try
{
and();
return true;
}
catch (InvalidQueryException ignore)
{
return false;
}
}
protected void addProperties(final String propertyName, final int clauseType, final String... propertyValues)
{
this.addProperties(propertyName, clauseType, false, propertyValues);
}
protected void addProperties(final String propertyName, final int clauseType, final boolean negated, final String... propertyValues)
{
final WhereProperty.ClauseType type = WhereProperty.ClauseType.of(clauseType, negated);
final Set<String> propertiesToAdd = Optional.ofNullable(propertyValues).map(Set::of).orElse(Collections.emptySet());
if (this.containsProperty(propertyName))
{
this.properties.get(propertyName).addValuesToType(type, propertiesToAdd);
}
else
{
this.properties.put(propertyName, new WhereProperty(propertyName, type, propertiesToAdd, validateStrictly));
}
}
protected boolean containsProperty(final String propertyName)
{
return this.properties.containsKey(propertyName);
}
protected boolean containsProperty(final String propertyName, final int clauseType, final boolean negated)
{
return this.properties.containsKey(propertyName) && this.properties.get(propertyName).containsType(clauseType, negated);
}
@Override
public Collection<String> getProperty(String propertyName, int type, boolean negated)
{
return this.getProperty(propertyName).getExpectedValuesFor(type, negated);
}
public WhereProperty getProperty(final String propertyName)
{
if (validateStrictly && !this.containsProperty(propertyName))
{
throw new InvalidQueryException(String.format(MISSING_PROPERTY, propertyName));
}
return this.properties.get(propertyName);
}
public List<WhereProperty> getProperties(final String... propertyNames)
{
return Arrays.stream(propertyNames)
.filter(StringUtils::isNotBlank)
.distinct()
.peek(propertyName -> {
if (validateStrictly && !this.containsProperty(propertyName))
{
throw new InvalidQueryException(String.format(MISSING_PROPERTY, propertyName));
}
})
.map(this.properties::get)
.collect(Collectors.toList());
}
public Map<String, WhereProperty> getPropertiesAsMap(final String... propertyNames)
{
return Arrays.stream(propertyNames)
.filter(StringUtils::isNotBlank)
.distinct()
.peek(propertyName -> {
if (validateStrictly && !this.containsProperty(propertyName))
{
throw new InvalidQueryException(String.format(MISSING_PROPERTY, propertyName));
}
})
.collect(Collectors.toMap(propertyName -> propertyName, this.properties::get));
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,10 +25,19 @@
*/
package org.alfresco.rest.framework.resource.parameters.where;
import static org.alfresco.rest.antlr.WhereClauseParser.BETWEEN;
import static org.alfresco.rest.antlr.WhereClauseParser.EQUALS;
import static org.alfresco.rest.antlr.WhereClauseParser.EXISTS;
import static org.alfresco.rest.antlr.WhereClauseParser.IN;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.antlr.runtime.tree.CommonTree;
@@ -45,14 +54,19 @@ public abstract class QueryHelper
/**
* An interface used when walking a query tree. Calls are made to methods when the particular clause is encountered.
*/
public static interface WalkerCallback
public interface WalkerCallback
{
InvalidQueryException UNSUPPORTED = new InvalidQueryException("Unsupported Predicate");
/**
* Called any time an EXISTS clause is encountered.
* @param propertyName Name of the property
* @param negated returns true if "NOT EXISTS" was used
*/
void exists(String propertyName, boolean negated);
default void exists(String propertyName, boolean negated)
{
throw UNSUPPORTED;
}
/**
* Called any time a BETWEEN clause is encountered.
@@ -61,12 +75,18 @@ public abstract class QueryHelper
* @param secondValue String
* @param negated returns true if "NOT BETWEEN" was used
*/
void between(String propertyName, String firstValue, String secondValue, boolean negated);
default void between(String propertyName, String firstValue, String secondValue, boolean negated)
{
throw UNSUPPORTED;
}
/**
* One of EQUALS LESSTHAN GREATERTHAN LESSTHANOREQUALS GREATERTHANOREQUALS;
*/
void comparison(int type, String propertyName, String propertyValue, boolean negated);
default void comparison(int type, String propertyName, String propertyValue, boolean negated)
{
throw UNSUPPORTED;
}
/**
* Called any time an IN clause is encountered.
@@ -74,7 +94,10 @@ public abstract class QueryHelper
* @param negated returns true if "NOT IN" was used
* @param propertyValues the property values
*/
void in(String property, boolean negated, String... propertyValues);
default void in(String property, boolean negated, String... propertyValues)
{
throw UNSUPPORTED;
}
/**
* Called any time a MATCHES clause is encountered.
@@ -82,42 +105,37 @@ public abstract class QueryHelper
* @param propertyValue String
* @param negated returns true if "NOT MATCHES" was used
*/
void matches(String property, String propertyValue, boolean negated);
default void matches(String property, String propertyValue, boolean negated)
{
throw UNSUPPORTED;
}
/**
* Called any time an AND is encountered.
*/
void and();
*/
default void and()
{
throw UNSUPPORTED;
}
/**
* Called any time an OR is encountered.
*/
void or();
*/
default void or()
{
throw UNSUPPORTED;
}
default Collection<String> getProperty(String propertyName, int type, boolean negated)
{
throw UNSUPPORTED;
}
}
/**
* Default implementation. Override the methods you are interested in. If you don't
* override the methods then an InvalidQueryException will be thrown.
*/
public static class WalkerCallbackAdapter implements WalkerCallback
{
private static final String UNSUPPORTED_TEXT = "Unsupported Predicate";
protected static final InvalidQueryException UNSUPPORTED = new InvalidQueryException(UNSUPPORTED_TEXT);
@Override
public void exists(String propertyName, boolean negated) { throw UNSUPPORTED;}
@Override
public void between(String propertyName, String firstValue, String secondValue, boolean negated) { throw UNSUPPORTED;}
@Override
public void comparison(int type, String propertyName, String propertyValue, boolean negated) { throw UNSUPPORTED;}
@Override
public void in(String propertyName, boolean negated, String... propertyValues) { throw UNSUPPORTED;}
@Override
public void matches(String property, String value, boolean negated) { throw UNSUPPORTED;}
@Override
public void and() {throw UNSUPPORTED;}
@Override
public void or() {throw UNSUPPORTED;}
}
public static class WalkerCallbackAdapter implements WalkerCallback {}
/**
* Walks a query with a callback for each operation
@@ -146,7 +164,7 @@ public abstract class QueryHelper
if (tree != null)
{
switch (tree.getType()) {
case WhereClauseParser.EXISTS:
case EXISTS:
if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
{
callback.exists(tree.getChild(0).getText(), negated);
@@ -160,7 +178,7 @@ public abstract class QueryHelper
return;
}
break;
case WhereClauseParser.IN:
case IN:
if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
{
List<Tree> children = getChildren(tree);
@@ -174,14 +192,14 @@ public abstract class QueryHelper
return;
}
break;
case WhereClauseParser.BETWEEN:
case BETWEEN:
if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
{
callback.between(tree.getChild(0).getText(), stripQuotes(tree.getChild(1).getText()), stripQuotes(tree.getChild(2).getText()), negated);
return;
}
break;
case WhereClauseParser.EQUALS: //fall through (comparison)
case EQUALS: //fall through (comparison)
case WhereClauseParser.LESSTHAN: //fall through (comparison)
case WhereClauseParser.GREATERTHAN: //fall through (comparison)
case WhereClauseParser.LESSTHANOREQUALS: //fall through (comparison)
@@ -286,4 +304,180 @@ public abstract class QueryHelper
}
return toBeStripped; //default to return the String unchanged.
}
public static QueryResolver.WalkerSpecifier resolve(final Query query)
{
return new QueryResolver.WalkerSpecifier(query);
}
/**
* Helper class allowing WHERE query resolving using query walker. By default {@link BasicQueryWalker} is used, but different walker can be supplied.
*/
public static abstract class QueryResolver<S extends QueryResolver<?>>
{
private final Query query;
protected WalkerCallback queryWalker;
protected Function<Collection<String>, BasicQueryWalker> orQueryWalkerSupplier;
protected boolean clausesNegatable = true;
protected boolean validateLeniently = false;
protected abstract S self();
public QueryResolver(Query query)
{
this.query = query;
}
/**
* Get property expected values.
* @param propertyName Property name.
* @param clauseType Property comparison type.
* @param negated Comparison type negation.
* @return Map composed of all comparators and compared values.
*/
public Collection<String> getProperty(final String propertyName, final int clauseType, final boolean negated)
{
processQuery(propertyName);
return queryWalker.getProperty(propertyName, clauseType, negated);
}
protected void processQuery(final String... propertyNames)
{
if (queryWalker == null)
{
if (orQueryWalkerSupplier != null)
{
queryWalker = orQueryWalkerSupplier.apply(Set.of(propertyNames));
}
else
{
queryWalker = new BasicQueryWalker(propertyNames);
}
}
if (queryWalker instanceof BasicQueryWalker)
{
((BasicQueryWalker) queryWalker).setClausesNegatable(clausesNegatable);
((BasicQueryWalker) queryWalker).setValidateStrictly(!validateLeniently);
}
walk(query, queryWalker);
}
/**
* Helper class providing methods related with default query walker {@link BasicQueryWalker}.
*/
public static class DefaultWalkerOperations<R extends DefaultWalkerOperations<?>> extends QueryResolver<R>
{
public DefaultWalkerOperations(Query query)
{
super(query);
}
@SuppressWarnings("unchecked")
@Override
protected R self()
{
return (R) this;
}
/**
* Specifies that query properties and comparison types should NOT be verified strictly.
*/
public R leniently()
{
this.validateLeniently = true;
return self();
}
/**
* Specifies that clause types negations are not allowed in query.
*/
public R withoutNegations()
{
this.clausesNegatable = false;
return self();
}
/**
* Get property with expected values.
* @param propertyName Property name.
* @return Map composed of all comparators and compared values.
*/
public WhereProperty getProperty(final String propertyName)
{
processQuery(propertyName);
return ((BasicQueryWalker) this.queryWalker).getProperty(propertyName);
}
/**
* Get multiple properties with it's expected values.
* @param propertyNames Property names.
* @return List of maps composed of all comparators and compared values.
*/
public List<WhereProperty> getProperties(final String... propertyNames)
{
processQuery(propertyNames);
return ((BasicQueryWalker) this.queryWalker).getProperties(propertyNames);
}
/**
* Get multiple properties with it's expected values.
* @param propertyNames Property names.
* @return Map composed of property names and maps composed of all comparators and compared values.
*/
public Map<String, WhereProperty> getPropertiesAsMap(final String... propertyNames)
{
processQuery(propertyNames);
return ((BasicQueryWalker) this.queryWalker).getPropertiesAsMap(propertyNames);
}
}
/**
* Helper class allowing to specify custom {@link WalkerCallback} implementation or {@link BasicQueryWalker} extension.
*/
public static class WalkerSpecifier extends DefaultWalkerOperations<WalkerSpecifier>
{
public WalkerSpecifier(Query query)
{
super(query);
}
@Override
protected WalkerSpecifier self()
{
return this;
}
/**
* Specifies that OR operator instead of AND should be used while resolving the query.
*/
public DefaultWalkerOperations<? extends DefaultWalkerOperations<?>> usingOrOperator()
{
this.orQueryWalkerSupplier = (propertyNames) -> new BasicQueryWalker(propertyNames)
{
@Override
public void or() {/*Enable OR support, disable AND support*/}
@Override
public void and() {throw UNSUPPORTED;}
};
return this;
}
/**
* Allows to specify custom {@link BasicQueryWalker} extension, which should be used to resolve the query.
*/
public <T extends BasicQueryWalker> DefaultWalkerOperations<? extends DefaultWalkerOperations<?>> usingWalker(final T queryWalker)
{
this.queryWalker = queryWalker;
return this;
}
/**
* Allows to specify custom {@link WalkerCallback} implementation, which should be used to resolve the query.
*/
public <T extends WalkerCallback> QueryResolver<? extends QueryResolver<?>> usingWalker(final T queryWalker)
{
this.queryWalker = queryWalker;
return this;
}
}
}
}

View File

@@ -0,0 +1,351 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.framework.resource.parameters.where;
import static java.util.function.Predicate.not;
import static org.alfresco.rest.framework.resource.parameters.where.BasicQueryWalker.MISSING_ANY_CLAUSE_OF_TYPE;
import static org.alfresco.rest.framework.resource.parameters.where.BasicQueryWalker.MISSING_CLAUSE_TYPE;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.alfresco.rest.antlr.WhereClauseParser;
/**
* Map composed of property comparison type and compared values.
* Map key is clause (comparison) type.
*/
public class WhereProperty extends HashMap<WhereProperty.ClauseType, Collection<String>>
{
private final String name;
private boolean validateStrictly;
public WhereProperty(final String name, final ClauseType clauseType, final Collection<String> values)
{
super(Map.of(clauseType, new HashSet<>(values)));
this.name = name;
this.validateStrictly = true;
}
public WhereProperty(final String name, final ClauseType clauseType, final Collection<String> values, final boolean validateStrictly)
{
this(name, clauseType, values);
this.validateStrictly = validateStrictly;
}
public String getName()
{
return name;
}
public void addValuesToType(final ClauseType clauseType, final Collection<String> values)
{
if (this.containsKey(clauseType))
{
this.get(clauseType).addAll(values);
}
else
{
this.put(clauseType, new HashSet<>(values));
}
}
public boolean containsType(final ClauseType clauseType)
{
return this.containsKey(clauseType);
}
public boolean containsType(final int clauseType, final boolean negated)
{
return this.containsKey(ClauseType.of(clauseType, negated));
}
public boolean containsAllTypes(final ClauseType... clauseType)
{
return Arrays.stream(clauseType).distinct().filter(this::containsKey).count() == clauseType.length;
}
public boolean containsAnyOfTypes(final ClauseType... clauseType)
{
return Arrays.stream(clauseType).distinct().anyMatch(this::containsKey);
}
public Collection<String> getExpectedValuesFor(final ClauseType clauseType)
{
verifyAllClausesPresence(clauseType);
return this.get(clauseType);
}
public HashMap<ClauseType, Collection<String>> getExpectedValuesForAllOf(final ClauseType... clauseTypes)
{
verifyAllClausesPresence(clauseTypes);
return Arrays.stream(clauseTypes)
.distinct()
.collect(Collectors.toMap(type -> type, this::get, (type1, type2) -> type1, MultiTypeNegatableValuesMap::new));
}
public HashMap<ClauseType, Collection<String>> getExpectedValuesForAnyOf(final ClauseType... clauseTypes)
{
verifyAnyClausesPresence(clauseTypes);
return Arrays.stream(clauseTypes)
.distinct()
.collect(Collectors.toMap(type -> type, this::get, (type1, type2) -> type1, MultiTypeNegatableValuesMap::new));
}
public Collection<String> getExpectedValuesFor(final int clauseType, final boolean negated)
{
verifyAllClausesPresence(ClauseType.of(clauseType, negated));
return this.get(ClauseType.of(clauseType, negated));
}
public NegatableValuesMap getExpectedValuesFor(final int clauseType)
{
verifyAllClausesPresence(clauseType);
final NegatableValuesMap values = new NegatableValuesMap();
final ClauseType type = ClauseType.of(clauseType);
final ClauseType negatedType = type.negate();
if (this.containsKey(type))
{
values.put(false, this.get(type));
}
if (this.containsKey(negatedType))
{
values.put(true, this.get(negatedType));
}
return values;
}
public MultiTypeNegatableValuesMap getExpectedValuesForAllOf(final int... clauseTypes)
{
verifyAllClausesPresence(clauseTypes);
return getExpectedValuesFor(clauseTypes);
}
public MultiTypeNegatableValuesMap getExpectedValuesForAnyOf(final int... clauseTypes)
{
verifyAnyClausesPresence(clauseTypes);
return getExpectedValuesFor(clauseTypes);
}
private MultiTypeNegatableValuesMap getExpectedValuesFor(final int... clauseTypes)
{
final MultiTypeNegatableValuesMap values = new MultiTypeNegatableValuesMap();
Arrays.stream(clauseTypes).distinct().forEach(clauseType -> {
final ClauseType type = ClauseType.of(clauseType);
final ClauseType negatedType = type.negate();
if (this.containsKey(type))
{
values.put(type, this.get(type));
}
if (this.containsKey(negatedType))
{
values.put(negatedType, this.get(negatedType));
}
});
return values;
}
/**
* Verify if all specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
*/
private void verifyAllClausesPresence(final ClauseType... clauseTypes)
{
if (validateStrictly)
{
Arrays.stream(clauseTypes).distinct().forEach(clauseType -> {
if (!this.containsType(clauseType))
{
throw new InvalidQueryException(String.format(MISSING_CLAUSE_TYPE, this.name, WhereClauseParser.tokenNames[clauseType.getTypeNumber()]));
}
});
}
}
/**
* Verify if all specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
* Exception is thrown when both, negated and non-negated types are missing.
*/
private void verifyAllClausesPresence(final int... clauseTypes)
{
if (validateStrictly)
{
Arrays.stream(clauseTypes).distinct().forEach(clauseType -> {
if (!this.containsType(clauseType, false) && !this.containsType(clauseType, true))
{
throw new InvalidQueryException(String.format(MISSING_CLAUSE_TYPE, this.name, WhereClauseParser.tokenNames[clauseType]));
}
});
}
}
/**
* Verify if any of specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
*/
private void verifyAnyClausesPresence(final ClauseType... clauseTypes)
{
if (validateStrictly)
{
if (!this.containsAnyOfTypes(clauseTypes))
{
throw new InvalidQueryException(String.format(MISSING_ANY_CLAUSE_OF_TYPE,
this.name, Arrays.stream(clauseTypes).map(type -> WhereClauseParser.tokenNames[type.getTypeNumber()]).collect(Collectors.toList())));
}
}
}
/**
* Verify if any of specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
* Exception is thrown when both, negated and non-negated types are missing.
*/
private void verifyAnyClausesPresence(final int... clauseTypes)
{
if (validateStrictly)
{
final Collection<ClauseType> expectedTypes = Arrays.stream(clauseTypes)
.distinct()
.boxed()
.flatMap(type -> Stream.of(ClauseType.of(type), ClauseType.of(type, true)))
.collect(Collectors.toSet());
if (!this.containsAnyOfTypes(expectedTypes.toArray(ClauseType[]::new)))
{
throw new InvalidQueryException(String.format(MISSING_ANY_CLAUSE_OF_TYPE,
this.name, Arrays.stream(clauseTypes).mapToObj(type -> WhereClauseParser.tokenNames[type]).collect(Collectors.toList())));
}
}
}
public enum ClauseType
{
EQUALS(WhereClauseParser.EQUALS),
NOT_EQUALS(WhereClauseParser.EQUALS, true),
GREATER_THAN(WhereClauseParser.GREATERTHAN),
NOT_GREATER_THAN(WhereClauseParser.GREATERTHAN, true),
LESS_THAN(WhereClauseParser.LESSTHAN),
NOT_LESS_THAN(WhereClauseParser.LESSTHAN, true),
GREATER_THAN_OR_EQUALS(WhereClauseParser.GREATERTHANOREQUALS),
NOT_GREATER_THAN_OR_EQUALS(WhereClauseParser.GREATERTHANOREQUALS, true),
LESS_THAN_OR_EQUALS(WhereClauseParser.LESSTHANOREQUALS),
NOT_LESS_THAN_OR_EQUALS(WhereClauseParser.LESSTHANOREQUALS, true),
BETWEEN(WhereClauseParser.BETWEEN),
NOT_BETWEEN(WhereClauseParser.BETWEEN, true),
IN(WhereClauseParser.IN),
NOT_IN(WhereClauseParser.IN, true),
MATCHES(WhereClauseParser.MATCHES),
NOT_MATCHES(WhereClauseParser.MATCHES, true),
EXISTS(WhereClauseParser.EXISTS),
NOT_EXISTS(WhereClauseParser.EXISTS, true);
private final int typeNumber;
private final boolean negated;
ClauseType(final int typeNumber)
{
this.typeNumber = typeNumber;
this.negated = false;
}
ClauseType(final int typeNumber, final boolean negated)
{
this.typeNumber = typeNumber;
this.negated = negated;
}
public static ClauseType of(final int type)
{
return of(type, false);
}
public static ClauseType of(final int type, final boolean negated)
{
return Arrays.stream(ClauseType.values())
.filter(clauseType -> clauseType.typeNumber == type && clauseType.negated == negated)
.findFirst()
.orElseThrow();
}
public ClauseType negate()
{
return of(typeNumber, !negated);
}
public int getTypeNumber()
{
return typeNumber;
}
public boolean isNegated()
{
return negated;
}
}
public static class NegatableValuesMap extends HashMap<Boolean, Collection<String>>
{
public Collection<String> skipNegated()
{
return this.get(false);
}
public Collection<String> onlyNegated()
{
return this.get(true);
}
}
public static class MultiTypeNegatableValuesMap extends HashMap<ClauseType, Collection<String>>
{
public Map<Integer, Collection<String>> skipNegated()
{
return this.keySet().stream()
.filter(not(ClauseType::isNegated))
.collect(Collectors.toMap(key -> key.typeNumber, this::get));
}
public Collection<String> skipNegated(final int clauseType)
{
return this.get(ClauseType.of(clauseType));
}
public Map<Integer, Collection<String>> onlyNegated()
{
return this.keySet().stream()
.filter(not(ClauseType::isNegated))
.collect(Collectors.toMap(key -> key.typeNumber, this::get));
}
public Collection<String> onlyNegated(final int clauseType)
{
return this.get(ClauseType.of(clauseType, true));
}
}
}

View File

@@ -1,32 +1,33 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.workflow.api.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -51,7 +52,7 @@ import org.apache.commons.beanutils.ConvertUtils;
* {@link InvalidArgumentException} is thrown unless the method
* {@link #handleUnmatchedComparison(int, String, String)} returns true (default
* implementation returns false).
*
*
* @author Frederik Heremans
* @author Tijs Rademakers
*/
@@ -72,21 +73,21 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
private Map<String, String> equalsProperties;
private Map<String, String> matchesProperties;
private Map<String, String> greaterThanProperties;
private Map<String, String> greaterThanOrEqualProperties;
private Map<String, String> lessThanProperties;
private Map<String, String> lessThanOrEqualProperties;
private List<QueryVariableHolder> variableProperties;
private boolean variablesEnabled;
private NamespaceService namespaceService;
private DictionaryService dictionaryService;
public MapBasedQueryWalker(Set<String> supportedEqualsParameters, Set<String> supportedMatchesParameters)
@@ -132,7 +133,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
lessThanOrEqualProperties = new HashMap<String, String>();
}
}
public void enableVariablesSupport(NamespaceService namespaceService, DictionaryService dictionaryService)
{
variablesEnabled = true;
@@ -148,7 +149,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
this.dictionaryService = dictionaryService;
variableProperties = new ArrayList<QueryVariableHolder>();
}
public List<QueryVariableHolder> getVariableProperties() {
return variableProperties;
}
@@ -158,9 +159,9 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
if(negated)
{
throw new InvalidArgumentException("Cannot use negated matching for property: " + property);
throw new InvalidArgumentException("Cannot use negated matching for property: " + property);
}
if (variablesEnabled && property.startsWith("variables/"))
if (variablesEnabled && property.startsWith("variables/"))
{
processVariable(property, value, WhereClauseParser.MATCHES);
}
@@ -170,19 +171,19 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throw new InvalidArgumentException("Cannot use matching for property: " + property);
throw new InvalidArgumentException("Cannot use matching for property: " + property);
}
}
@Override
public void comparison(int type, String propertyName, String propertyValue, boolean negated)
{
if (variablesEnabled && propertyName.startsWith("variables/"))
if (variablesEnabled && propertyName.startsWith("variables/"))
{
processVariable(propertyName, propertyValue, type);
processVariable(propertyName, propertyValue, type);
return;
}
}
boolean throwError = false;
if (type == WhereClauseParser.EQUALS)
{
@@ -192,7 +193,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.MATCHES)
@@ -203,7 +204,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.GREATERTHAN)
@@ -214,7 +215,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.GREATERTHANOREQUALS)
@@ -225,7 +226,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.LESSTHAN)
@@ -236,7 +237,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.LESSTHANOREQUALS)
@@ -247,7 +248,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else
@@ -255,15 +256,24 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
if (throwError)
{
throw new InvalidArgumentException("framework.exception.InvalidProperty", new Object[] {propertyName, propertyValue, WhereClauseParser.tokenNames[type]});
}
else if (negated)
{
// Throw error for the unsupported negation only if the property was valid for comparison, show the more meaningful error first.
throw new InvalidArgumentException("Cannot use NOT for " + WhereClauseParser.tokenNames[type] + " comparison.");
if (throwError)
{
throw new InvalidArgumentException("framework.exception.InvalidProperty", new Object[] {propertyName, propertyValue, WhereClauseParser.tokenNames[type]});
}
else if (negated)
{
// Throw error for the unsupported negation only if the property was valid for comparison, show the more meaningful error first.
throw new InvalidArgumentException("Cannot use NOT for " + WhereClauseParser.tokenNames[type] + " comparison.");
}
}
/**
* Get expected value for property and comparison type. This class supports only non-negated comparisons, thus parameter negated is ignored in bellow method.
*/
@Override
public Collection<String> getProperty(String propertyName, int type, boolean negated)
{
return Set.of(this.getProperty(propertyName, type));
}
public String getProperty(String propertyName, int type)
@@ -300,7 +310,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
/**
* Get the property value, converted to the requested type.
*
*
* @param propertyName name of the parameter
* @param type int
* @param returnType type of object to return
@@ -334,7 +344,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
// Conversion failed, wrap in Illegal
throw new InvalidArgumentException("Query property value for '" + propertyName + "' should be a valid "
+ returnType.getSimpleName());
+ returnType.getSimpleName());
}
}
@@ -345,7 +355,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
// method indicates that AND is
// supported. OR is not supported at the same time.
}
protected void processVariable(String propertyName, String propertyValue, int type)
{
String localPropertyName = propertyName.replaceFirst("variables/", "");
@@ -353,25 +363,25 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
DataTypeDefinition dataTypeDefinition = null;
// variable scope global is default
String scopeDef = "global";
// look for variable scope
if (localPropertyName.contains("local/"))
{
scopeDef = "local";
localPropertyName = localPropertyName.replaceFirst("local/", "");
}
if (localPropertyName.contains("global/"))
{
localPropertyName = localPropertyName.replaceFirst("global/", "");
}
// look for variable type definition
if ((propertyValue.contains("_") || propertyValue.contains(":")) && propertyValue.contains(" "))
if ((propertyValue.contains("_") || propertyValue.contains(":")) && propertyValue.contains(" "))
{
int indexOfSpace = propertyValue.indexOf(' ');
if ((propertyValue.contains("_") && indexOfSpace > propertyValue.indexOf("_")) ||
(propertyValue.contains(":") && indexOfSpace > propertyValue.indexOf(":")))
if ((propertyValue.contains("_") && indexOfSpace > propertyValue.indexOf("_")) ||
(propertyValue.contains(":") && indexOfSpace > propertyValue.indexOf(":")))
{
String typeDef = propertyValue.substring(0, indexOfSpace);
try
@@ -386,7 +396,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
}
}
if (dataTypeDefinition != null && "java.util.Date".equalsIgnoreCase(dataTypeDefinition.getJavaClassName()))
{
// fix for different ISO 8601 Date format classes in Alfresco (org.alfresco.util and Spring Surf)
@@ -396,18 +406,18 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
actualValue = DefaultTypeConverter.INSTANCE.convert(dataTypeDefinition, propertyValue);
}
else
else
{
actualValue = propertyValue;
}
variableProperties.add(new QueryVariableHolder(localPropertyName, type, actualValue, scopeDef));
}
/**
* Called when unsupported property is encountered or comparison operator
* other than equals.
*
*
* @return true, if the comparison is handles successfully. False, if an
* exception should be thrown because the comparison can't be
* handled.
@@ -416,25 +426,25 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
return false;
}
public static class QueryVariableHolder implements Serializable
{
private static final long serialVersionUID = 1L;
private String propertyName;
private int operator;
private Object propertyValue;
private String scope;
public QueryVariableHolder() {}
public QueryVariableHolder(String propertyName, int operator, Object propertyValue, String scope) {
this.propertyName = propertyName;
this.operator = operator;
this.propertyValue = propertyValue;
this.scope = scope;
}
public String getPropertyName()
{
return propertyName;

View File

@@ -858,6 +858,7 @@
<property name="taggingService" ref="TaggingService" />
<property name="authorityService" ref="AuthorityService" />
<property name="typeConstraint" ref="nodeTypeConstraint" />
<property name="nodeService" ref="NodeService"/>
</bean>
<bean id="Tags" class="org.springframework.aop.framework.ProxyFactoryBean">

View File

@@ -27,26 +27,38 @@ package org.alfresco.rest.api.impl;
import static org.alfresco.rest.api.impl.TagsImpl.NOT_A_VALID_TAG;
import static org.alfresco.rest.api.impl.TagsImpl.NO_PERMISSION_TO_MANAGE_A_TAG;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException;
import org.alfresco.rest.framework.tools.RecognizedParamsExtractor;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
@@ -62,17 +74,29 @@ import org.mockito.junit.MockitoJUnitRunner;
public class TagsImplTest
{
private static final String TAG_ID = "tag-node-id";
private static final String PARENT_NODE_ID = "tag:tag-root";
private static final String TAG_NAME = "tag-dummy-name";
private static final NodeRef TAG_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
private static final NodeRef TAG_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(TAG_NAME));
private static final NodeRef TAG_PARENT_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_NODE_ID);
private final RecognizedParamsExtractor queryExtractor = new RecognizedParamsExtractor() {};
@Mock
private Nodes nodesMock;
@Mock
private ChildAssociationRef primaryParentMock;
@Mock
private NodeService nodeServiceMock;
@Mock
private AuthorityService authorityServiceMock;
@Mock
private TaggingService taggingServiceMock;
@Mock
private Parameters parametersMock;
@Mock
private Paging pagingMock;
@Mock
private PagingResults<Pair<NodeRef, String>> pagingResultsMock;
@InjectMocks
private TagsImpl objectUnderTest;
@@ -81,36 +105,147 @@ public class TagsImplTest
public void setup()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
given(nodesMock.validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID)).willReturn(TAG_NODE_REF);
given(nodesMock.validateNode(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID)).willReturn(TAG_NODE_REF);
given(taggingServiceMock.getTagName(TAG_NODE_REF)).willReturn(TAG_NAME);
given(nodeServiceMock.getPrimaryParent(TAG_NODE_REF)).willReturn(primaryParentMock);
}
@Test
public void testGetTags() {
final List<String> tagNames = List.of("testTag","tag11");
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
public void testGetTags()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
given(pagingResultsMock.getPage()).willReturn(List.of(new Pair<>(TAG_NODE_REF, TAG_NAME)));
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), isNull(), isNull());
then(taggingServiceMock).shouldHaveNoMoreInteractions();
final List<Tag> expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME)).stream().peek(tag -> tag.setCount(0)).collect(Collectors.toList());
assertEquals(expectedTags, actualTags.getCollection());
}
@Test
public void testGetTags_verifyIfCountIsZero()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
given(pagingResultsMock.getPage()).willReturn(List.of(new Pair<>(TAG_NODE_REF, TAG_NAME)));
given(parametersMock.getInclude()).willReturn(List.of("count"));
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).stream()
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE);
final List<Tag> expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME)).stream()
.peek(tag -> tag.setCount(0))
.collect(Collectors.toList());
assertEquals(expectedTags, actualCreatedTags);
assertEquals(expectedTags, actualTags.getCollection());
}
@Test
public void testGetTags_withEqualsClauseWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag=expectedName)"));
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
//when
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), eq(Set.of("expectedname")), isNull());
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualTags).isNotNull();
}
@Test
public void testGetTags_withInClauseWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag IN (expectedName1, expectedName2))"));
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
//when
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), eq(Set.of("expectedname1", "expectedname2")), isNull());
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualTags).isNotNull();
}
@Test
public void testGetTags_withMatchesClauseWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag MATCHES ('expectedName*'))"));
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
//when
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), isNull(), eq(Set.of("expectedname*")));
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualTags).isNotNull();
}
@Test
public void testGetTags_withBothInAndEqualsClausesInSingleWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag=expectedName AND tag IN (expectedName1, expectedName2))"));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testGetTags_withOtherClauseInWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag BETWEEN ('expectedName', 'expectedName2'))"));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testGetTags_withNotEqualsClauseInWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(NOT tag=expectedName)"));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testDeleteTagById()
{
//when
objectUnderTest.deleteTagById(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
given(primaryParentMock.getParentRef()).willReturn(TAG_PARENT_NODE_REF);
objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(nodesMock).should().validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
then(nodesMock).should().validateNode(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).should().getTagName(TAG_NODE_REF);
then(taggingServiceMock).should().deleteTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_NAME);
then(taggingServiceMock).should().deleteTag(STORE_REF_WORKSPACE_SPACESSTORE, TAG_NAME);
then(taggingServiceMock).shouldHaveNoMoreInteractions();
}
@@ -120,7 +255,7 @@ public class TagsImplTest
given(authorityServiceMock.hasAdminAuthority()).willReturn(false);
//when
assertThrows(PermissionDeniedException.class, () -> objectUnderTest.deleteTagById(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID));
assertThrows(PermissionDeniedException.class, () -> objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID));
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
@@ -134,12 +269,12 @@ public class TagsImplTest
public void testDeleteTagById_nonExistentTag()
{
//when
assertThrows(EntityNotFoundException.class, () -> objectUnderTest.deleteTagById(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id"));
assertThrows(EntityNotFoundException.class, () -> objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id"));
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(nodesMock).should().validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id");
then(nodesMock).should().validateNode(STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id");
then(nodesMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).shouldHaveNoInteractions();
@@ -157,11 +292,11 @@ public class TagsImplTest
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, tagNames);
then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, tagNames);
then(taggingServiceMock).shouldHaveNoMoreInteractions();
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames);
assertThat(actualCreatedTags)
.isNotNull()
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedTags);
}
@@ -225,7 +360,7 @@ public class TagsImplTest
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(List.of(createTag(TAG_NAME)), parametersMock));
then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualException).isInstanceOf(DuplicateChildNodeNameException.class);
}
@@ -240,7 +375,7 @@ public class TagsImplTest
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
final List<Tag> expectedTags = List.of(createTagWithNodeRef(TAG_NAME));
assertThat(actualCreatedTags)
.isNotNull()
@@ -266,10 +401,21 @@ public class TagsImplTest
.isEqualTo(expectedTags);
}
@Test(expected = EntityNotFoundException.class)
public void testGetTagByIdNotFoundValidation()
{
given(primaryParentMock.getParentRef()).willReturn(TAG_NODE_REF);
objectUnderTest.getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,TAG_ID);
then(nodeServiceMock).shouldHaveNoInteractions();
then(nodesMock).should().validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).shouldHaveNoInteractions();
}
private static List<Pair<String, NodeRef>> createTagAndNodeRefPairs(final List<String> tagNames)
{
return tagNames.stream()
.map(tagName -> createPair(tagName, new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName))))
.map(tagName -> createPair(tagName, new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName))))
.collect(Collectors.toList());
}
@@ -298,7 +444,7 @@ public class TagsImplTest
private static Tag createTagWithNodeRef(final String tagName)
{
return Tag.builder()
.nodeRef(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName)))
.nodeRef(new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName)))
.tag(tagName)
.create();
}

View File

@@ -529,7 +529,7 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
interceptingRemoteUserMapper.setActive(true);
interceptingRemoteUserMapper.setPersonService(personServiceLocal);
interceptingRemoteUserMapper.setIdentityServiceDeployment(null);
interceptingRemoteUserMapper.setIdentityServiceFacade(null);
interceptingRemoteUserMapper.setUserIdToReturn(user2);
remoteUserMapper = interceptingRemoteUserMapper;
}

View File

@@ -0,0 +1,666 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.framework.resource.parameters.where;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.catchThrowable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.framework.tools.RecognizedParamsExtractor;
import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker;
import org.junit.Test;
/**
* Tests verifying {@link QueryHelper.QueryResolver} functionality based on {@link BasicQueryWalker}.
*/
public class QueryResolverTest
{
private final RecognizedParamsExtractor queryExtractor = new RecognizedParamsExtractor() {};
@Test
public void testResolveQuery_equals()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, true)).isFalse();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_greaterThan()
{
final Query query = queryExtractor.getWhereClause("(propName > testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHAN, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_greaterThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(propName >= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHANOREQUALS, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_lessThan()
{
final Query query = queryExtractor.getWhereClause("(propName < testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHAN, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_lessThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(propName <= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHANOREQUALS, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_between()
{
final Query query = queryExtractor.getWhereClause("(propName BETWEEN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.BETWEEN, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_in()
{
final Query query = queryExtractor.getWhereClause("(propName IN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.IN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.IN, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_matches()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES ('*Value'))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.MATCHES, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.MATCHES, false)).containsOnly("*Value");
}
@Test
public void testResolveQuery_exists()
{
final Query query = queryExtractor.getWhereClause("(EXISTS (propName))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EXISTS, false)).isEmpty();
}
@Test
public void testResolveQuery_notEquals()
{
final Query query = queryExtractor.getWhereClause("(NOT propName=testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, true)).isTrue();
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, true)).isFalse();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notGreaterThan()
{
final Query query = queryExtractor.getWhereClause("(NOT propName > testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHAN, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notGreaterThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(NOT propName >= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHANOREQUALS, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notLessThan()
{
final Query query = queryExtractor.getWhereClause("(NOT propName < testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHAN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHAN, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notLessThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(NOT propName <= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHANOREQUALS, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notBetween()
{
final Query query = queryExtractor.getWhereClause("(NOT propName BETWEEN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.BETWEEN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.BETWEEN, true)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_notIn()
{
final Query query = queryExtractor.getWhereClause("(NOT propName IN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.IN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.IN, true)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_notMatches()
{
final Query query = queryExtractor.getWhereClause("(NOT propName MATCHES ('*Value'))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.MATCHES, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.MATCHES, true)).containsOnly("*Value");
}
@Test
public void testResolveQuery_notExists()
{
final Query query = queryExtractor.getWhereClause("(NOT EXISTS (propName))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EXISTS, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EXISTS, true)).isEmpty();
}
@Test
public void testResolveQuery_propertyNotExpected()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND differentName>18)");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("differentName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_propertyNotExpectedUsingLenientApproach()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND differentName>18)");
//when
final WhereProperty property = QueryHelper.resolve(query).leniently().getProperty("differentName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EQUALS, true)).isFalse();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).isNull();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, true)).isNull();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHAN, false)).containsOnly("18");
}
@Test
public void testResolveQuery_propertyNotPresentUsingLenientApproach()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("differentName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_slashInPropertyName()
{
final Query query = queryExtractor.getWhereClause("(EXISTS (prop/name/with/slashes))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("prop/name/with/slashes");
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EXISTS, false)).isEmpty();
}
@Test
public void testResolveQuery_propertyBetweenDates()
{
final Query query = queryExtractor.getWhereClause("(propName BETWEEN ('2012-01-01', '2012-12-31'))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.BETWEEN, false)).containsOnly("2012-01-01", "2012-12-31");
}
@Test
public void testResolveQuery_singlePropertyGreaterThanOrEqualsAndLessThan()
{
final Query query = queryExtractor.getWhereClause("(propName >= 18 AND propName < 65)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHANOREQUALS, false)).containsOnly("18");
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHAN, false)).containsOnly("65");
}
@Test
public void testResolveQuery_onePropertyGreaterThanAndSecondPropertyNotMatches()
{
final Query query = queryExtractor.getWhereClause("(propName1 > 20 AND NOT propName2 MATCHES ('external*'))");
//when
final List<WhereProperty> property = QueryHelper.resolve(query).getProperties("propName1", "propName2");
assertThat(property.get(0).containsType(WhereClauseParser.GREATERTHAN, false)).isTrue();
assertThat(property.get(0).getExpectedValuesFor(WhereClauseParser.GREATERTHAN, false)).containsOnly("20");
assertThat(property.get(1).containsType(WhereClauseParser.MATCHES, true)).isTrue();
assertThat(property.get(1).getExpectedValuesFor(WhereClauseParser.MATCHES, true)).containsOnly("external*");
}
@Test
public void testResolveQuery_negationsForbidden()
{
final Query query = queryExtractor.getWhereClause("(NOT propName=testValue)");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).withoutNegations().getProperty("propName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_withoutNegations()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final WhereProperty actualProperty = QueryHelper.resolve(query).withoutNegations().getProperty("propName");
assertThat(actualProperty.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(actualProperty.containsType(WhereClauseParser.EQUALS, true)).isFalse();
assertThat(actualProperty.getExpectedValuesFor(WhereClauseParser.EQUALS).onlyNegated()).isNull();
assertThat(actualProperty.getExpectedValuesFor(WhereClauseParser.EQUALS).skipNegated()).containsOnly("testValue");
}
@Test
public void testResolveQuery_orNotAllowed()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName BETWEEN (testValue2, testValue3))");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("propName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_orAllowedInFavorOfAnd()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName=testValue2)");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.usingOrOperator()
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_usingCustomQueryWalker()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final Collection<String> propertyValues = QueryHelper
.resolve(query)
.usingWalker(new MapBasedQueryWalker(Set.of("propName"), null))
.getProperty("propName", WhereClauseParser.EQUALS, false);
assertThat(propertyValues).containsOnly("testValue");
}
@Test
public void testResolveQuery_usingCustomBasicQueryWalkerExtension()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName=testValue2)");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.usingWalker(new BasicQueryWalker("propName")
{
@Override
public void or() {}
@Override
public void and() {throw UNSUPPORTED;}
})
.withoutNegations()
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_equalsAndInNotAllowedTogether()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName IN (testValue2, testValue3))");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("propName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_equalsOrInAllowedTogether()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName IN (testValue2, testValue3))");
//when
final WhereProperty whereProperty = QueryHelper.resolve(query).usingOrOperator().getProperty("propName");
assertThat(whereProperty).isNotNull();
assertThat(whereProperty.getExpectedValuesForAllOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated())
.isEqualTo(Map.of(WhereClauseParser.EQUALS, Set.of("testValue"), WhereClauseParser.IN, Set.of("testValue2", "testValue3")));
}
@Test
public void testResolveQuery_equalsAndInAllowedTogetherWithDifferentProperties()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName2 IN (testValue2, testValue3))");
//when
final List<WhereProperty> properties = QueryHelper
.resolve(query)
.getProperties("propName", "propName2");
assertThat(properties.get(0).containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(properties.get(0).containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(properties.get(0).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.EQUALS)).containsOnly("testValue");
assertThat(properties.get(0).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.IN)).isFalse();
assertThat(properties.get(1).containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(properties.get(1).containsType(WhereClauseParser.IN, false)).isTrue();
assertThat(properties.get(1).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.EQUALS)).isFalse();
assertThat(properties.get(1).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.IN)).containsOnly("testValue2", "testValue3");
}
@Test
public void testResolveQuery_equalsAndInAllowedAlternately_equals()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.EQUALS)).containsOnly("testValue");
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.IN)).isFalse();
}
@Test
public void testResolveQuery_equalsAndInAllowedAlternately_in()
{
final Query query = queryExtractor.getWhereClause("(propName IN (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, false)).isTrue();
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.EQUALS)).isFalse();
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.IN)).containsOnly("testValue");
}
@Test
public void testResolveQuery_missingEqualsClauseType()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThatExceptionOfType(InvalidQueryException.class)
.isThrownBy(() -> property.getExpectedValuesForAllOf(WhereClauseParser.EQUALS, WhereClauseParser.MATCHES));
}
@Test
public void testResolveQuery_ignoreUnexpectedClauseType()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.getExpectedValuesForAllOf(WhereClauseParser.EQUALS).skipNegated(WhereClauseParser.EQUALS)).containsOnly("testValue");
}
@Test
public void testResolveQuery_complexAndQuery()
{
final Query query = queryExtractor.getWhereClause("(a=v1 AND b>18 AND b<=65 AND NOT c BETWEEN ('2012-01-01','2012-12-31') AND d IN (v1, v2) AND e MATCHES ('*@mail.com') AND EXISTS (f/g))");
//when
final List<WhereProperty> properties = QueryHelper
.resolve(query)
.getProperties("a", "b", "c", "d", "e", "f/g");
assertThat(properties).hasSize(6);
assertThat(properties.get(0).getExpectedValuesFor(WhereProperty.ClauseType.EQUALS)).containsOnly("v1");
assertThat(properties.get(1).containsAllTypes(WhereProperty.ClauseType.GREATER_THAN, WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).isTrue();
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.GREATER_THAN)).containsOnly("18");
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).containsOnly("65");
assertThat(properties.get(2).getExpectedValuesFor(WhereProperty.ClauseType.NOT_BETWEEN)).containsOnly("2012-01-01", "2012-12-31");
assertThat(properties.get(3).getExpectedValuesFor(WhereProperty.ClauseType.IN)).containsOnly("v1", "v2");
assertThat(properties.get(4).getExpectedValuesFor(WhereProperty.ClauseType.MATCHES)).containsOnly("*@mail.com");
assertThat(properties.get(5).containsType(WhereProperty.ClauseType.EXISTS)).isTrue();
assertThat(properties.get(5).getExpectedValuesFor(WhereProperty.ClauseType.EXISTS)).isEmpty();
}
@Test
public void testResolveQuery_complexOrQuery()
{
final Query query = queryExtractor.getWhereClause("(a=v1 OR b>18 OR b<=65 OR NOT c BETWEEN ('2012-01-01','2012-12-31') OR d IN (v1, v2) OR e MATCHES ('*@mail.com') OR EXISTS (f/g))");
//when
final List<WhereProperty> properties = QueryHelper
.resolve(query)
.usingOrOperator()
.getProperties("a", "b", "c", "d", "e", "f/g");
assertThat(properties).hasSize(6);
assertThat(properties.get(0).getExpectedValuesFor(WhereProperty.ClauseType.EQUALS)).containsOnly("v1");
assertThat(properties.get(1).containsAllTypes(WhereProperty.ClauseType.GREATER_THAN, WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).isTrue();
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.GREATER_THAN)).containsOnly("18");
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).containsOnly("65");
assertThat(properties.get(2).getExpectedValuesFor(WhereProperty.ClauseType.NOT_BETWEEN)).containsOnly("2012-01-01", "2012-12-31");
assertThat(properties.get(3).getExpectedValuesFor(WhereProperty.ClauseType.IN)).containsOnly("v1", "v2");
assertThat(properties.get(4).getExpectedValuesFor(WhereProperty.ClauseType.MATCHES)).containsOnly("*@mail.com");
assertThat(properties.get(5).containsType(WhereProperty.ClauseType.EXISTS)).isTrue();
assertThat(properties.get(5).getExpectedValuesFor(WhereProperty.ClauseType.EXISTS)).isEmpty();
}
@Test
public void testResolveQuery_clauseTypeOptional()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.MATCHES).skipNegated(WhereClauseParser.MATCHES)).containsOnly("testValue");
}
@Test
public void testResolveQuery_optionalClauseTypesNotPresent()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThatExceptionOfType(InvalidQueryException.class)
.isThrownBy(() -> property.getExpectedValuesForAnyOf(WhereClauseParser.IN));
}
@Test
public void testResolveQuery_matchesOrMatchesAllowed()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES ('test*') OR propName MATCHES ('*value*'))");
//when
final Collection<String> expectedValues = QueryHelper
.resolve(query)
.usingOrOperator()
.getProperty("propName")
.getExpectedValuesFor(WhereClauseParser.MATCHES)
.skipNegated();
assertThat(expectedValues).containsOnly("test*", "*value*");
}
}

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.92</version>
<version>20.113</version>
</parent>
<dependencies>
@@ -119,6 +119,10 @@
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
@@ -387,14 +391,19 @@
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${dependency.spring-security.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</exclusion>
</exclusions>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
@@ -557,17 +566,6 @@
</dependency>
<!-- Keycloak dependencies -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>

View File

@@ -1,61 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import org.keycloak.adapters.BearerTokenRequestAuthenticator;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCAuthenticationError.Reason;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.HttpFacade;
/**
* Extends the Keycloak BearerTokenRequestAuthenticator class to capture the error description
* when token valiation fails.
*
* @author Gavin Cornwell
*/
public class AlfrescoBearerTokenRequestAuthenticator extends BearerTokenRequestAuthenticator
{
private String validationFailureDescription;
public AlfrescoBearerTokenRequestAuthenticator(KeycloakDeployment deployment)
{
super(deployment);
}
public String getValidationFailureDescription()
{
return this.validationFailureDescription;
}
@Override
protected AuthChallenge challengeResponse(HttpFacade facade, Reason reason, String error, String description)
{
this.validationFailureDescription = description;
return super.challengeResponse(facade, reason, error, description);
}
}

View File

@@ -1,119 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.HttpClient;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.springframework.beans.factory.FactoryBean;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
*
* Creates an instance of {@link AuthzClient}. <br>
* The creation of {@link AuthzClient} requires connection to a Keycloak server, disable this factory if Keycloak cannot be reached. <br>
* This factory can return a null if it is disabled.
*
*/
public class AuthenticatorAuthzClientFactoryBean implements FactoryBean<AuthzClient>
{
private static Log logger = LogFactory.getLog(AuthenticatorAuthzClientFactoryBean.class);
private IdentityServiceConfig identityServiceConfig;
private boolean enabled;
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
{
this.identityServiceConfig = identityServiceConfig;
}
@Override
public AuthzClient getObject() throws Exception
{
// The creation of the client can be disabled for testing or when the username/password authentication is not required,
// for instance when Keycloak is configured for 'bearer only' authentication or Direct Access Grants are disabled.
if (!enabled)
{
return null;
}
// Build default http client using the keycloak client builder.
int conTimeout = identityServiceConfig.getClientConnectionTimeout();
int socTimeout = identityServiceConfig.getClientSocketTimeout();
HttpClient client = new HttpClientBuilder()
.establishConnectionTimeout(conTimeout, TimeUnit.MILLISECONDS)
.socketTimeout(socTimeout, TimeUnit.MILLISECONDS)
.build(this.identityServiceConfig);
// Add secret to credentials if needed.
// AuthzClient configuration needs credentials with a secret even if the client in Keycloak is configured as public.
Map<String, Object> credentials = identityServiceConfig.getCredentials();
if (credentials == null || !credentials.containsKey("secret"))
{
credentials = credentials == null ? new HashMap<>() : new HashMap<>(credentials);
credentials.put("secret", "");
}
// Create default AuthzClient for authenticating users against keycloak
String authServerUrl = identityServiceConfig.getAuthServerUrl();
String realm = identityServiceConfig.getRealm();
String resource = identityServiceConfig.getResource();
Configuration authzConfig = new Configuration(authServerUrl, realm, resource, credentials, client);
AuthzClient authzClient = AuthzClient.create(authzConfig);
if (logger.isDebugEnabled())
{
logger.debug(" Created Keycloak AuthzClient");
logger.debug(" Keycloak AuthzClient server URL: " + authzClient.getConfiguration().getAuthServerUrl());
logger.debug(" Keycloak AuthzClient realm: " + authzClient.getConfiguration().getRealm());
logger.debug(" Keycloak AuthzClient resource: " + authzClient.getConfiguration().getResource());
}
return authzClient;
}
@Override
public Class<?> getObjectType()
{
return AuthenticatorAuthzClientFactoryBean.class;
}
@Override
public boolean isSingleton()
{
return true;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,38 +25,35 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.net.ConnectException;
import org.alfresco.error.ExceptionStackUtil;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.util.HttpResponseException;
/**
*
* Authenticates a user against Keycloak.
* Keycloak's {@link AuthzClient} is used to retrieve an access token for the provided user credentials,
* user is set as the current user if the user's access token can be obtained.
* Authenticates a user against Identity Service (Keycloak/Authorization Server).
* {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the
* user credentials are valid.
* <br>
* The AuthzClient can be null in which case this authenticator will just fall through to the next one in the chain.
* The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator
* will just fall through to the next one in the chain.
*
*/
public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
{
private final Log logger = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
/** client used to authenticate user credentials against Keycloak **/
private AuthzClient authzClient;
private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
/** client used to authenticate user credentials against Authorization Server **/
private IdentityServiceFacade identityServiceFacade;
/** enabled flag for the identity service subsystem**/
private boolean active;
private boolean allowGuestLogin;
public void setAuthenticatorAuthzClient(AuthzClient authenticatorAuthzClient)
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
{
this.authzClient = authenticatorAuthzClient;
this.identityServiceFacade = identityServiceFacade;
}
public void setAllowGuestLogin(boolean allowGuestLogin)
@@ -66,50 +63,31 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
{
if (authzClient == null)
if (identityServiceFacade == null)
{
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("AuthzClient was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property. ");
LOGGER.debug("IdentityServiceFacade was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
}
throw new AuthenticationException("User not authenticated because AuthzClient was not set.");
throw new AuthenticationException("User not authenticated because IdentityServiceFacade was not set.");
}
try
{
// Attempt to get an access token using the user credentials
authzClient.obtainAccessToken(userName, new String(password));
// Attempt to verify user credentials
identityServiceFacade.verifyCredentials(userName, new String(password));
// Successfully obtained access token so treat as authenticated user
// Verification was successful so treat as authenticated user
setCurrentUser(userName);
}
catch (HttpResponseException e)
catch (CredentialsVerificationException e)
{
if (logger.isDebugEnabled())
{
logger.debug("Failed to authenticate user against Keycloak. Status: " + e.getStatusCode() + " Reason: "+ e.getReasonPhrase());
}
throw new AuthenticationException("Failed to authenticate user against Keycloak.", e);
throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", e);
}
catch (RuntimeException e)
{
Throwable cause = ExceptionStackUtil.getCause(e, ConnectException.class);
if (cause != null)
{
if (logger.isWarnEnabled())
{
logger.warn("Couldn't connect to Keycloak server to authenticate user. Reason: " + cause.getMessage());
}
throw new AuthenticationException("Couldn't connect to Keycloak server to authenticate user.", cause);
}
if (logger.isDebugEnabled())
{
logger.debug("Error occurred while authenticating user against Keycloak. Reason: " + e.getMessage());
}
throw new AuthenticationException("Error occurred while authenticating user against Keycloak.", e);
throw new AuthenticationException("Failed to verify user credentials.", e);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,6 +26,7 @@
package org.alfresco.repo.security.authentication.identityservice;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.TreeMap;
@@ -33,6 +34,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Class to hold configuration for the Identity Service.
@@ -41,8 +43,9 @@ import org.springframework.beans.factory.InitializingBean;
*/
public class IdentityServiceConfig extends AdapterConfig implements InitializingBean
{
private static Log logger = LogFactory.getLog(IdentityServiceConfig.class);
private static final Log LOGGER = LogFactory.getLog(IdentityServiceConfig.class);
private static final String REALMS = "realms";
private static final String SECRET = "secret";
private static final String CREDENTIALS_SECRET = "identity-service.credentials.secret";
private static final String CREDENTIALS_PROVIDER = "identity-service.credentials.provider";
@@ -95,13 +98,13 @@ public class IdentityServiceConfig extends AdapterConfig implements Initializing
@Override
public void afterPropertiesSet() throws Exception
{
// programatically build the more complex objects i.e. credentials
// programmatically build the more complex objects i.e. credentials
Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
String secret = this.globalProperties.getProperty(CREDENTIALS_SECRET);
if (secret != null && !secret.isEmpty())
{
credentials.put("secret", secret);
credentials.put(SECRET, secret);
}
String provider = this.globalProperties.getProperty(CREDENTIALS_PROVIDER);
@@ -116,10 +119,27 @@ public class IdentityServiceConfig extends AdapterConfig implements Initializing
{
this.setCredentials(credentials);
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("Created credentials map from config: " + credentials);
LOGGER.debug("Created credentials map from config: " + credentials);
}
}
}
String getIssuerUrl()
{
return UriComponentsBuilder.fromUriString(getAuthServerUrl())
.pathSegment(REALMS, getRealm())
.build()
.toString();
}
public String getClientSecret()
{
return Optional.ofNullable(getCredentials())
.map(c -> c.get(SECRET))
.filter(String.class::isInstance)
.map(String.class::cast)
.orElse("");
}
}

View File

@@ -1,105 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.HttpClient;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.springframework.beans.factory.FactoryBean;
import java.util.concurrent.TimeUnit;
/**
* Creates an instance of a KeycloakDeployment object for communicating with the Identity Service.
*
* @author Gavin Cornwell
*/
public class IdentityServiceDeploymentFactoryBean implements FactoryBean<KeycloakDeployment>
{
private static Log logger = LogFactory.getLog(IdentityServiceDeploymentFactoryBean.class);
private IdentityServiceConfig identityServiceConfig;
public void setIdentityServiceConfig(IdentityServiceConfig config)
{
this.identityServiceConfig = config;
}
@Override
public KeycloakDeployment getObject() throws Exception
{
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(this.identityServiceConfig);
// Set client with custom timeout values if client was created by the KeycloakDeploymentBuilder.
// This can be removed if the future versions of Keycloak accept timeout values through the config.
if (deployment.getClient() != null)
{
int connectionTimeout = identityServiceConfig.getClientConnectionTimeout();
int socketTimeout = identityServiceConfig.getClientSocketTimeout();
HttpClient client = new HttpClientBuilder()
.establishConnectionTimeout(connectionTimeout, TimeUnit.MILLISECONDS)
.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
.build(this.identityServiceConfig);
deployment.setClient(client);
if (logger.isDebugEnabled())
{
logger.debug("Created HttpClient for Keycloak deployment with connection timeout: "+ connectionTimeout + " ms, socket timeout: "+ socketTimeout+" ms.");
}
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("HttpClient for Keycloak deployment was not set.");
}
}
if (logger.isInfoEnabled())
{
logger.info("Keycloak JWKS URL: " + deployment.getJwksUrl());
logger.info("Keycloak Realm: " + deployment.getRealm());
logger.info("Keycloak Client ID: " + deployment.getResourceName());
}
return deployment;
}
@Override
public Class<KeycloakDeployment> getObjectType()
{
return KeycloakDeployment.class;
}
@Override
public boolean isSingleton()
{
return true;
}
}

View File

@@ -0,0 +1,90 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.util.Optional;
/**
* Allows to interact with the Identity Service
*/
interface IdentityServiceFacade
{
/**
* Verifies provided user credentials. The OAuth2's Client role is only used to verify the user credentials (Resource Owner Password
* Credentials Flow) this is why there is an explicit method for verifying these.
*
* @param username user's name
* @param password user's password
* @throws CredentialsVerificationException when the verification failed or couldn't be performed
*/
void verifyCredentials(String username, String password);
/**
* Extracts username from provided token
*
* @param token token representation
* @return possible username
*/
Optional<String> extractUsernameFromToken(String token);
class IdentityServiceFacadeException extends RuntimeException
{
IdentityServiceFacadeException(String message)
{
super(message);
}
IdentityServiceFacadeException(String message, Throwable cause)
{
super(message, cause);
}
}
class CredentialsVerificationException extends IdentityServiceFacadeException
{
CredentialsVerificationException(String message)
{
super(message);
}
CredentialsVerificationException(String message, Throwable cause)
{
super(message, cause);
}
}
class TokenException extends IdentityServiceFacadeException
{
TokenException(String message)
{
super(message);
}
TokenException(String message, Throwable cause)
{
super(message, cause);
}
}
}

View File

@@ -0,0 +1,388 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder.PasswordGrantBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.web.client.RestTemplate;
/**
*
* Creates an instance of {@link IdentityServiceFacade}. <br>
* This factory can return a null if it is disabled.
*
*/
public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentityServiceFacade>
{
private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
private boolean enabled;
private SpringBasedIdentityServiceFacadeFactory factory;
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
{
factory = new SpringBasedIdentityServiceFacadeFactory(identityServiceConfig);
}
@Override
public IdentityServiceFacade getObject() throws Exception
{
// The creation of the client can be disabled for testing or when the username/password authentication is not required,
// for instance when Keycloak is configured for 'bearer only' authentication or Direct Access Grants are disabled.
if (!enabled)
{
return null;
}
return new LazyInstantiatingIdentityServiceFacade(factory::createIdentityServiceFacade);
}
@Override
public Class<?> getObjectType()
{
return IdentityServiceFacade.class;
}
@Override
public boolean isSingleton()
{
return true;
}
private static IdentityServiceFacadeException authorizationServerCantBeUsedException(RuntimeException cause)
{
return new IdentityServiceFacadeException("Unable to use the Authorization Server.", cause);
}
// The target facade is created lazily to improve resiliency on Identity Service
// (Keycloak/Authorization Server) failures when Spring Context is starting up.
static class LazyInstantiatingIdentityServiceFacade implements IdentityServiceFacade
{
private final AtomicReference<IdentityServiceFacade> targetFacade = new AtomicReference<>();
private final Supplier<IdentityServiceFacade> targetFacadeCreator;
LazyInstantiatingIdentityServiceFacade(Supplier<IdentityServiceFacade> targetFacadeCreator)
{
this.targetFacadeCreator = requireNonNull(targetFacadeCreator);
}
@Override
public void verifyCredentials(String username, String password)
{
getTargetFacade().verifyCredentials(username, password);
}
@Override
public Optional<String> extractUsernameFromToken(String token)
{
return getTargetFacade().extractUsernameFromToken(token);
}
private IdentityServiceFacade getTargetFacade()
{
return ofNullable(targetFacade.get())
.orElseGet(() -> targetFacade.updateAndGet(prev ->
ofNullable(prev).orElseGet(this::createTargetFacade)));
}
private IdentityServiceFacade createTargetFacade()
{
try
{
return targetFacadeCreator.get();
}
catch (IdentityServiceFacadeException e)
{
throw e;
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to instantiate IdentityServiceFacade.", e);
throw authorizationServerCantBeUsedException(e);
}
}
}
private static class SpringBasedIdentityServiceFacadeFactory
{
private static final long CLOCK_SKEW_MS = 0;
private final IdentityServiceConfig config;
SpringBasedIdentityServiceFacadeFactory(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
}
private IdentityServiceFacade createIdentityServiceFacade()
{
//Here we preserve the behaviour of previously used Keycloak Adapter
// * Client is authenticating itself using basic auth
// * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
// * There is no caching of authenticated clients (NoStoredAuthorizedClient)
// * There is only one Authorization Server/Client pair (SingleClientRegistration)
final RestTemplate restTemplate = createRestTemplate();
final ClientRegistration clientRegistration = createClientRegistration(restTemplate);
final OAuth2AuthorizedClientManager clientManager = createAuthorizedClientManager(restTemplate, clientRegistration);
final JwtDecoder jwtDecoder = createJwtDecoder(clientRegistration);
return new SpringBasedIdentityServiceFacade(clientManager, jwtDecoder);
}
private RestTemplate createRestTemplate()
{
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(config.getClientConnectionTimeout());
requestFactory.setReadTimeout(config.getClientSocketTimeout());
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
return restTemplate;
}
private ClientRegistration createClientRegistration(RestTemplate restTemplate)
{
try
{
return ClientRegistrations
.fromIssuerLocation(config.getIssuerUrl())
.clientId(config.getResource())
.clientSecret(config.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.registrationId(SpringBasedIdentityServiceFacade.CLIENT_REGISTRATION_ID)
.build();
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to create ClientRegistration.", e);
throw authorizationServerCantBeUsedException(e);
}
}
private OAuth2AuthorizedClientManager createAuthorizedClientManager(RestTemplate restTemplate, ClientRegistration clientRegistration)
{
final AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
new SingleClientRegistration(clientRegistration),
new NoStoredAuthorizedClient());
final Consumer<PasswordGrantBuilder> passwordGrantConfigurer = b -> {
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
client.setRestOperations(restTemplate);
b.accessTokenResponseClient(client);
b.clockSkew(Duration.of(CLOCK_SKEW_MS, ChronoUnit.MILLIS));
};
manager.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder()
.password(passwordGrantConfigurer)
.build());
manager.setContextAttributesMapper(OAuth2AuthorizeRequest::getAttributes);
return manager;
}
private JwtDecoder createJwtDecoder(ClientRegistration clientRegistration)
{
final OidcIdTokenDecoderFactory decoderFactory = new OidcIdTokenDecoderFactory();
decoderFactory.setJwtValidatorFactory(c -> new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.of(CLOCK_SKEW_MS, ChronoUnit.MILLIS)),
new JwtIssuerValidator(c.getProviderDetails().getIssuerUri()),
new JwtClaimValidator<String>("typ", "Bearer"::equals),
new JwtClaimValidator<String>(JwtClaimNames.SUB, Objects::nonNull)
));
try
{
return decoderFactory.createDecoder(clientRegistration);
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to create JwtDecoder.", e);
throw authorizationServerCantBeUsedException(e);
}
}
private static class NoStoredAuthorizedClient implements OAuth2AuthorizedClientService
{
@Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName)
{
return null;
}
@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal)
{
//do nothing
}
@Override
public void removeAuthorizedClient(String clientRegistrationId, String principalName)
{
//do nothing
}
}
private static class SingleClientRegistration implements ClientRegistrationRepository
{
private final ClientRegistration clientRegistration;
private SingleClientRegistration(ClientRegistration clientRegistration)
{
this.clientRegistration = requireNonNull(clientRegistration);
}
@Override
public ClientRegistration findByRegistrationId(String registrationId)
{
return Objects.equals(registrationId, clientRegistration.getRegistrationId()) ? clientRegistration : null;
}
}
}
static class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
{
static final String CLIENT_REGISTRATION_ID = "ids";
private final OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
private JwtDecoder jwtDecoder;
SpringBasedIdentityServiceFacade(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, JwtDecoder jwtDecoder)
{
this.oAuth2AuthorizedClientManager = requireNonNull(oAuth2AuthorizedClientManager);
this.jwtDecoder = requireNonNull(jwtDecoder);
}
@Override
public void verifyCredentials(String username, String password)
{
final OAuth2AuthorizedClient authorizedClient;
try
{
final OAuth2AuthorizeRequest authRequest = createPasswordCredentialsRequest(username, password);
authorizedClient = oAuth2AuthorizedClientManager.authorize(authRequest);
}
catch (OAuth2AuthorizationException e)
{
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
throw new CredentialsVerificationException("Authorization against the Authorization Server failed with " + e.getError() + ".", e);
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
throw new CredentialsVerificationException("Failed to authorize against Authorization Server.", e);
}
if (authorizedClient == null || authorizedClient.getAccessToken() == null)
{
throw new CredentialsVerificationException("Resource Owner Password Credentials is not supported by the Authorization Server.");
}
}
@Override
public Optional<String> extractUsernameFromToken(String token)
{
final Jwt validToken;
try
{
validToken = jwtDecoder.decode(requireNonNull(token));
}
catch (RuntimeException e)
{
throw new TokenException("Failed to decode token. " + e.getMessage(), e);
}
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Bearer token outcome: " + validToken);
}
return Optional.ofNullable(validToken)
.map(Jwt::getClaims)
.map(c -> c.get("preferred_username"))
.filter(String.class::isInstance)
.map(String.class::cast);
}
private OAuth2AuthorizeRequest createPasswordCredentialsRequest(String userName, String password)
{
return OAuth2AuthorizeRequest
.withClientRegistrationId(CLIENT_REGISTRATION_ID)
.principal(userName)
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, userName)
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password)
.build();
}
}
}

View File

@@ -1,107 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.adapters.servlet.ServletHttpFacade;
/**
* HttpFacade wrapper so we can re-use Keycloak authenticator classes.
*
* @author Gavin Cornwell
*/
public class IdentityServiceHttpFacade extends ServletHttpFacade
{
public IdentityServiceHttpFacade(HttpServletRequest request)
{
super(request, null);
}
@Override
public Response getResponse()
{
// return our dummy NoOp implementation so we don't effect the ACS response
return new NoOpResponseFacade();
}
/**
* NoOp implementation of Keycloak Response interface.
*/
private class NoOpResponseFacade implements Response
{
@Override
public void setStatus(int status)
{
}
@Override
public void addHeader(String name, String value)
{
}
@Override
public void setHeader(String name, String value)
{
}
@Override
public void resetCookie(String name, String path)
{
}
@Override
public void setCookie(String name, String value, String path, String domain, int maxAge,
boolean secure, boolean httpOnly)
{
}
@Override
public OutputStream getOutputStream()
{
return new ByteArrayOutputStream();
}
@Override
public void sendError(int code)
{
}
@Override
public void sendError(int code, String message)
{
}
@Override
public void end()
{
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -27,17 +27,19 @@ package org.alfresco.repo.security.authentication.identityservice;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
import org.alfresco.service.cmr.security.PersonService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.representations.AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
/**
* A {@link RemoteUserMapper} implementation that detects and validates JWTs
@@ -47,7 +49,7 @@ import org.keycloak.representations.AccessToken;
*/
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
{
private static Log logger = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
/** Is the mapper enabled */
private boolean isEnabled;
@@ -57,9 +59,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
/** The person service. */
private PersonService personService;
/** The Keycloak deployment object */
private KeycloakDeployment keycloakDeployment;
private BearerTokenResolver bearerTokenResolver;
private IdentityServiceFacade identityServiceFacade;
/**
* Sets the active flag
@@ -91,58 +93,57 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
{
this.personService = personService;
}
public void setIdentityServiceDeployment(KeycloakDeployment deployment)
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
{
this.keycloakDeployment = deployment;
this.bearerTokenResolver = bearerTokenResolver;
}
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
{
this.identityServiceFacade = identityServiceFacade;
}
/*
* (non-Javadoc)
* @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(javax.servlet.http.HttpServletRequest)
*/
@Override
public String getRemoteUser(HttpServletRequest request)
{
LOGGER.trace("Retrieving username from http request...");
if (!this.isEnabled)
{
LOGGER.debug("IdentityServiceRemoteUserMapper is disabled, returning null.");
return null;
}
try
{
if (logger.isTraceEnabled())
{
logger.trace("Retrieving username from http request...");
}
if (!this.isEnabled)
{
if (logger.isDebugEnabled())
{
logger.debug("IdentityServiceRemoteUserMapper is disabled, returning null.");
}
return null;
}
String headerUserId = extractUserFromHeader(request);
if (headerUserId != null)
{
// Normalize the user ID taking into account case sensitivity settings
String normalizedUserId = normalizeUserId(headerUserId);
if (logger.isTraceEnabled())
{
logger.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
}
LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
return normalizedUserId;
}
}
catch (Exception e)
catch (TokenException e)
{
logger.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
if (!isValidationFailureSilent)
{
throw new AuthenticationException("Failed to extract username from token: " + e.getMessage(), e);
}
LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
}
if (logger.isTraceEnabled())
catch (RuntimeException e)
{
logger.trace("Could not identify a userId. Returning null.");
LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
}
LOGGER.trace("Could not identify a userId. Returning null.");
return null;
}
@@ -163,57 +164,32 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
*/
private String extractUserFromHeader(HttpServletRequest request)
{
String userName = null;
IdentityServiceHttpFacade facade = new IdentityServiceHttpFacade(request);
// try authenticating with bearer token first
if (logger.isDebugEnabled())
LOGGER.debug("Trying bearer token...");
final String bearerToken;
try
{
logger.debug("Trying bearer token...");
bearerToken = bearerTokenResolver.resolve(request);
}
AlfrescoBearerTokenRequestAuthenticator tokenAuthenticator =
new AlfrescoBearerTokenRequestAuthenticator(this.keycloakDeployment);
AuthOutcome tokenOutcome = tokenAuthenticator.authenticate(facade);
if (logger.isDebugEnabled())
catch (OAuth2AuthenticationException e)
{
logger.debug("Bearer token outcome: " + tokenOutcome);
LOGGER.debug("Failed to resolve Bearer token.", e);
return null;
}
if (tokenOutcome == AuthOutcome.FAILED && !isValidationFailureSilent)
final Optional<String> possibleUsername = Optional.ofNullable(bearerToken)
.flatMap(identityServiceFacade::extractUsernameFromToken);
if (possibleUsername.isEmpty())
{
throw new AuthenticationException("Token validation failed: " +
tokenAuthenticator.getValidationFailureDescription());
LOGGER.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
return null;
}
if (tokenOutcome == AuthOutcome.AUTHENTICATED)
{
userName = extractUserFromToken(tokenAuthenticator.getToken());
}
else
{
if (logger.isDebugEnabled())
{
logger.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
}
}
return userName;
}
private String extractUserFromToken(AccessToken jwt)
{
// retrieve the preferred_username claim
String userName = jwt.getPreferredUsername();
if (logger.isTraceEnabled())
{
logger.trace("Extracted username: " + AuthenticationUtil.maskUsername(userName));
}
return userName;
String username = possibleUsername.get();
LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(username));
return username;
}
/**
@@ -238,9 +214,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
}
}, AuthenticationUtil.getSystemUserName());
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized));
LOGGER.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized));
}
return normalized == null ? userId : normalized;

View File

@@ -42,6 +42,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.alfresco.model.ContentModel;
@@ -914,51 +915,23 @@ public class TaggingServiceImpl implements TaggingService,
return new EmptyPagingResults<Pair<NodeRef, String>>();
}
public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest)
{
return getTags(storeRef, pagingRequest, null, null);
}
/**
* @see org.alfresco.service.cmr.tagging.TaggingService#getTags(org.alfresco.service.cmr.repository.StoreRef, org.alfresco.query.PagingRequest)
*/
public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest)
public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest, Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
{
ParameterCheck.mandatory("storeRef", storeRef);
PagingResults<ChildAssociationRef> rootCategories = this.categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, pagingRequest, true);
final List<Pair<NodeRef, String>> result = new ArrayList<Pair<NodeRef, String>>(rootCategories.getPage().size());
for (ChildAssociationRef rootCategory : rootCategories.getPage())
{
String name = (String)this.nodeService.getProperty(rootCategory.getChildRef(), ContentModel.PROP_NAME);
result.add(new Pair<NodeRef, String>(rootCategory.getChildRef(), name));
}
final boolean hasMoreItems = rootCategories.hasMoreItems();
final Pair<Integer, Integer> totalResultCount = rootCategories.getTotalResultCount();
final String queryExecutionId = rootCategories.getQueryExecutionId();
rootCategories = null;
PagingResults<ChildAssociationRef> rootCategories = categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, pagingRequest, true,
exactNamesFilter, alikeNamesFilter);
return new PagingResults<Pair<NodeRef, String>>()
{
@Override
public List<Pair<NodeRef, String>> getPage()
{
return result;
}
@Override
public boolean hasMoreItems()
{
return hasMoreItems;
}
@Override
public Pair<Integer, Integer> getTotalResultCount()
{
return totalResultCount;
}
@Override
public String getQueryExecutionId()
{
return queryExecutionId;
}
};
return mapPagingResult(rootCategories,
(childAssociation) -> new Pair<>(childAssociation.getChildRef(), childAssociation.getQName().getLocalName()));
}
/**
@@ -1600,4 +1573,36 @@ public class TaggingServiceImpl implements TaggingService,
createTagBehaviour.enable();
}
}
private <T, R> PagingResults<R> mapPagingResult(final PagingResults<T> pagingResults, final Function<T, R> mapper)
{
return new PagingResults<R>()
{
@Override
public List<R> getPage()
{
return pagingResults.getPage().stream()
.map(mapper)
.collect(Collectors.toList());
}
@Override
public boolean hasMoreItems()
{
return pagingResults.hasMoreItems();
}
@Override
public Pair<Integer, Integer> getTotalResultCount()
{
return pagingResults.getTotalResultCount();
}
@Override
public String getQueryExecutionId()
{
return pagingResults.getQueryExecutionId();
}
};
}
}

View File

@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Optional;
import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.service.Auditable;
@@ -136,6 +137,24 @@ public interface CategoryService
@Auditable(parameters = {"storeRef", "aspectName", "pagingRequest", "sortByName", "filter"})
PagingResults<ChildAssociationRef> getRootCategories(StoreRef storeRef, QName aspectName, PagingRequest pagingRequest, boolean sortByName, String filter);
/**
* Get a paged list of the root categories for an aspect/classification supporting multiple name filters.
*
* @param storeRef
* @param aspectName
* @param pagingRequest
* @param sortByName
* @param exactNamesFilter
* @param alikeNamesFilter
* @return
*/
@Auditable(parameters = {"storeRef", "aspectName", "pagingRequest", "sortByName", "exactNamesFilter", "alikeNamesFilter"})
default PagingResults<ChildAssociationRef> getRootCategories(StoreRef storeRef, QName aspectName, PagingRequest pagingRequest, boolean sortByName,
Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
{
return new EmptyPagingResults<>();
}
/**
* Get the root categories for an aspect/classification with names that start with filter
*

View File

@@ -25,10 +25,12 @@
*/
package org.alfresco.service.cmr.tagging;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.service.Auditable;
@@ -75,6 +77,21 @@ public interface TaggingService
*/
@NotAuditable
PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest);
/**
* Get a paged list of tags filtered by name
*
* @param storeRef StoreRef
* @param pagingRequest PagingRequest
* @param exactNamesFilter PagingRequest
* @param alikeNamesFilter PagingRequest
* @return PagingResults
*/
@NotAuditable
default PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest, Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
{
return new EmptyPagingResults<>();
}
/**
* Get all the tags currently available that match the provided filter.

View File

@@ -1159,7 +1159,7 @@
on z.parent_node_id = #{parentNode.id}
and z.child_node_id = a.parent_node_id
where c.child_node_id = a.child_node_id
);
)
</select>
<select id="select_ChildAssocsByPropertyValue" parameterType="ChildProperty" resultMap="result_ChildAssoc">

View File

@@ -1149,7 +1149,7 @@ smart.folders.config.type.templates.path=${spaces.dictionary.childname}/${spaces
smart.folders.config.type.templates.qname.filter=none
# Preferred password encoding, md4, sha256, bcrypt10
system.preferred.password.encoding=md4
system.preferred.password.encoding=bcrypt10
# Upgrade Password Hash Job
system.upgradePasswordHash.jobBatchSize=100

View File

@@ -21,12 +21,12 @@
<property name="allowGuestLogin">
<value>${identity-service.authentication.allowGuestLogin}</value>
</property>
<property name="authenticatorAuthzClient">
<ref bean="authenticatorAuthzClient"/>
<property name="identityServiceFacade">
<ref bean="identityServiceFacade"/>
</property>
</bean>
<bean name="authenticatorAuthzClient" class="org.alfresco.repo.security.authentication.identityservice.AuthenticatorAuthzClientFactoryBean">
<bean name="identityServiceFacade" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean">
<property name="identityServiceConfig">
<ref bean="identityServiceConfig" />
</property>
@@ -204,12 +204,6 @@
<value>${identity-service.client-socket-timeout:2000}</value>
</property>
</bean>
<bean name="identityServiceDeployment" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceDeploymentFactoryBean">
<property name="identityServiceConfig">
<ref bean="identityServiceConfig" />
</property>
</bean>
<!-- Enable control over mapping between request and user ID -->
<bean id="remoteUserMapper" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper">
@@ -222,8 +216,11 @@
<property name="personService">
<ref bean="PersonService" />
</property>
<property name="identityServiceDeployment">
<ref bean="identityServiceDeployment" />
<property name="bearerTokenResolver">
<bean class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver" />
</property>
<property name="identityServiceFacade">
<ref bean="identityServiceFacade" />
</property>
</bean>

View File

@@ -25,6 +25,8 @@
*/
package org.alfresco;
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
import org.alfresco.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
import org.junit.experimental.categories.Categories;
@@ -136,6 +138,8 @@ import org.junit.runners.Suite;
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
SpringBasedIdentityServiceFacadeUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,

View File

@@ -26,15 +26,12 @@
package org.alfresco.repo.security.authentication;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
@@ -48,7 +45,6 @@ import net.sf.acegisecurity.DisabledException;
import net.sf.acegisecurity.LockedException;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.admin.SysAdminParamsImpl;
@@ -519,50 +515,48 @@ public class AuthenticationTest extends TestCase
assertTrue("The user should exist", dao.userExists(userName));
}
public void testCreateAndyUserAndOtherCRUD() throws NoSuchAlgorithmException, UnsupportedEncodingException
public void testCreateAndyUserAndUpdatePassword()
{
RepositoryAuthenticationDao dao = createRepositoryAuthenticationDao();
dao.createUser("Andy", "cabbage".toCharArray());
assertNotNull(dao.getUserOrNull("Andy"));
UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(AndyDetails);
assertEquals("Andy", AndyDetails.getUsername());
// assertNotNull(dao.getSalt(AndyDetails));
assertTrue(AndyDetails.isAccountNonExpired());
assertTrue(AndyDetails.isAccountNonLocked());
assertTrue(AndyDetails.isCredentialsNonExpired());
assertTrue(AndyDetails.isEnabled());
assertNotSame("cabbage", AndyDetails.getPassword());
assertTrue(compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(),"cabbage", AndyDetails.getPassword(), null));
assertEquals(1, AndyDetails.getAuthorities().length);
RepositoryAuthenticatedUser andyDetails = (RepositoryAuthenticatedUser) dao.loadUserByUsername("Andy");
assertNotNull("User unexpectedly null", andyDetails);
assertEquals("Unexpected username", "Andy", andyDetails.getUsername());
Object originalSalt = andyDetails.getSalt();
assertNotNull("Salt was not generated", originalSalt);
assertTrue("Account unexpectedly expired", andyDetails.isAccountNonExpired());
assertTrue("Account unexpectedly locked", andyDetails.isAccountNonLocked());
assertTrue("Credentials unexpectedly expired", andyDetails.isCredentialsNonExpired());
assertTrue("User unexpectedly disabled", andyDetails.isEnabled());
assertNotSame("Password was not hashed", "cabbage", andyDetails.getPassword());
assertTrue("Failed to recalculate same password hash", compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(),"cabbage", andyDetails.getPassword(), originalSalt));
assertEquals("User does not have a single authority", 1, andyDetails.getAuthorities().length);
// Object oldSalt = dao.getSalt(AndyDetails);
dao.updateUser("Andy", "carrot".toCharArray());
UserDetails newDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(newDetails);
assertEquals("Andy", newDetails.getUsername());
// assertNotNull(dao.getSalt(newDetails));
assertTrue(newDetails.isAccountNonExpired());
assertTrue(newDetails.isAccountNonLocked());
assertTrue(newDetails.isCredentialsNonExpired());
assertTrue(newDetails.isEnabled());
assertNotSame("carrot", newDetails.getPassword());
assertEquals(1, newDetails.getAuthorities().length);
RepositoryAuthenticatedUser newDetails = (RepositoryAuthenticatedUser) dao.loadUserByUsername("Andy");
assertNotNull("New details were null", newDetails);
assertEquals("New details contain wrong username", "Andy", newDetails.getUsername());
Object updatedSalt = newDetails.getSalt();
assertNotNull("New details contain null salt", updatedSalt);
assertTrue("Updated account is expired", newDetails.isAccountNonExpired());
assertTrue("Updated account is locked", newDetails.isAccountNonLocked());
assertTrue("Updated account has expired credentials", newDetails.isCredentialsNonExpired());
assertTrue("Updated account is not enabled", newDetails.isEnabled());
assertNotSame("Updated account contains unhashed password", "carrot", newDetails.getPassword());
assertEquals("Updated account should have a single authority", 1, newDetails.getAuthorities().length);
assertTrue("Failed to validate updated password hash", compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(),"carrot", newDetails.getPassword(), updatedSalt));
assertNotSame("Expected salt to be replaced when password was updated", originalSalt, updatedSalt);
assertNotSame(AndyDetails.getPassword(), newDetails.getPassword());
RepositoryAuthenticatedUser rau = (RepositoryAuthenticatedUser) newDetails;
assertTrue(compositePasswordEncoder.matchesPassword("carrot", newDetails.getPassword(), null, rau.getHashIndicator()));
// assertNotSame(oldSalt, dao.getSalt(newDetails));
//Update again
dao.updateUser("Andy", "potato".toCharArray());
newDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(newDetails);
assertEquals("Andy", newDetails.getUsername());
rau = (RepositoryAuthenticatedUser) newDetails;
assertTrue(compositePasswordEncoder.matchesPassword("potato", newDetails.getPassword(), null, rau.getHashIndicator()));
// Update back to first password again.
dao.updateUser("Andy", "cabbage".toCharArray());
RepositoryAuthenticatedUser thirdDetails = (RepositoryAuthenticatedUser) dao.loadUserByUsername("Andy");
Object thirdSalt = thirdDetails.getSalt();
assertNotSame("New salt should not match original salt", thirdSalt, originalSalt);
assertNotSame("New salt should not match previous salt", thirdSalt, updatedSalt);
assertTrue("New password hash was not reproducible", compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(), "cabbage", thirdDetails.getPassword(), thirdSalt));
dao.deleteUser("Andy");
assertFalse("Should not be a cache entry for 'Andy'.", authenticationCache.contains("Andy"));
@@ -1989,131 +1983,142 @@ public class AuthenticationTest extends TestCase
* Tests the scenario where a user logs in after the system has been upgraded.
* Their password should get re-hashed using the preferred encoding.
*/
public void testRehashedPasswordOnAuthentication() throws Exception
public void testRehashedPasswordOnAuthentication()
{
// create the Andy authentication
assertNull(authenticationComponent.getCurrentAuthentication());
authenticationComponent.setSystemUserAsCurrentUser();
pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray());
// find the node representing the Andy user and it's properties
NodeRef andyUserNodeRef = getRepositoryAuthenticationDao(). getUserOrNull("Andy");
assertNotNull(andyUserNodeRef);
// ensure the properties are in the state we're expecting
Map<QName, Serializable> userProps = nodeService.getProperties(andyUserNodeRef);
String passwordProp = (String)userProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
String password2Prop = (String)userProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
String passwordHashProp = (String)userProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
List<String> hashIndicatorProp = (List<String>)userProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
// re-generate an md4 hashed password
MD4PasswordEncoderImpl md4PasswordEncoder = new MD4PasswordEncoderImpl();
String md4Password = md4PasswordEncoder.encodePassword("auth1", null);
// re-generate a sha256 hashed password
String salt = (String)userProps.get(ContentModel.PROP_SALT);
ShaPasswordEncoderImpl sha256PasswordEncoder = new ShaPasswordEncoderImpl(256);
String sha256Password = sha256PasswordEncoder.encodePassword("auth1", salt);
// change the underlying user object to represent state in previous release
userProps.put(ContentModel.PROP_PASSWORD, md4Password);
userProps.put(ContentModel.PROP_PASSWORD_SHA256, sha256Password);
userProps.remove(ContentModel.PROP_PASSWORD_HASH);
userProps.remove(ContentModel.PROP_HASH_INDICATOR);
nodeService.setProperties(andyUserNodeRef, userProps);
// make sure the changes took effect
Map<QName, Serializable> updatedProps = nodeService.getProperties(andyUserNodeRef);
String usernameProp = (String)updatedProps.get(ContentModel.PROP_USER_USERNAME);
assertEquals("Expected the username property to be 'Andy'", "Andy", usernameProp);
passwordProp = (String)updatedProps.get(ContentModel.PROP_PASSWORD);
assertNotNull("Expected the password property to be populated", passwordProp);
password2Prop = (String)updatedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNotNull("Expected the password2 property to be populated", password2Prop);
passwordHashProp = (String)updatedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNull("Expected the passwordHash property to be null", passwordHashProp);
hashIndicatorProp = (List<String>)updatedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNull("Expected the hashIndicator property to be null", hashIndicatorProp);
// authenticate the user
authenticationComponent.clearCurrentSecurityContext();
pubAuthenticationService.authenticate("Andy", "auth1".toCharArray());
assertEquals("Andy", authenticationService.getCurrentUserName());
// commit the transaction to invoke the password hashing of the user
userTransaction.commit();
// start another transaction and change to system user
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
authenticationComponent.setSystemUserAsCurrentUser();
// verify that the new properties are populated and the old ones are cleaned up
Map<QName, Serializable> upgradedProps = nodeService.getProperties(andyUserNodeRef);
passwordProp = (String)upgradedProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
password2Prop = (String)upgradedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
passwordHashProp = (String)upgradedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
hashIndicatorProp = (List<String>)upgradedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
assertTrue("Expected there to be a single hash indicator entry", (hashIndicatorProp.size() == 1));
String preferredEncoding = compositePasswordEncoder.getPreferredEncoding();
String hashEncoding = (String)hashIndicatorProp.get(0);
assertEquals("Expected hash indicator to be '" + preferredEncoding + "' but it was: " + hashEncoding,
// This test requires upgrading from md4 to sha256 hashing.
String defaultPreferredEncoding = compositePasswordEncoder.getPreferredEncoding();
compositePasswordEncoder.setPreferredEncoding("md4");
try
{
// create the Andy authentication
assertNull(authenticationComponent.getCurrentAuthentication());
authenticationComponent.setSystemUserAsCurrentUser();
pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray());
// find the node representing the Andy user and its properties
NodeRef andyUserNodeRef = getRepositoryAuthenticationDao().getUserOrNull("Andy");
assertNotNull(andyUserNodeRef);
// ensure the properties are in the state we're expecting
Map<QName, Serializable> userProps = nodeService.getProperties(andyUserNodeRef);
String passwordProp = (String) userProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
String password2Prop = (String) userProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
String passwordHashProp = (String) userProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
List<String> hashIndicatorProp = (List<String>) userProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
// re-generate an md4 hashed password
MD4PasswordEncoderImpl md4PasswordEncoder = new MD4PasswordEncoderImpl();
String md4Password = md4PasswordEncoder.encodePassword("auth1", null);
// re-generate a sha256 hashed password
String salt = (String) userProps.get(ContentModel.PROP_SALT);
ShaPasswordEncoderImpl sha256PasswordEncoder = new ShaPasswordEncoderImpl(256);
String sha256Password = sha256PasswordEncoder.encodePassword("auth1", salt);
// change the underlying user object to represent state in previous release
userProps.put(ContentModel.PROP_PASSWORD, md4Password);
userProps.put(ContentModel.PROP_PASSWORD_SHA256, sha256Password);
userProps.remove(ContentModel.PROP_PASSWORD_HASH);
userProps.remove(ContentModel.PROP_HASH_INDICATOR);
nodeService.setProperties(andyUserNodeRef, userProps);
// make sure the changes took effect
Map<QName, Serializable> updatedProps = nodeService.getProperties(andyUserNodeRef);
String usernameProp = (String) updatedProps.get(ContentModel.PROP_USER_USERNAME);
assertEquals("Expected the username property to be 'Andy'", "Andy", usernameProp);
passwordProp = (String) updatedProps.get(ContentModel.PROP_PASSWORD);
assertNotNull("Expected the password property to be populated", passwordProp);
password2Prop = (String) updatedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNotNull("Expected the password2 property to be populated", password2Prop);
passwordHashProp = (String) updatedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNull("Expected the passwordHash property to be null", passwordHashProp);
hashIndicatorProp = (List<String>) updatedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNull("Expected the hashIndicator property to be null", hashIndicatorProp);
// authenticate the user
authenticationComponent.clearCurrentSecurityContext();
pubAuthenticationService.authenticate("Andy", "auth1".toCharArray());
assertEquals("Andy", authenticationService.getCurrentUserName());
// commit the transaction to invoke the password hashing of the user
userTransaction.commit();
// start another transaction and change to system user
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
authenticationComponent.setSystemUserAsCurrentUser();
// verify that the new properties are populated and the old ones are cleaned up
Map<QName, Serializable> upgradedProps = nodeService.getProperties(andyUserNodeRef);
passwordProp = (String) upgradedProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
password2Prop = (String) upgradedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
passwordHashProp = (String) upgradedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
hashIndicatorProp = (List<String>) upgradedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
assertTrue("Expected there to be a single hash indicator entry", (hashIndicatorProp.size() == 1));
String preferredEncoding = compositePasswordEncoder.getPreferredEncoding();
String hashEncoding = hashIndicatorProp.get(0);
assertEquals("Expected hash indicator to be '" + preferredEncoding + "' but it was: " + hashEncoding,
preferredEncoding, hashEncoding);
// delete the user and clear the security context
this.deleteAndy();
authenticationComponent.clearCurrentSecurityContext();
// delete the user and clear the security context
this.deleteAndy();
authenticationComponent.clearCurrentSecurityContext();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
compositePasswordEncoder.setPreferredEncoding(defaultPreferredEncoding);
}
}
/**
* For on premise the default is MD4, for cloud BCRYPT10
*
* @throws Exception
* Test password encoding with MD4 without a salt.
*/
public void testDefaultEncodingIsMD4() throws Exception
public void testGetsMD4Password()
{
assertNotNull(compositePasswordEncoder);
assertEquals("md4", compositePasswordEncoder.getPreferredEncoding());
}
String defaultPreferredEncoding = compositePasswordEncoder.getPreferredEncoding();
compositePasswordEncoder.setPreferredEncoding("md4");
/**
* For on premise the default is MD4, get it
*
* @throws Exception
*/
public void testGetsMD4Password() throws Exception
{
String user = "mduzer";
String rawPass = "roarPazzw0rd";
assertEquals("md4", compositePasswordEncoder.getPreferredEncoding());
dao.createUser(user, null, rawPass.toCharArray());
NodeRef userNodeRef = getRepositoryAuthenticationDao().getUserOrNull(user);
assertNotNull(userNodeRef);
String pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertTrue(compositePasswordEncoder.matches("md4", rawPass, pass, null));
try
{
String user = "mduzer";
String rawPass = "roarPazzw0rd";
dao.createUser(user, null, rawPass.toCharArray());
NodeRef userNodeRef = getRepositoryAuthenticationDao().getUserOrNull(user);
assertNotNull(userNodeRef);
String pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertTrue(compositePasswordEncoder.matches("md4", rawPass, pass, null));
Map<QName, Serializable> properties = nodeService.getProperties(userNodeRef);
properties.remove(ContentModel.PROP_PASSWORD_HASH);
properties.remove(ContentModel.PROP_HASH_INDICATOR);
properties.remove(ContentModel.PROP_PASSWORD);
properties.remove(ContentModel.PROP_PASSWORD_SHA256);
String encoded = compositePasswordEncoder.encode("md4",new String(rawPass), null);
properties.put(ContentModel.PROP_PASSWORD, encoded);
nodeService.setProperties(userNodeRef, properties);
pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertEquals(encoded, pass);
dao.deleteUser(user);
Map<QName, Serializable> properties = nodeService.getProperties(userNodeRef);
properties.remove(ContentModel.PROP_PASSWORD_HASH);
properties.remove(ContentModel.PROP_HASH_INDICATOR);
properties.remove(ContentModel.PROP_PASSWORD);
properties.remove(ContentModel.PROP_PASSWORD_SHA256);
String encoded = compositePasswordEncoder.encodePassword("md4", rawPass, List.of("md4"));
properties.put(ContentModel.PROP_PASSWORD, encoded);
nodeService.setProperties(userNodeRef, properties);
pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertEquals(encoded, pass);
dao.deleteUser(user);
}
finally
{
compositePasswordEncoder.setPreferredEncoding(defaultPreferredEncoding);
}
}
/**

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,14 +25,16 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.ConnectException;
import org.alfresco.error.ExceptionStackUtil;
import org.alfresco.repo.security.authentication.AuthenticationContext;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
@@ -41,9 +43,6 @@ import org.alfresco.util.BaseSpringTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.representations.AccessTokenResponse;
import org.springframework.beans.factory.annotation.Autowired;
public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@@ -65,7 +64,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Autowired
private PersonService personService;
private AuthzClient mockAuthzClient;
private IdentityServiceFacade mockIdentityServiceFacade;
@Before
public void setUp()
@@ -76,8 +75,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
authComponent.setNodeService(nodeService);
authComponent.setPersonService(personService);
mockAuthzClient = mock(AuthzClient.class);
authComponent.setAuthenticatorAuthzClient(mockAuthzClient);
mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
}
@After
@@ -89,8 +88,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test (expected=AuthenticationException.class)
public void testAuthenticationFail()
{
when(mockAuthzClient.obtainAccessToken("username", "password"))
.thenThrow(new HttpResponseException("Unauthorized", 401, "Unauthorized", null));
doThrow(new CredentialsVerificationException("Failed"))
.when(mockIdentityServiceFacade)
.verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
}
@@ -98,8 +98,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test(expected = AuthenticationException.class)
public void testAuthenticationFail_connectionException()
{
when(mockAuthzClient.obtainAccessToken("username", "password")).thenThrow(
new RuntimeException("Couldn't connect to server", new ConnectException("ConnectionRefused")));
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
.when(mockIdentityServiceFacade)
.verifyCredentials("username", "password");
try
{
@@ -116,8 +117,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test (expected=AuthenticationException.class)
public void testAuthenticationFail_otherException()
{
when(mockAuthzClient.obtainAccessToken("username", "password"))
.thenThrow(new RuntimeException("Some other errors!"));
doThrow(new RuntimeException("Some other errors!"))
.when(mockIdentityServiceFacade)
.verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
}
@@ -125,8 +127,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test
public void testAuthenticationPass()
{
when(mockAuthzClient.obtainAccessToken("username", "password"))
.thenReturn(new AccessTokenResponse());
doNothing().when(mockIdentityServiceFacade).verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
@@ -135,9 +136,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
}
@Test (expected= AuthenticationException.class)
public void testFallthroughWhenAuthzClientIsNull()
public void testFallthroughWhenIdentityServiceFacadeIsNull()
{
authComponent.setAuthenticatorAuthzClient(null);
authComponent.setIdentityServiceFacade(null);
authComponent.authenticateImpl("username", "password".toCharArray());
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,377 +25,89 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.ArgumentMatchers.any;
import static java.util.Optional.ofNullable;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.util.Enumeration;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Pattern;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import org.alfresco.repo.management.subsystems.AbstractChainedSubsystemTest;
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
import junit.framework.TestCase;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
import org.alfresco.util.ApplicationContextHelper;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.AccessToken;
import org.springframework.context.ApplicationContext;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
import org.alfresco.service.cmr.security.PersonService;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
/**
* Tests the Identity Service based authentication subsystem.
*
* @author Gavin Cornwell
*/
public class IdentityServiceRemoteUserMapperTest extends AbstractChainedSubsystemTest
public class IdentityServiceRemoteUserMapperTest extends TestCase
{
private static final String REMOTE_USER_MAPPER_BEAN_NAME = "remoteUserMapper";
private static final String DEPLOYMENT_BEAN_NAME = "identityServiceDeployment";
private static final String CONFIG_BEAN_NAME = "identityServiceConfig";
private static final String TEST_USER_USERNAME = "testuser";
private static final String TEST_USER_EMAIL = "testuser@mail.com";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private static final String BASIC_PREFIX = "Basic ";
private static final String CONFIG_SILENT_ERRORS = "identity-service.authentication.validation.failure.silent";
private static final String PASSWORD_GRANT_RESPONSE = "{" +
"\"access_token\": \"%s\"," +
"\"expires_in\": 300," +
"\"refresh_expires_in\": 1800," +
"\"refresh_token\": \"%s\"," +
"\"token_type\": \"bearer\"," +
"\"not-before-policy\": 0," +
"\"session_state\": \"71c2c5ba-9c98-49fc-882f-dedcf80ee1b5\"}";
ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
DefaultChildApplicationContextManager childApplicationContextManager;
ChildApplicationContextFactory childApplicationContextFactory;
private KeyPair keyPair;
private IdentityServiceConfig identityServiceConfig;
@Override
protected void setUp() throws Exception
public void testValidToken()
{
// switch authentication to use token auth
childApplicationContextManager = (DefaultChildApplicationContextManager) ctx.getBean("Authentication");
childApplicationContextManager.stop();
childApplicationContextManager.setProperty("chain", "identity-service1:identity-service");
childApplicationContextFactory = getChildApplicationContextFactory(childApplicationContextManager, "identity-service1");
// generate keys for test
this.keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
// hardcode the realm public key in the deployment bean to stop it fetching keys
applyHardcodedPublicKey(this.keyPair.getPublic());
// extract config
this.identityServiceConfig = (IdentityServiceConfig)childApplicationContextFactory.
getApplicationContext().getBean(CONFIG_BEAN_NAME);
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("VaLiD-ToKeN", () -> "johny"));
HttpServletRequest mockRequest = createMockTokenRequest("VaLiD-ToKeN");
final String user = mapper.getRemoteUser(mockRequest);
assertEquals("johny", user);
}
@Override
protected void tearDown() throws Exception
public void testWrongTokenWithSilentValidation()
{
childApplicationContextManager.destroy();
childApplicationContextManager = null;
childApplicationContextFactory = null;
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected ");}));
mapper.setValidationFailureSilent(true);
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
final String user = mapper.getRemoteUser(mockRequest);
assertNull(user);
}
public void testKeycloakConfig() throws Exception
public void testWrongTokenWithoutSilentValidation()
{
//Get the host of the IDS test server
String ip = "localhost";
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
// filters out 127.0.0.1 and inactive interfaces
if (iface.isLoopback() || !iface.isUp())
continue;
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected");}));
mapper.setValidationFailureSilent(false);
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while(addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if(Pattern.matches("([0-9]{1,3}\\.){3}[0-9]{1,3}", addr.getHostAddress())){
ip = addr.getHostAddress();
break;
}
}
}
} catch (SocketException e) {
throw new RuntimeException(e);
}
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
// check string overrides
assertEquals("identity-service.auth-server-url", "http://"+ip+":8999/auth",
this.identityServiceConfig.getAuthServerUrl());
assertEquals("identity-service.realm", "alfresco",
this.identityServiceConfig.getRealm());
assertThatExceptionOfType(AuthenticationException.class)
.isThrownBy(() -> mapper.getRemoteUser(mockRequest))
.havingCause().withNoCause().withMessage("Expected");
}
assertEquals("identity-service.realm-public-key",
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l" +
"BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM" +
"uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W" +
"ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD" +
"P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j" +
"QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx" +
"jwIDAQAB",
this.identityServiceConfig.getRealmKey());
assertEquals("identity-service.ssl-required", "external",
this.identityServiceConfig.getSslRequired());
assertEquals("identity-service.resource", "test",
this.identityServiceConfig.getResource());
assertEquals("identity-service.cors-allowed-headers", "Authorization",
this.identityServiceConfig.getCorsAllowedHeaders());
assertEquals("identity-service.cors-allowed-methods", "POST, PUT, DELETE, GET",
this.identityServiceConfig.getCorsAllowedMethods());
assertEquals("identity-service.cors-exposed-headers", "WWW-Authenticate, My-custom-exposed-Header",
this.identityServiceConfig.getCorsExposedHeaders());
assertEquals("identity-service.truststore",
"classpath:/alfresco/subsystems/identityServiceAuthentication/keystore.jks",
this.identityServiceConfig.getTruststore());
assertEquals("identity-service.truststore-password", "password",
this.identityServiceConfig.getTruststorePassword());
assertEquals("identity-service.client-keystore",
"classpath:/alfresco/subsystems/identityServiceAuthentication/keystore.jks",
this.identityServiceConfig.getClientKeystore());
assertEquals("identity-service.client-keystore-password", "password",
this.identityServiceConfig.getClientKeystorePassword());
assertEquals("identity-service.client-key-password", "password",
this.identityServiceConfig.getClientKeyPassword());
assertEquals("identity-service.token-store", "SESSION",
this.identityServiceConfig.getTokenStore());
assertEquals("identity-service.principal-attribute", "preferred_username",
this.identityServiceConfig.getPrincipalAttribute());
// check number overrides
assertEquals("identity-service.confidential-port", 100,
this.identityServiceConfig.getConfidentialPort());
assertEquals("identity-service.cors-max-age", 1000,
this.identityServiceConfig.getCorsMaxAge());
assertEquals("identity-service.connection-pool-size", 5,
this.identityServiceConfig.getConnectionPoolSize());
assertEquals("identity-service.register-node-period", 50,
this.identityServiceConfig.getRegisterNodePeriod());
assertEquals("identity-service.token-minimum-time-to-live", 10,
this.identityServiceConfig.getTokenMinimumTimeToLive());
assertEquals("identity-service.min-time-between-jwks-requests", 60,
this.identityServiceConfig.getMinTimeBetweenJwksRequests());
assertEquals("identity-service.public-key-cache-ttl", 3600,
this.identityServiceConfig.getPublicKeyCacheTtl());
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
{
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
when(facade.extractUsernameFromToken(anyString()))
.thenAnswer(i ->
ofNullable(tokenToUser.get(i.getArgument(0, String.class)))
.map(Supplier::get));
assertEquals("identity-service.client-connection-timeout", 3000,
this.identityServiceConfig.getClientConnectionTimeout());
final PersonService personService = mock(PersonService.class);
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
assertEquals("identity-service.client-socket-timeout", 1000,
this.identityServiceConfig.getClientSocketTimeout());
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
mapper.setIdentityServiceFacade(facade);
mapper.setPersonService(personService);
mapper.setActive(true);
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
// check boolean overrides
assertFalse("identity-service.public-client",
this.identityServiceConfig.isPublicClient());
assertTrue("identity-service.use-resource-role-mappings",
this.identityServiceConfig.isUseResourceRoleMappings());
assertTrue("identity-service.enable-cors",
this.identityServiceConfig.isCors());
assertTrue("identity-service.expose-token",
this.identityServiceConfig.isExposeToken());
assertTrue("identity-service.bearer-only",
this.identityServiceConfig.isBearerOnly());
assertTrue("identity-service.autodetect-bearer-only",
this.identityServiceConfig.isAutodetectBearerOnly());
assertTrue("identity-service.enable-basic-auth",
this.identityServiceConfig.isEnableBasicAuth());
assertTrue("identity-service.allow-any-hostname",
this.identityServiceConfig.isAllowAnyHostname());
assertTrue("identity-service.disable-trust-manager",
this.identityServiceConfig.isDisableTrustManager());
assertTrue("identity-service.always-refresh-token",
this.identityServiceConfig.isAlwaysRefreshToken());
assertTrue("identity-service.register-node-at-startup",
this.identityServiceConfig.isRegisterNodeAtStartup());
assertTrue("identity-service.enable-pkce",
this.identityServiceConfig.isPkce());
assertTrue("identity-service.ignore-oauth-query-parameter",
this.identityServiceConfig.isIgnoreOAuthQueryParameter());
assertTrue("identity-service.turn-off-change-session-id-on-login",
this.identityServiceConfig.getTurnOffChangeSessionIdOnLogin());
// check credentials overrides
Map<String, Object> credentials = this.identityServiceConfig.getCredentials();
assertNotNull("Expected a credentials map", credentials);
assertFalse("Expected to retrieve a populated credentials map", credentials.isEmpty());
assertEquals("identity-service.credentials.secret", "11111", credentials.get("secret"));
assertEquals("identity-service.credentials.provider", "secret", credentials.get("provider"));
return mapper;
}
public void testValidToken() throws Exception
{
// create token
String jwt = generateToken(false);
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
// validate correct user was found
assertEquals(TEST_USER_USERNAME, ((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
}
public void testWrongPublicKey() throws Exception
{
// generate and apply an incorrect public key
childApplicationContextFactory.stop();
applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
// create token
String jwt = generateToken(false);
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
// ensure null is returned if the public key is wrong
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
}
public void testWrongPublicKeyWithError() throws Exception
{
// generate and apply an incorrect public key
childApplicationContextFactory.stop();
childApplicationContextFactory.setProperty(CONFIG_SILENT_ERRORS, "false");
applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
// create token
String jwt = generateToken(false);
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
// ensure user mapper falls through instead of throwing an exception
String user = ((RemoteUserMapper)childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest);
assertEquals("Returned user should be null when wrong public key is used.", null, user);
}
public void testInvalidJwt() throws Exception
{
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest("thisisnotaJWT");
// ensure null is returned if the JWT is invalid
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
}
public void testMissingToken() throws Exception
{
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest("");
// ensure null is returned if the token is missing
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
}
public void testExpiredToken() throws Exception
{
// create token
String jwt = generateToken(true);
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
// ensure null is returned if the token has expired
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
}
public void testExpiredTokenWithError() throws Exception
{
// turn on validation failure reporting
childApplicationContextFactory.stop();
childApplicationContextFactory.setProperty(CONFIG_SILENT_ERRORS, "false");
applyHardcodedPublicKey(this.keyPair.getPublic());
// create token
String jwt = generateToken(true);
// create mock request object
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
// ensure an exception is thrown with correct description
String user = ((RemoteUserMapper)childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest);
assertEquals("Returned user should be null when the token is expired.", null, user);
}
public void testMissingHeader() throws Exception
{
// create mock request object with no Authorization header
HttpServletRequest mockRequest = createMockTokenRequest(null);
// ensure null is returned if the header was missing
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
}
/**
* Utility method for creating a mocked Servlet request with a token.
*
@@ -412,99 +124,12 @@ public class IdentityServiceRemoteUserMapperTest extends AbstractChainedSubsyste
{
authHeaderValues.add(BEARER_PREFIX + token);
}
when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements());
return mockRequest;
}
/**
* Utility method for creating a mocked Servlet request with basic auth.
*
* @return The mocked request object
*/
@SuppressWarnings("unchecked")
private HttpServletRequest createMockBasicRequest()
{
// Mock a request with the token in the Authorization header (if supplied)
HttpServletRequest mockRequest = mock(HttpServletRequest.class);
Vector<String> authHeaderValues = new Vector<>(1);
String userPwd = TEST_USER_USERNAME + ":" + TEST_USER_USERNAME;
authHeaderValues.add(BASIC_PREFIX + Base64.encodeBytes(userPwd.getBytes()));
// NOTE: as getHeaders gets called twice provide two separate Enumeration objects so that
// an empty result is not returned for the second invocation.
when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements(),
authHeaderValues.elements());
return mockRequest;
}
private HttpClient createMockHttpClient() throws Exception
{
// mock HttpClient object and set on keycloak deployment to avoid basic auth
// attempting to get a token using HTTP POST
HttpClient mockHttpClient = mock(HttpClient.class);
HttpResponse mockHttpResponse = mock(HttpResponse.class);
StatusLine mockStatusLine = mock(StatusLine.class);
HttpEntity mockHttpEntity = mock(HttpEntity.class);
// for the purpose of this test use the same token for access and refresh
String token = generateToken(false);
String jsonResponse = String.format(PASSWORD_GRANT_RESPONSE, token, token);
ByteArrayInputStream jsonResponseStream = new ByteArrayInputStream(jsonResponse.getBytes());
when(mockHttpClient.execute(any())).thenReturn(mockHttpResponse);
when(mockHttpResponse.getStatusLine()).thenReturn(mockStatusLine);
when(mockHttpResponse.getEntity()).thenReturn(mockHttpEntity);
when(mockStatusLine.getStatusCode()).thenReturn(200);
when(mockHttpEntity.getContent()).thenReturn(jsonResponseStream);
return mockHttpClient;
}
/**
* Utility method to create tokens for testing.
*
* @param expired Determines whether to create an expired JWT
* @return The string representation of the JWT
*/
private String generateToken(boolean expired) throws Exception
{
String issuerUrl = this.identityServiceConfig.getAuthServerUrl() + "/realms/" + this.identityServiceConfig.getRealm();
AccessToken token = new AccessToken();
token.type("Bearer");
token.id("1234");
token.subject("abc123");
token.issuer(issuerUrl);
token.setPreferredUsername(TEST_USER_USERNAME);
token.setEmail(TEST_USER_EMAIL);
token.setGivenName("Joe");
token.setFamilyName("Bloggs");
if (expired)
{
token.expiration(Time.currentTime() - 60);
}
String jwt = new JWSBuilder()
.jsonContent(token)
.rsa256(keyPair.getPrivate());
when(mockRequest.getHeaders(AUTHORIZATION_HEADER))
.thenReturn(authHeaderValues.elements());
when(mockRequest.getHeader(AUTHORIZATION_HEADER))
.thenReturn(authHeaderValues.isEmpty() ? null : authHeaderValues.get(0));
return jwt;
}
/**
* Finds the keycloak deployment bean and applies a hardcoded public key locator using the
* provided public key.
*/
private void applyHardcodedPublicKey(PublicKey publicKey)
{
KeycloakDeployment deployment = (KeycloakDeployment)childApplicationContextFactory.getApplicationContext().
getBean(DEPLOYMENT_BEAN_NAME);
HardcodedPublicKeyLocator publicKeyLocator = new HardcodedPublicKeyLocator(publicKey);
deployment.setPublicKeyLocator(publicKeyLocator);
return mockRequest;
}
}

View File

@@ -0,0 +1,101 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.util.function.Supplier;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.LazyInstantiatingIdentityServiceFacade;
import org.junit.Test;
public class LazyInstantiatingIdentityServiceFacadeUnitTest
{
private static final String USER_NAME = "marlon";
private static final String PASSWORD = "brando";
private static final String TOKEN = "token";
@Test
public void shouldRecoverFromInitialAuthorizationServerUnavailability()
{
final IdentityServiceFacade targetFacade = mock(IdentityServiceFacade.class);
final LazyInstantiatingIdentityServiceFacade facade = new LazyInstantiatingIdentityServiceFacade(faultySupplier(3, targetFacade));
assertThatExceptionOfType(IdentityServiceFacadeException.class)
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
.havingCause().withNoCause().withMessage("Expected failure #1");
verifyNoInteractions(targetFacade);
assertThatExceptionOfType(IdentityServiceFacadeException.class)
.isThrownBy(() -> facade.verifyCredentials(USER_NAME, PASSWORD))
.havingCause().withNoCause().withMessage("Expected failure #2");
verifyNoInteractions(targetFacade);
assertThatExceptionOfType(IdentityServiceFacadeException.class)
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
.havingCause().withNoCause().withMessage("Expected failure #3");
verifyNoInteractions(targetFacade);
facade.verifyCredentials(USER_NAME, PASSWORD);
verify(targetFacade).verifyCredentials(USER_NAME, PASSWORD);
}
@Test
public void shouldAvoidCreatingMultipleInstanceOfOAuth2AuthorizedClientManager()
{
final IdentityServiceFacade targetFacade = mock(IdentityServiceFacade.class);
final Supplier<IdentityServiceFacade> supplier = mock(Supplier.class);
when(supplier.get()).thenReturn(targetFacade);
final LazyInstantiatingIdentityServiceFacade facade = new LazyInstantiatingIdentityServiceFacade(supplier);
facade.verifyCredentials(USER_NAME, PASSWORD);
facade.extractUsernameFromToken(TOKEN);
facade.verifyCredentials(USER_NAME, PASSWORD);
facade.extractUsernameFromToken(TOKEN);
facade.verifyCredentials(USER_NAME, PASSWORD);
verify(supplier, times(1)).get();
verify(targetFacade, times(3)).verifyCredentials(USER_NAME, PASSWORD);
verify(targetFacade, times(2)).extractUsernameFromToken(TOKEN);
}
private Supplier<IdentityServiceFacade> faultySupplier(int numberOfInitialFailures, IdentityServiceFacade facade)
{
final int[] counter = new int[]{0};
return () -> {
if (counter[0]++ < numberOfInitialFailures)
{
throw new RuntimeException("Expected failure #" + counter[0]);
}
return facade;
};
}
}

View File

@@ -0,0 +1,73 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.SpringBasedIdentityServiceFacade;
import org.junit.Test;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.jwt.JwtDecoder;
public class SpringBasedIdentityServiceFacadeUnitTest
{
private static final String USER_NAME = "user";
private static final String PASSWORD = "password";
private static final String TOKEN = "tEsT-tOkEn";
@Test
public void shouldThrowVerificationExceptionOnFailure()
{
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
when(authClientManager.authorize(any())).thenThrow(new RuntimeException("Expected"));
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(authClientManager, jwtDecoder);
assertThatExceptionOfType(CredentialsVerificationException.class)
.isThrownBy(() -> facade.verifyCredentials(USER_NAME, PASSWORD))
.havingCause().withNoCause().withMessage("Expected");
}
@Test
public void shouldThrowTokenExceptionOnFailure()
{
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
when(jwtDecoder.decode(TOKEN)).thenThrow(new RuntimeException("Expected"));
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(authClientManager, jwtDecoder);
assertThatExceptionOfType(TokenException.class)
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
.havingCause().withNoCause().withMessage("Expected");
}
}

View File

@@ -66,3 +66,5 @@ encryption.cipherAlgorithm=DESede/CBC/PKCS5Padding
encryption.keystore.type=JCEKS
encryption.keystore.backup.type=JCEKS
# For CI override the default hashing algorithm for password storage to save build time.
system.preferred.password.encoding=sha256

21
scripts/prepare_buildx.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
BUILDER_NAME="${1}"
TARGET_REGISTRY="${2}"
TARGET_IMAGE="${3}"
IMAGE_TAG="${4}"
#Create a `docker-container` builder with host networking and required flags (quay.io)
docker --config target/docker/"${TARGET_REGISTRY}"/"${TARGET_IMAGE}"/"${IMAGE_TAG}"/docker \
buildx create --use --name "${BUILDER_NAME}" --driver-opt network=host \
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
#Create a `docker-container` builder with host networking and required flags (docker.io)
docker --config target/docker/"${TARGET_IMAGE}"/"${IMAGE_TAG}"/docker \
buildx create --use --name "${BUILDER_NAME}" --driver-opt network=host \
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
#Create a `docker-container` builder with host networking and required flags (local registry)
docker --config target/docker/127.0.0.1/5000/"${TARGET_IMAGE}"/"${IMAGE_TAG}"/docker \
buildx create --use --name "${BUILDER_NAME}" --driver-opt network=host \
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'