Compare commits

...

100 Commits

Author SHA1 Message Date
alfresco-build
7b572a3e02 [maven-release-plugin][skip ci] prepare release 20.132 2023-04-04 08:13:24 +00:00
Piotr Żurek
b1cbebacc3 ACS-4891 Configurable Public Key Cache (#1858) 2023-04-04 09:25:22 +02:00
alfresco-build
0bbe73aee1 [maven-release-plugin][skip ci] prepare for next development iteration 2023-04-03 19:15:09 +00:00
alfresco-build
9135fdae8b [maven-release-plugin][skip ci] prepare release 20.131 2023-04-03 19:15:05 +00:00
MohinishSah
0cb0cfa8f6 Merge pull request #1857 from Alfresco/ACA-4689
added the new email template
2023-04-03 16:46:48 +00:00
alfresco-build
5a96cce5bb [maven-release-plugin][skip ci] prepare for next development iteration 2023-04-03 16:20:26 +00:00
alfresco-build
c999fb25b9 [maven-release-plugin][skip ci] prepare release 20.130 2023-04-03 16:20:23 +00:00
Mohinish Sah
3c2643dbf2 added the new email template 2023-04-03 21:09:46 +05:30
Piotr Żurek
106e398393 ACS-4895 Restore support for providing IDS public key (#1856) 2023-04-03 17:35:14 +02:00
alfresco-build
1c45748f9a [maven-release-plugin][skip ci] prepare for next development iteration 2023-04-03 09:15:11 +00:00
alfresco-build
2b7ff213a9 [maven-release-plugin][skip ci] prepare release 20.129 2023-04-03 09:15:08 +00:00
Piotr Żurek
f693c49e87 ACS-4893 ACS-4890 Restore connection/TLS/mTLS IDS configuration (#1853)
* Unify http client for all IDS related activities
* Restore connection configuration
* Restore TLS/mTLS configuration
2023-04-03 10:30:01 +02:00
alfresco-build
0f156ba259 [maven-release-plugin][skip ci] prepare for next development iteration 2023-04-02 00:06:41 +00:00
alfresco-build
31a0a51290 [maven-release-plugin][skip ci] prepare release 20.128 2023-04-02 00:06:38 +00:00
Alfresco CI User
c8fe044993 [force] Force release for 2023-04-02. 2023-04-02 00:03:22 +00:00
alfresco-build
28058864e6 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-31 14:13:44 +00:00
alfresco-build
23ccb397f4 [maven-release-plugin][skip ci] prepare release 20.127 2023-03-31 14:13:40 +00:00
Piotr Żurek
73ae0d0a6e ACS-4847 Remove Keycloak dependencies (#1848) 2023-03-31 15:28:14 +02:00
alfresco-build
f9b5e5a6fe [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-31 10:27:19 +00:00
alfresco-build
e5bfb0298d [maven-release-plugin][skip ci] prepare release 20.126 2023-03-31 10:27:16 +00:00
Piotr Żurek
34cb9bf76a ACS-4847 Bump AOS module (#1846) 2023-03-31 10:38:09 +02:00
alfresco-build
fcbd406170 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-30 12:26:47 +00:00
alfresco-build
730ab768e9 [maven-release-plugin][skip ci] prepare release 20.125 2023-03-30 12:26:43 +00:00
Kacper Magdziarz
fab591eb9b [ACS-4459] Investigate and extend/universalize current custom Solr mTLS implementation in Repository (#1735)
* ACS-4459 Add new HttpClient Factory for Mutual TLS and implement it for Transform Service
* ACS-4462 Add e2e for MTLS
2023-03-30 13:43:42 +02:00
alfresco-build
5bb96729fc [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-29 13:04:41 +00:00
alfresco-build
a80f2c4db7 [maven-release-plugin][skip ci] prepare release 20.124 2023-03-29 13:04:38 +00:00
Piotr Żurek
82df7ce5d4 ACS-4847 Expose authorization code and refresh token grant types for the AOS (#1836) 2023-03-29 14:16:08 +02:00
alfresco-build
73a1de37f6 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-29 11:22:30 +00:00
alfresco-build
63f9a8e142 [maven-release-plugin][skip ci] prepare release 20.123 2023-03-29 11:22:25 +00:00
rrajoria
f668ed9198 Updating webscript version 2023-03-29 15:33:18 +05:30
alfresco-build
ea15673116 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-29 08:46:12 +00:00
alfresco-build
6a2a298445 [maven-release-plugin][skip ci] prepare release 20.122 2023-03-29 08:46:08 +00:00
pksingh41
4b0a6d27f9 Merge pull request #1833 from Alfresco/ACS-4636-CreateTagCountParamSupport
[ACS-4636] create tag count param support
2023-03-29 13:26:45 +05:30
Praveen Singh
b432fdc6a3 Addressed review comments 2023-03-29 11:17:00 +05:30
alfresco-build
7d62c89708 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-28 11:09:25 +00:00
alfresco-build
e79fcf64a5 [maven-release-plugin][skip ci] prepare release 20.121 2023-03-28 11:09:22 +00:00
kcichonczyk
5df4cfff58 [ACS-4929][ACS-4927] transform-service and transform-core version bump (#1837) 2023-03-28 11:42:33 +02:00
alfresco-build
66ab0c4f46 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-28 07:41:54 +00:00
alfresco-build
1d1252438c [maven-release-plugin][skip ci] prepare release 20.120 2023-03-28 07:41:50 +00:00
MohinishSah
e4ac036f9f Merge pull request #1609 from Alfresco/feature/ACA-4619-Self-service-User-password-reset-ADW
Feature/aca 4619 self service user password reset adw
2023-03-28 06:56:49 +00:00
suneet.gupta
a9ec842358 Merge branch 'master' of github.com:Alfresco/alfresco-community-repo into ACS-4636-CreateTagCountParamSupport 2023-03-28 12:18:42 +05:30
alfresco-build
9590051941 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-27 17:12:19 +00:00
alfresco-build
37618bd56a [maven-release-plugin][skip ci] prepare release 20.119 2023-03-27 17:12:14 +00:00
Tom Page
8bb30643d4 Merge pull request #1790 from Alfresco/feature/ACS-4779-Count_isssue_for_GET_call
ACS-4779:Count field issue for GET call
2023-03-27 17:23:27 +01:00
Tom Page
f0e0819bc5 ACS-4779 Remove failing integration test.
There is no way that the changes to TagsImpl should have affected shared links and so this
test must be failing due to interactions with other tests (e.g. a change in ordering, or
unexpected data in the system).

Notably a comment above the test suggests it may be duplicated by testSharedLinkCreateGetDelete
(which is another unweildy test that covers a large number of difference scenarios). The two
tests definitely contain a large amount of duplicated code.
2023-03-27 16:32:13 +01:00
suneet.gupta
5999a448d4 Merge branch 'master' of github.com:Alfresco/alfresco-community-repo into ACS-4636-CreateTagCountParamSupport 2023-03-27 13:57:30 +05:30
Mohinish Sah
8f00b44a03 Merge remote-tracking branch 'origin/master' into feature/ACA-4619-Self-service-User-password-reset-ADW 2023-03-27 13:07:01 +05:30
alfresco-build
c0d1b33201 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-26 00:06:25 +00:00
alfresco-build
4053daaf1a [maven-release-plugin][skip ci] prepare release 20.118 2023-03-26 00:06:23 +00:00
Alfresco CI User
6942294780 [force] Force release for 2023-03-26. 2023-03-26 00:03:13 +00:00
Tom Page
384177a9d9 Merge pull request #1826 from Alfresco/feature/ACS-4863_ValidateNodeWithoutPath
ACS-4863 Add method validateOrLookupNode without path.
2023-03-24 13:53:44 +00:00
alfresco-build
bbdeb5ae97 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-24 12:17:17 +00:00
alfresco-build
95fb286c14 [maven-release-plugin][skip ci] prepare release 20.117 2023-03-24 12:17:14 +00:00
Tom Page
0652cea296 ACS-4863 Move new validation method to interface as a default implementation. 2023-03-24 11:44:55 +00:00
Tom Page
a86a954771 Merge pull request #1829 from Alfresco/feature/ACS-4863_InvestigateTestFailure
ACS-4863 Restore code to allow aliases in tag and category APIs.
2023-03-24 11:37:15 +00:00
Tom Page
c27a44d711 ACS-4863 Remove test for invalid URL.
This test behaves differently against community and enterprise as the double slash is treated differently.
2023-03-24 10:16:28 +00:00
Krystian Dabrowski
ef089472fb ACS-4915: Editing an orphaned tag results in the tag being deleted (#1823)
* ACS-4915: Editing an orphaned tag results in the tag being deleted
2023-03-24 10:55:12 +01:00
alfresco-build
5769cbe54e [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-24 09:03:52 +00:00
alfresco-build
9899f16d61 [maven-release-plugin][skip ci] prepare release 20.116 2023-03-24 09:03:48 +00:00
Tom Page
219acd6c53 Revert "Revert "ACS-4863 Allow referencing nodes by aliases for tag and category application. (#1822)""
This reverts commit 70b7d5a1f8.
2023-03-24 08:21:11 +00:00
Tom Page
70b7d5a1f8 Revert "ACS-4863 Allow referencing nodes by aliases for tag and category application. (#1822)"
This reverts commit dafa77d0a0.
2023-03-24 08:20:50 +00:00
alfresco-build
279679d78a [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-24 07:03:42 +00:00
alfresco-build
6d1dcf6c7e [maven-release-plugin][skip ci] prepare release 20.115 2023-03-24 07:03:39 +00:00
suneet.gupta
18810de7d0 Merge branch 'master' of github.com:Alfresco/alfresco-community-repo into ACS-4636-CreateTagCountParamSupport 2023-03-24 08:15:46 +05:30
Piotr Żurek
fb119565dc ACS-4888 Use Keycloak free tas-utility (#1827) 2023-03-23 18:29:19 +01:00
Tom Page
10b0d7b5f0 ACS-4863 Ensure we don't accidentally introduce circular reference. 2023-03-23 16:01:07 +00:00
Tom Page
36292d749e ACS-4779 Simplify list creation in test. 2023-03-23 13:58:45 +00:00
Tom Page
c1d424b386 Merge master into ACS-4779-Count_isssue_for_GET_call. 2023-03-23 13:56:54 +00:00
Tom Page
06c6efc6c9 ACS-4779 Remove duplicate @BeforeClass method. 2023-03-23 13:51:47 +00:00
suneet.gupta
626e5b34ef [ACS-4636] Added count support for creation of tags to a node 2023-03-23 18:35:12 +05:30
Tom Page
a549859cbe ACS-4863 Add method validateOrLookupNode without path. 2023-03-23 09:11:48 +00:00
alfresco-build
f2d858f911 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-22 17:39:49 +00:00
alfresco-build
808a7c67b7 [maven-release-plugin][skip ci] prepare release 20.114 2023-03-22 17:39:45 +00:00
Tom Page
dafa77d0a0 ACS-4863 Allow referencing nodes by aliases for tag and category application. (#1822) 2023-03-22 16:54:01 +00:00
Tom Page
fd96c90c08 ACS-4779 Remove test for unsupported use case. 2023-03-22 16:02:55 +00:00
Tom Page
a41fcdff0f ACS-4779 Ensure we only include the count for tags in the GET response when requested. 2023-03-22 15:18:37 +00:00
Tom Page
ab461e34af ACS-4779 Tidy test class. 2023-03-22 14:48:01 +00:00
Tom Page
c92ef4393c Merge master into feature branch. 2023-03-22 14:46:15 +00:00
alfresco-build
ebdc9e159e [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-22 13:30:35 +00:00
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
rrajoria
9ddbda377d Test Case for Client Workspace and changes for Review comments 2023-03-22 12:56:45 +05:30
adam.zakrzewski
267bd98d21 ACS-4779: Adding update for tests after Tome's review 2023-03-08 16:14:21 +01:00
adam.zakrzewski
bcaeedb162 ACS-4779:Count field exists in response for get list of tags even when that field is not included. Other issue is that count field is not present when we do GET call with included count for /tags/{tagId} 2023-03-08 14:39:47 +01:00
adam.zakrzewski
f93bced905 ACS-4779:Count field exists in response for get list of tags even when that field is not included. Other issue is that count field is not present when we do GET call with included count for /tags/{tagId} 2023-03-08 14:27:16 +01:00
Mohinish Sah
91f3edf8e9 incorporated review comments 2023-03-02 22:10:54 +05:30
Mohinish Sah
479724365e incorporated review comments 2023-03-02 22:04:50 +05:30
Mohinish Sah
bb1d5899d9 Merge remote-tracking branch 'origin/master' into feature/ACA-4619-Self-service-User-password-reset-ADW 2023-03-02 21:46:53 +05:30
Mohinish Sah
7ab5e8afd0 Updated the client App name 2023-03-01 18:45:08 +05:30
Mohinish Sah
5107fdfe41 resolved review comments 2023-03-01 12:45:15 +05:30
Mohinish Sah
f59ff23a45 Rebase Branch 2022-12-08 16:26:44 +05:30
rrajoria
5e2f1db714 ACA-4619: Forgot Password Changes 2022-12-08 16:21:29 +05:30
rrajoria
67ee2efc60 ACA-4619: Forgot Password Changes 2022-12-08 16:21:29 +05:30
MohinishSah
1e7dc6ed8d update Gdrive version 2022-11-14 10:54:40 +05:30
rrajoria
e5ea6db30c ACA-4619: Forgot Password Changes 2022-11-09 18:11:16 +05:30
rrajoria
78a613b1de ACA-4619: Forgot Password Changes 2022-11-09 18:10:29 +05:30
105 changed files with 5015 additions and 3588 deletions

View File

@@ -53,24 +53,6 @@ updates:
- dependency-name: org.freemarker:freemarker
versions:
- "> 2.3.20-alfresco-patched-20200421"
- dependency-name: org.keycloak:keycloak-adapter-core
versions:
- "> 12.0.2"
- dependency-name: org.keycloak:keycloak-adapter-spi
versions:
- "> 12.0.2"
- dependency-name: org.keycloak:keycloak-authz-client
versions:
- "> 12.0.2"
- dependency-name: org.keycloak:keycloak-common
versions:
- "> 12.0.2"
- dependency-name: org.keycloak:keycloak-core
versions:
- "> 12.0.2"
- dependency-name: org.keycloak:keycloak-servlet-adapter-spi
versions:
- "> 12.0.2"
- dependency-name: org.eclipse.jetty:jetty-server
versions:
- 9.4.38.v20210224

View File

@@ -313,6 +313,10 @@ jobs:
- testSuite: SearchTestSuite
compose-profile: default
mvn-options: '-Dindex.subsystem.name=solr6'
- testSuite: MTLSTestSuite
compose-profile: with-mtls-transform-core-aio
mtls: true
mvn-options: '-Dencryption.ssl.keystore.location=${GITHUB_WORKSPACE}/keystores/alfresco/alfresco.keystore -Dencryption.ssl.truststore.location=${GITHUB_WORKSPACE}/keystores/alfresco/alfresco.truststore'
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
@@ -321,6 +325,11 @@ jobs:
run: bash ./scripts/ci/init.sh
- name: "Set transformers tag"
run: echo "TRANSFORMERS_TAG=$(mvn help:evaluate -Dexpression=dependency.alfresco-transform-core.version -q -DforceStdout)" >> $GITHUB_ENV
- name: "Generate Keystores and Truststores for Mutual TLS configuration"
if: ${{ matrix.mtls }}
run: |
git clone -b "master" --depth=1 "https://${{ secrets.BOT_GITHUB_USERNAME }}:${{ secrets.BOT_GITHUB_TOKEN }}@github.com/Alfresco/alfresco-ssl-generator.git"
bash ./scripts/ci/generate_keystores.sh
- name: "Set up the environment"
run: |
if [ -e ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh ]; then

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>20.111</version>
<version>20.132</version>
</parent>
<properties>

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>20.111</version>
<version>20.132</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.111</version>
<version>20.132</version>
</parent>
<dependencies>
@@ -137,6 +137,10 @@
<artifactId>commons-dbcp2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2023 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.httpclient;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
public class HttpClient4Factory
{
protected static final String TLS_PROTOCOL = "TLS";
protected static final String HTTPS_PROTOCOL = "https";
protected static final String HTTP_TARGET_HOST = "http.target_host";
protected static final String TLS_V_1_2 = "TLSv1.2";
protected static final String TLS_V_1_3 = "TLSv1.3";
private static SSLContext createSSLContext(HttpClientConfig config)
{
KeyManager[] keyManagers = config.getKeyStore().createKeyManagers();
TrustManager[] trustManagers = config.getTrustStore().createTrustManagers();
try
{
SSLContext sslcontext = SSLContext.getInstance(TLS_PROTOCOL);
sslcontext.init(keyManagers, trustManagers, null);
return sslcontext;
}
catch(Throwable e)
{
throw new AlfrescoRuntimeException("Unable to create SSL context", e);
}
}
public static CloseableHttpClient createHttpClient(HttpClientConfig config)
{
return createHttpClient(config, null);
}
public static CloseableHttpClient createHttpClient(HttpClientConfig config, HttpClientConnectionManager connectionManager)
{
HttpClientBuilder clientBuilder = HttpClients.custom();
if(config.isMTLSEnabled())
{
clientBuilder.addInterceptorFirst((HttpRequestInterceptor) (request, context) -> {
if (!((HttpHost) context.getAttribute(HTTP_TARGET_HOST)).getSchemeName().equals(HTTPS_PROTOCOL))
{
String msg = "mTLS is enabled but provided URL does not use a secured protocol";
throw new HttpClientException(msg);
}
});
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
createSSLContext(config),
new String[] { TLS_V_1_2, TLS_V_1_3 },
null,
config.isHostnameVerificationDisabled() ? new NoopHostnameVerifier() : SSLConnectionSocketFactory.getDefaultHostnameVerifier());
clientBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
}
if(connectionManager != null)
{
clientBuilder.setConnectionManager(connectionManager);
}
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(config.getConnectionTimeout())
.setSocketTimeout(config.getSocketTimeout())
.setConnectionRequestTimeout(config.getConnectionRequestTimeout())
.build();
clientBuilder.setDefaultRequestConfig(requestConfig);
clientBuilder.setMaxConnTotal(config.getMaxTotalConnections());
clientBuilder.setMaxConnPerRoute(config.getMaxHostConnections());
clientBuilder.setRetryHandler(new StandardHttpRequestRetryHandler(5, false));
return clientBuilder.build();
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2023 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.httpclient;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
import org.alfresco.encryption.AlfrescoKeyStore;
import org.alfresco.encryption.AlfrescoKeyStoreImpl;
import org.alfresco.encryption.KeyResourceLoader;
import org.alfresco.encryption.ssl.SSLEncryptionParameters;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class HttpClientConfig
{
private static final String HTTPCLIENT_CONFIG = "httpclient.config.";
protected static final Log LOGGER = LogFactory.getLog(HttpClientConfig.class);
private Properties properties;
private String serviceName;
private SSLEncryptionParameters sslEncryptionParameters;
private KeyResourceLoader keyResourceLoader;
private AlfrescoKeyStore keyStore;
private AlfrescoKeyStore trustStore;
private Map<String, String> config;
public void setProperties(Properties properties)
{
this.properties = properties;
}
public void setServiceName(String serviceName)
{
this.serviceName = serviceName;
}
public void setSslEncryptionParameters(SSLEncryptionParameters sslEncryptionParameters)
{
this.sslEncryptionParameters = sslEncryptionParameters;
}
public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader)
{
this.keyResourceLoader = keyResourceLoader;
}
public AlfrescoKeyStore getKeyStore()
{
return keyStore;
}
public AlfrescoKeyStore getTrustStore()
{
return trustStore;
}
public void init()
{
this.keyStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getKeyStoreParameters(), keyResourceLoader);
this.trustStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getTrustStoreParameters(), keyResourceLoader);
config = retrieveConfig(serviceName);
checkUnsupportedProperties(config);
}
/**
* Method used for retrieving HttpClient config from Global Properties
* @param serviceName name of used service
* @return map of properties
*/
private Map<String, String> retrieveConfig(String serviceName)
{
return properties.keySet().stream()
.filter(key -> key instanceof String)
.map(Object::toString)
.filter(key -> key.startsWith(HTTPCLIENT_CONFIG + serviceName))
.collect(Collectors.toMap(
key -> key.replace(HTTPCLIENT_CONFIG + serviceName + ".", ""),
key -> properties.getProperty(key, null)));
}
private void checkUnsupportedProperties(Map<String, String> config)
{
config.keySet().stream()
.filter(propertyName -> !HttpClientPropertiesEnum.isPropertyNameSupported(propertyName))
.forEach(propertyName -> LOGGER.warn(String.format("For service [%s], an unsupported property [%s] is set", serviceName, propertyName)));
}
private Integer getIntegerProperty(HttpClientPropertiesEnum property)
{
return Integer.parseInt(extractValueFromConfig(property).orElse("0"));
}
private Boolean getBooleanProperty(HttpClientPropertiesEnum property)
{
return Boolean.parseBoolean(extractValueFromConfig(property).orElse("false"));
}
private Optional<String> extractValueFromConfig(HttpClientPropertiesEnum property)
{
Optional<String> optionalProperty = Optional.ofNullable(config.get(property.name));
if(property.isRequired && optionalProperty.isEmpty())
{
String msg = String.format("Required property: '%s' is empty.", property.name);
throw new HttpClientException(msg);
}
return optionalProperty;
}
public Integer getConnectionTimeout()
{
return getIntegerProperty(HttpClientPropertiesEnum.CONNECTION_REQUEST_TIMEOUT);
}
public Integer getSocketTimeout()
{
return getIntegerProperty(HttpClientPropertiesEnum.SOCKET_TIMEOUT);
}
public Integer getConnectionRequestTimeout()
{
return getIntegerProperty(HttpClientPropertiesEnum.CONNECTION_REQUEST_TIMEOUT);
}
public Integer getMaxTotalConnections()
{
return getIntegerProperty(HttpClientPropertiesEnum.MAX_TOTAL_CONNECTIONS);
}
public Integer getMaxHostConnections()
{
return getIntegerProperty(HttpClientPropertiesEnum.MAX_HOST_CONNECTIONS);
}
public Boolean isMTLSEnabled()
{
return getBooleanProperty(HttpClientPropertiesEnum.MTLS_ENABLED);
}
public boolean isHostnameVerificationDisabled()
{
return getBooleanProperty(HttpClientPropertiesEnum.HOSTNAME_VERIFICATION_DISABLED);
}
private enum HttpClientPropertiesEnum
{
CONNECTION_TIMEOUT("connectionTimeout", true),
SOCKET_TIMEOUT("socketTimeout", true),
CONNECTION_REQUEST_TIMEOUT("connectionRequestTimeout", true),
MAX_TOTAL_CONNECTIONS("maxTotalConnections", true),
MAX_HOST_CONNECTIONS("maxHostConnections", true),
HOSTNAME_VERIFICATION_DISABLED("hostnameVerificationDisabled", false),
MTLS_ENABLED("mTLSEnabled", true);
private final String name;
private final Boolean isRequired;
HttpClientPropertiesEnum(String propertyName, Boolean isRequired)
{
this.name = propertyName;
this.isRequired = isRequired;
}
private static final List<String> supportedProperties = new ArrayList<>();
static {
for (HttpClientPropertiesEnum property : HttpClientPropertiesEnum.values()) {
supportedProperties.add(property.name);
}
}
public static boolean isPropertyNameSupported(String propertyName) {
return supportedProperties.contains(propertyName);
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.httpclient;
import org.alfresco.error.AlfrescoRuntimeException;
public class HttpClientException extends AlfrescoRuntimeException
{
public HttpClientException(String msgId)
{
super(msgId);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,6 @@ import org.alfresco.utility.data.AisToken;
import org.alfresco.utility.data.auth.DataAIS;
import org.alfresco.utility.model.UserModel;
import org.apache.chemistry.opencmis.commons.SessionParameter;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.representations.AccessTokenResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -86,9 +84,9 @@ public class AuthParameterProviderFactory
parameters.put(SessionParameter.OAUTH_REFRESH_TOKEN, aisToken.getRefreshToken());
parameters.put(SessionParameter.OAUTH_EXPIRATION_TIMESTAMP, String.valueOf(System.currentTimeMillis()
+ (aisToken.getExpiresIn() * 1000))); // getExpiresIn is in seconds
parameters.put(SessionParameter.OAUTH_TOKEN_ENDPOINT, cmisProperties.aisProperty().getAdapterConfig().getAuthServerUrl()
parameters.put(SessionParameter.OAUTH_TOKEN_ENDPOINT, cmisProperties.aisProperty().getAuthServerUrl()
+ "/realms/alfresco/protocol/openid-connect/token");
parameters.put(SessionParameter.OAUTH_CLIENT_ID, cmisProperties.aisProperty().getAdapterConfig().getResource());
parameters.put(SessionParameter.OAUTH_CLIENT_ID, cmisProperties.aisProperty().getResource());
return parameters;
}
@@ -110,10 +108,10 @@ public class AuthParameterProviderFactory
// Attempt to get an access token for userModel from AIS
aisToken = dataAIS.perform().getAccessToken(userModel);
}
catch (HttpResponseException e)
catch (AssertionError e)
{
// Trying to authenticate with invalid user credentials so return an invalid access token
if (e.getStatusCode() == 401)
if (e.getMessage().contains("invalid_grant"))
{
STEP(String.format("%s Invalid user credentials were provided %s:%s. Using invalid token for reqest.",
STEP_PREFIX, userModel.getUsername(), userModel.getPassword()));

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.111</version>
<version>20.132</version>
</parent>
<properties>

View File

@@ -30,7 +30,6 @@ import static org.alfresco.utility.report.log.Step.STEP;
import org.alfresco.utility.data.AisToken;
import org.alfresco.utility.data.auth.DataAIS;
import org.alfresco.utility.model.UserModel;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -86,12 +85,11 @@ public class RestAisAuthentication
// Attempt to get an access token for userModel from AIS
aisToken = dataAIS.perform().getAccessToken(userModel);
}
catch (HttpResponseException e)
catch (AssertionError e)
{
// Trying to authenticate with invalid user credentials or disabled
// user so return an invalid access token
String httpResponse = new String(e.getBytes());
if (e.getStatusCode() == 401 || httpResponse.contains(USER_DISABLED_MSG))
if (e.getMessage().contains("invalid_grant"))
{
STEP(String.format("%s User disabled or invalid user credentials were provided %s:%s. Using invalid token for request.", STEP_PREFIX,
userModel.getUsername(), userModel.getPassword()));

View File

@@ -1,5 +1,10 @@
package org.alfresco.rest.tags;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
import org.alfresco.rest.model.RestErrorModel;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.utility.constants.UserRole;
@@ -14,6 +19,11 @@ import org.testng.annotations.Test;
public class GetTagTests extends TagsDataPrep
{
private static final String FIELD_ID = "id";
private static final String FIELD_TAG = "tag";
private static final String FIELD_COUNT = "count";
private static final String TAG_NAME_PREFIX = "tag-name";
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION, description = "Verify admin user gets tag using REST API and status code is OK (200)")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void adminIsAbleToGetTag() throws Exception
@@ -128,4 +138,25 @@ public class GetTagTests extends TagsDataPrep
.descriptionURLIs(RestErrorModel.RESTAPIEXPLORER)
.stackTraceIs(RestErrorModel.STACKTRACE);
}
}
/**
* Verify that count field is not present for searched tag.
*/
@Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void testGetTag_notIncludingCount()
{
STEP("Create single tag as admin");
final RestTagModel tagModel = createTagModelWithName(getRandomName(TAG_NAME_PREFIX).toLowerCase());
final RestTagModel createdTag = restClient.authenticateUser(adminUserModel).withCoreAPI().createSingleTag(tagModel);
restClient.assertStatusCodeIs(CREATED);
STEP("Get a single tag, not including count and verify if it is not present in the response");
final RestTagModel searchedTag = restClient.withCoreAPI().getTag(createdTag);
restClient.assertStatusCodeIs(OK);
searchedTag.assertThat().field(FIELD_TAG).is(tagModel.getTag())
.assertThat().field(FIELD_ID).isNotEmpty()
.assertThat().field(FIELD_COUNT).isNull();
}
}

View File

@@ -1,6 +1,8 @@
package org.alfresco.rest.tags;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.OK;
import java.util.Set;
@@ -8,7 +10,6 @@ import org.alfresco.rest.model.RestErrorModel;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.rest.model.RestTagModelsCollection;
import org.alfresco.utility.constants.UserRole;
import org.alfresco.utility.data.RandomData;
import org.alfresco.utility.model.TestGroup;
import org.alfresco.utility.testrail.ExecutionType;
import org.alfresco.utility.testrail.annotation.TestRail;
@@ -18,13 +19,18 @@ import org.testng.annotations.Test;
@Test(groups = {TestGroup.REQUIRE_SOLR})
public class GetTagsTests extends TagsDataPrep
{
private static final String FIELD_ID = "id";
private static final String FIELD_TAG = "tag";
private static final String FIELD_COUNT = "count";
@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
public void getTagsWithManagerRole()
{
restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager));
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase());
@@ -32,11 +38,11 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION, description = "Verify user with Collaborator role gets tags using REST API and status code is OK (200)")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void getTagsWithCollaboratorRole() throws Exception
public void getTagsWithCollaboratorRole()
{
restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteCollaborator));
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase());
@@ -44,11 +50,11 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION, description = "Verify user with Contributor role gets tags using REST API and status code is OK (200)")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void getTagsWithContributorRole() throws Exception
public void getTagsWithContributorRole()
{
restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteContributor));
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase());
@@ -56,11 +62,11 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION, description = "Verify user with Consumer role gets tags using REST API and status code is OK (200)")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void getTagsWithConsumerRole() throws Exception
public void getTagsWithConsumerRole()
{
restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteConsumer));
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase());
@@ -69,7 +75,7 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.SANITY, description = "Failed authentication get tags call returns status code 401 with Manager role")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.SANITY })
// @Bug(id="MNT-16904", description = "It fails only on environment with tenants")
public void failedAuthenticationReturnsUnauthorizedStatus() throws Exception
public void failedAuthenticationReturnsUnauthorizedStatus()
{
restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager));
userModel = dataUser.createRandomTestUser();
@@ -83,7 +89,7 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "Verify that if maxItems is invalid status code returned is 400")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void maxItemsInvalidValueTest() throws Exception
public void maxItemsInvalidValueTest()
{
restClient.authenticateUser(adminUserModel).withParams("maxItems=abc").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError().containsSummary(String.format(RestErrorModel.INVALID_MAXITEMS, "abc"));
@@ -92,7 +98,7 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "Verify that if skipCount is invalid status code returned is 400")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void skipCountInvalidValueTest() throws Exception
public void skipCountInvalidValueTest()
{
restClient.authenticateUser(adminUserModel).withParams("skipCount=abc").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError().containsSummary(String.format(RestErrorModel.INVALID_SKIPCOUNT, "abc"));
@@ -101,11 +107,11 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "Verify that file tag is retrieved")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void fileTagIsRetrieved() throws Exception
public void fileTagIsRetrieved()
{
restClient.authenticateUser(adminUserModel);
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase());
@@ -114,11 +120,11 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "Verify that folder tag is retrieved")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void folderTagIsRetrieved() throws Exception
public void folderTagIsRetrieved()
{
restClient.authenticateUser(adminUserModel);
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", folderTagValue.toLowerCase());
}
@@ -127,11 +133,11 @@ public class GetTagsTests extends TagsDataPrep
description = "Verify site Manager is able to get tags using properties parameter."
+ "Check that properties filter is applied.")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void siteManagerIsAbleToRetrieveTagsWithPropertiesParameter() throws Exception
public void siteManagerIsAbleToRetrieveTagsWithPropertiesParameter()
{
returnedCollection = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager))
.withParams("maxItems=5000&properties=tag").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase())
@@ -141,15 +147,15 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "With admin get tags and use skipCount parameter. Check pagination")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void useSkipCountCheckPagination() throws Exception
public void useSkipCountCheckPagination()
{
returnedCollection = restClient.authenticateUser(adminUserModel).withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
RestTagModel firstTag = returnedCollection.getEntries().get(0).onModel();
RestTagModel secondTag = returnedCollection.getEntries().get(1).onModel();
RestTagModelsCollection tagsWithSkipCount = restClient.withParams("skipCount=2").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
tagsWithSkipCount.assertThat().entriesListDoesNotContain("tag", firstTag.getTag())
.assertThat().entriesListDoesNotContain("tag", secondTag.getTag());
@@ -159,15 +165,15 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "With admin get tags and use maxItems parameter. Check pagination")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void useMaxItemsParameterCheckPagination() throws Exception
public void useMaxItemsParameterCheckPagination()
{
returnedCollection = restClient.authenticateUser(adminUserModel).withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
RestTagModel firstTag = returnedCollection.getEntries().get(0).onModel();
RestTagModel secondTag = returnedCollection.getEntries().get(1).onModel();
RestTagModelsCollection tagsWithMaxItems = restClient.withParams("maxItems=2").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
tagsWithMaxItems.assertThat().entriesListContains("tag", firstTag.getTag())
.assertThat().entriesListContains("tag", secondTag.getTag())
@@ -178,11 +184,11 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "With manager get tags and use high skipCount parameter. Check pagination")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void useHighSkipCountCheckPagination() throws Exception
public void useHighSkipCountCheckPagination()
{
returnedCollection = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager))
.withParams("skipCount=20000").withCoreAPI().getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
restClient.assertStatusCodeIs(OK);
returnedCollection.assertThat().entriesListIsEmpty()
.getPagination().assertThat().field("maxItems").is(100)
.and().field("hasMoreItems").is("false")
@@ -194,7 +200,7 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "With Collaborator user get tags and use maxItems with value zero. Check default error model schema")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void useMaxItemsWithValueZeroCheckDefaultErrorModelSchema() throws Exception
public void useMaxItemsWithValueZeroCheckDefaultErrorModelSchema()
{
returnedCollection = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteCollaborator))
.withParams("maxItems=0").withCoreAPI().getTags();
@@ -208,9 +214,9 @@ public class GetTagsTests extends TagsDataPrep
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "With Manager user delete tag. Check it is not retrieved anymore.")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void checkThatDeletedTagIsNotRetrievedAnymore() throws Exception
public void checkThatDeletedTagIsNotRetrievedAnymore()
{
String removedTag = RandomData.getRandomName("tag3");
String removedTag = getRandomName("tag3");
RestTagModel deletedTag = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager))
.withCoreAPI().usingResource(document).addTag(removedTag);
@@ -339,4 +345,36 @@ public class GetTagsTests extends TagsDataPrep
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST)
.assertLastError().containsSummary("An invalid WHERE query was received. Unsupported Predicate");
}
/**
* Verify if count field is present for searched tags.
*/
@Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void testGetTags_includingCount()
{
STEP("Get tags including count and verify if it is present in the response");
final RestTagModelsCollection searchedTags = restClient.withCoreAPI().include(FIELD_COUNT).getTags();
restClient.assertStatusCodeIs(OK);
searchedTags.assertThat().entriesListIsNotEmpty()
.assertThat().entriesListContains(FIELD_COUNT)
.assertThat().entriesListContains(FIELD_TAG)
.assertThat().entriesListContains(FIELD_ID);
}
/**
* Verify if count field is not present for searched tags.
*/
@Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void testGetTags_notIncludingCount()
{
STEP("Get tags, not including count and verify if it is not in the response");
final RestTagModelsCollection searchedTags = restClient.withCoreAPI().getTags();
restClient.assertStatusCodeIs(OK);
searchedTags.assertThat().entriesListIsNotEmpty()
.assertThat().entriesListDoesNotContain(FIELD_COUNT)
.assertThat().entriesListContains(FIELD_TAG)
.assertThat().entriesListContains(FIELD_ID);
}
}

View File

@@ -78,4 +78,9 @@ public class TagsDataPrep extends RestTest
.tag(tag)
.create();
}
protected RestTagModel createTagModelWithName(final String tagName)
{
return RestTagModel.builder().tag(tagName).create();
}
}

View File

@@ -1,5 +1,7 @@
package org.alfresco.rest.tags;
import static org.alfresco.utility.report.log.Step.STEP;
import org.alfresco.rest.model.RestErrorModel;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.utility.Utility;
@@ -41,6 +43,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.withCoreAPI().usingTag(oldTag).update(randomTag);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(randomTag);
returnedModel.assertThat().field("id").isNotNull();
}
@TestRail(section = { TestGroup.REST_API,
@@ -158,8 +161,8 @@ public class UpdateTagTests extends TagsDataPrep
{
String invalidTagBody = ".\"/<>*";
RestTagModel tag = restClient.authenticateUser(adminUserModel).withCoreAPI().usingResource(document).addTag(RandomData.getRandomName("tag"));
Utility.sleep(500, 20000, () ->
{
Utility.sleep(500, 20000, () ->
{
restClient.withCoreAPI().usingTag(tag).update(invalidTagBody);
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST)
.assertLastError().containsSummary(String.format(RestErrorModel.INVALID_TAG, invalidTagBody));
@@ -178,6 +181,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.withCoreAPI().usingTag(oldTag).update(largeStringTag);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(largeStringTag);
returnedModel.assertThat().field("id").isNotNull();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -192,6 +196,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.withCoreAPI().usingTag(oldTag).update(shortStringTag);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(shortStringTag);
returnedModel.assertThat().field("id").isNotNull();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -206,6 +211,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.withCoreAPI().usingTag(oldTag).update(specialCharsString);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(specialCharsString);
returnedModel.assertThat().field("id").isNotNull();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -223,6 +229,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.withCoreAPI().usingTag(oldExistingTag).update(existingTag);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(existingTag);
returnedModel.assertThat().field("id").isNotNull();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -242,6 +249,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.withCoreAPI().usingTag(newTagModel).update(newTag);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(newTag);
returnedModel.assertThat().field("id").isNotNull();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -255,6 +263,7 @@ public class UpdateTagTests extends TagsDataPrep
returnedModel = restClient.authenticateUser(adminUserModel).withCoreAPI().usingTag(oldTag).update(newTag);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(newTag);
returnedModel.assertThat().field("id").isNotNull();
restClient.withCoreAPI().usingResource(document).deleteTag(returnedModel);
restClient.assertStatusCodeIs(HttpStatus.NO_CONTENT);
@@ -262,4 +271,18 @@ public class UpdateTagTests extends TagsDataPrep
restClient.withCoreAPI().usingResource(document).addTag(newTag);
restClient.assertStatusCodeIs(HttpStatus.CREATED);
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.SANITY,
description = "Verify Admin user updates orphan tags and status code is 200")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.SANITY })
public void adminIsAbleToUpdateOrphanTag()
{
STEP("Update orphan tag and expect 200");
final String newTagName = RandomData.getRandomName("new");
returnedModel = restClient.authenticateUser(adminUserModel).withCoreAPI().usingTag(orphanTag).update(newTagName);
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedModel.assertThat().field("tag").is(newTagName);
returnedModel.assertThat().field("id").isNotNull();
}
}

View File

@@ -150,18 +150,6 @@ public class GetNodeTagsTests extends TagsDataPrep
restClient.assertStatusCodeIs(HttpStatus.NOT_FOUND).assertLastError().containsSummary(String.format(RestErrorModel.ENTITY_NOT_FOUND, nodeRef));
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "Verify that if node id is empty returns status code 403")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
public void emptyNodeIdTest() throws Exception
{
FileModel badDocument = dataContent.usingSite(siteModel).usingUser(adminUserModel).createContent(CMISUtil.DocumentType.TEXT_PLAIN);
badDocument.setNodeRef("");
restClient.authenticateUser(adminUserModel).withCoreAPI().usingResource(badDocument).getNodeTags();
restClient.assertStatusCodeIs(HttpStatus.NOT_FOUND).assertLastError().containsSummary(String.format(RestErrorModel.ENTITY_NOT_FOUND, ""));
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
description = "Verify folder tags")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION})
@@ -303,4 +291,4 @@ public class GetNodeTagsTests extends TagsDataPrep
.and().field("skipCount").is("10000");
returnedCollection.assertThat().entriesListCountIs(0);
}
}
}

View File

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

View File

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

19
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.111</version>
<version>20.132</version>
<packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name>
@@ -52,17 +52,17 @@
<dependency.alfresco-messaging-repo.version>1.2.20</dependency.alfresco-messaging-repo.version>
<dependency.activiti-engine.version>5.23.0</dependency.activiti-engine.version>
<dependency.activiti.version>5.23.0</dependency.activiti.version>
<dependency.alfresco-transform-service.version>2.0.0</dependency.alfresco-transform-service.version>
<dependency.alfresco-transform-core.version>3.0.0</dependency.alfresco-transform-core.version>
<dependency.alfresco-transform-service.version>2.1.0-A2</dependency.alfresco-transform-service.version>
<dependency.alfresco-transform-core.version>3.1.0-A2</dependency.alfresco-transform-core.version>
<dependency.alfresco-greenmail.version>6.5</dependency.alfresco-greenmail.version>
<dependency.acs-event-model.version>0.0.18</dependency.acs-event-model.version>
<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.38</dependency.webscripts.version>
<dependency.webscripts.version>8.40</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>
@@ -83,7 +83,6 @@
<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.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 -->
@@ -113,7 +112,7 @@
<dependency.jakarta-rpc-api.version>1.1.4</dependency.jakarta-rpc-api.version>
<alfresco.googledrive.version>3.4.0-M1</alfresco.googledrive.version>
<alfresco.aos-module.version>1.6.0-M1</alfresco.aos-module.version>
<alfresco.aos-module.version>1.6.0-A4</alfresco.aos-module.version>
<alfresco.api-explorer.version>7.3.0</alfresco.api-explorer.version> <!-- Also in alfresco-enterprise-share -->
<alfresco.maven-plugin.version>2.2.0</alfresco.maven-plugin.version>
@@ -123,7 +122,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.61</dependency.tas-utility.version>
<dependency.tas-utility.version>4.0.0</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>
@@ -151,7 +150,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.111</tag>
<tag>20.132</tag>
</scm>
<distributionManagement>
@@ -530,7 +529,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>

View File

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

View File

@@ -39,6 +39,8 @@ import org.alfresco.rest.api.model.LockInfo;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.api.model.PathInfo;
import org.alfresco.rest.api.model.UserInfo;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.resource.content.BasicContentInfo;
import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
@@ -208,6 +210,19 @@ public interface Nodes
NodeRef validateNode(StoreRef storeRef, String nodeId);
NodeRef validateNode(String nodeId);
NodeRef validateNode(NodeRef nodeRef);
/**
* Check that the specified id refers to a valid node.
*
* @param nodeId The node id to look up using SpacesStore or an alias like -root-.
* @return The node ref.
* @throws InvalidArgumentException if the specified node id is not a valid format.
* @throws EntityNotFoundException if the specified node was not found in the database.
*/
default NodeRef validateOrLookupNode(String nodeId)
{
return validateOrLookupNode(nodeId, null);
}
NodeRef validateOrLookupNode(String nodeId, String path);
boolean nodeMatches(NodeRef nodeRef, Set<QName> expectedTypes, Set<QName> excludedTypes);

View File

@@ -37,7 +37,7 @@ import org.alfresco.service.cmr.repository.StoreRef;
public interface Tags
{
List<Tag> addTags(String nodeId, List<Tag> tags);
List<Tag> addTags(String nodeId, List<Tag> tags, Parameters parameters);
Tag getTag(StoreRef storeRef, String tagId);
void deleteTag(String nodeId, String tagId);
CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params);

View File

@@ -189,7 +189,7 @@ public class CategoriesImpl implements Categories
@Override
public List<Category> listCategoriesForNode(final String nodeId, final Parameters parameters)
{
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
final NodeRef contentNodeRef = nodes.validateOrLookupNode(nodeId);
verifyReadPermission(contentNodeRef);
verifyNodeType(contentNodeRef);
@@ -211,7 +211,7 @@ public class CategoriesImpl implements Categories
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY);
}
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
final NodeRef contentNodeRef = nodes.validateOrLookupNode(nodeId);
verifyChangePermission(contentNodeRef);
verifyNodeType(contentNodeRef);
@@ -237,7 +237,7 @@ public class CategoriesImpl implements Categories
public void unlinkNodeFromCategory(final StoreRef storeRef, final String nodeId, final String categoryId, final Parameters parameters)
{
final NodeRef categoryNodeRef = getCategoryNodeRef(storeRef, categoryId);
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
final NodeRef contentNodeRef = nodes.validateOrLookupNode(nodeId);
verifyChangePermission(contentNodeRef);
verifyNodeType(contentNodeRef);

View File

@@ -1356,7 +1356,7 @@ public class NodesImpl implements Nodes
private void calculateRelativePath(String parentFolderNodeId, Node node)
{
NodeRef rootNodeRef = validateOrLookupNode(parentFolderNodeId, null);
NodeRef rootNodeRef = validateOrLookupNode(parentFolderNodeId);
try
{
// get the path elements
@@ -1741,7 +1741,7 @@ public class NodesImpl implements Nodes
@Override
public void deleteNode(String nodeId, Parameters parameters)
{
NodeRef nodeRef = validateOrLookupNode(nodeId, null);
NodeRef nodeRef = validateOrLookupNode(nodeId);
if (isSpecialNode(nodeRef, getNodeType(nodeRef)))
{
@@ -1785,7 +1785,7 @@ public class NodesImpl implements Nodes
validateProperties(nodeInfo.getProperties(), EXCLUDED_NS, Arrays.asList());
// check that requested parent node exists and it's type is a (sub-)type of folder
NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId);
// node name - mandatory
String nodeName = nodeInfo.getName();
@@ -2290,7 +2290,7 @@ public class NodesImpl implements Nodes
validateAspects(nodeInfo.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS);
validateProperties(nodeInfo.getProperties(), EXCLUDED_NS, Arrays.asList());
final NodeRef nodeRef = validateOrLookupNode(nodeId, null);
final NodeRef nodeRef = validateOrLookupNode(nodeId);
QName nodeTypeQName = getNodeType(nodeRef);
@@ -2523,8 +2523,8 @@ public class NodesImpl implements Nodes
throw new InvalidArgumentException("Missing targetParentId");
}
final NodeRef parentNodeRef = validateOrLookupNode(targetParentId, null);
final NodeRef sourceNodeRef = validateOrLookupNode(sourceNodeId, null);
final NodeRef parentNodeRef = validateOrLookupNode(targetParentId);
final NodeRef sourceNodeRef = validateOrLookupNode(sourceNodeId);
FileInfo fi = moveOrCopyImpl(sourceNodeRef, parentNodeRef, name, isCopy);
return getFolderOrDocument(fi.getNodeRef().getId(), parameters);
@@ -2954,7 +2954,7 @@ public class NodesImpl implements Nodes
throw new InvalidArgumentException("The request content-type is not multipart: "+parentFolderNodeId);
}
NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId);
if (!nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false))
{
throw new InvalidArgumentException("NodeId of folder is expected: " + parentNodeRef.getId());
@@ -3385,7 +3385,7 @@ public class NodesImpl implements Nodes
@Override
public Node lock(String nodeId, LockInfo lockInfo, Parameters parameters)
{
NodeRef nodeRef = validateOrLookupNode(nodeId, null);
NodeRef nodeRef = validateOrLookupNode(nodeId);
if (isSpecialNode(nodeRef, getNodeType(nodeRef)))
{
@@ -3424,7 +3424,7 @@ public class NodesImpl implements Nodes
@Override
public Node unlock(String nodeId, Parameters parameters)
{
NodeRef nodeRef = validateOrLookupNode(nodeId, null);
NodeRef nodeRef = validateOrLookupNode(nodeId);
if (isSpecialNode(nodeRef, getNodeType(nodeRef)))
{

View File

@@ -185,7 +185,7 @@ public class QueriesImpl implements Queries, InitializingBean
String rootNodeId = parameters.getParameter(PARAM_ROOT_NODE_ID);
if (rootNodeId != null)
{
NodeRef nodeRef = nodes.validateOrLookupNode(rootNodeId, null);
NodeRef nodeRef = nodes.validateOrLookupNode(rootNodeId);
query.append("PATH:\"").append(getQNamePath(nodeRef.getId())).append("//*\" AND (");
}
if (term != null)

View File

@@ -695,7 +695,7 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware
{
if (versionLabelId != null)
{
nodeRef = nodes.validateOrLookupNode(nodeRef.getId(), null);
nodeRef = nodes.validateOrLookupNode(nodeRef.getId());
VersionHistory vh = versionService.getVersionHistory(nodeRef);
if (vh != null)
{

View File

@@ -25,11 +25,12 @@
*/
package org.alfresco.rest.api.impl;
import static java.util.stream.Collectors.toList;
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;
@@ -95,7 +96,7 @@ public class TagsImpl implements Tags
this.typeConstraint = typeConstraint;
}
public void setNodes(Nodes nodes)
public void setNodes(Nodes nodes)
{
this.nodes = nodes;
}
@@ -114,44 +115,36 @@ public class TagsImpl implements Tags
this.authorityService = authorityService;
}
public List<Tag> addTags(String nodeId, final List<Tag> tags)
{
NodeRef nodeRef = nodes.validateNode(nodeId);
if(!typeConstraint.matches(nodeRef))
{
throw new UnsupportedResourceOperationException("Cannot tag this node");
}
public List<Tag> addTags(String nodeId, final List<Tag> tags, final Parameters parameters)
{
NodeRef nodeRef = nodes.validateOrLookupNode(nodeId);
if (!typeConstraint.matches(nodeRef))
{
throw new UnsupportedResourceOperationException("Cannot tag this node");
}
List<String> tagValues = new AbstractList<String>()
List<String> tagValues = tags.stream().map(Tag::getTag).collect(toList());
try
{
List<Pair<String, NodeRef>> tagNodeRefs = taggingService.addTags(nodeRef, tagValues);
List<Tag> ret = new ArrayList<>(tags.size());
List<Pair<String, Integer>> tagsCountPairList = taggingService.findTaggedNodesAndCountByTagName(nodeRef.getStoreRef());
Map<String, Integer> tagsCountMap = tagsCountPairList.stream().collect(Collectors.toMap(Pair::getFirst,Pair::getSecond));
for (Pair<String, NodeRef> pair : tagNodeRefs)
{
@Override
public String get(int arg0)
{
String tag = tags.get(arg0).getTag();
return tag;
}
@Override
public int size()
{
return tags.size();
}
};
try
{
List<Pair<String, NodeRef>> tagNodeRefs = taggingService.addTags(nodeRef, tagValues);
List<Tag> ret = new ArrayList<Tag>(tags.size());
for(Pair<String, NodeRef> pair : tagNodeRefs)
{
ret.add(new Tag(pair.getSecond(), pair.getFirst()));
}
return ret;
Tag createdTag = new Tag(pair.getSecond(), pair.getFirst());
if (parameters.getInclude().contains(PARAM_INCLUDE_COUNT)) {
createdTag.setCount(Optional.ofNullable(tagsCountMap.get(createdTag.getTag())).orElse(0) + 1);
}
ret.add(createdTag);
}
catch(IllegalArgumentException e)
{
throw new InvalidArgumentException(e.getMessage());
}
}
return ret;
}
catch (IllegalArgumentException e)
{
throw new InvalidArgumentException(e.getMessage());
}
}
public void deleteTag(String nodeId, String tagId)
{
@@ -171,21 +164,25 @@ public class TagsImpl implements Tags
taggingService.deleteTag(storeRef, tagValue);
}
@Override
@Override
public CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params)
{
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));
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<>(page.size());
List<Pair<String, Integer>> tagsByCount;
Map<String, Integer> tagsByCountMap = new HashMap<>();
for (Pair<NodeRef, String> pair : page)
{
Tag selectedTag = new Tag(pair.getFirst(), pair.getSecond());
tags.add(selectedTag);
}
if (params.getInclude().contains(PARAM_INCLUDE_COUNT))
{
tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef);
List<Pair<String, Integer>> tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef);
Map<String, Integer> tagsByCountMap = new HashMap<>();
if (tagsByCount != null)
{
for (Pair<String, Integer> tagByCountElem : tagsByCount)
@@ -193,12 +190,7 @@ public class TagsImpl implements Tags
tagsByCountMap.put(tagByCountElem.getFirst(), tagByCountElem.getSecond());
}
}
}
for (Pair<NodeRef, String> pair : page)
{
Tag selectedTag = new Tag(pair.getFirst(), pair.getSecond());
selectedTag.setCount(Optional.ofNullable(tagsByCountMap.get(selectedTag.getTag())).orElse(0));
tags.add(selectedTag);
tags.forEach(tag -> tag.setCount(Optional.ofNullable(tagsByCountMap.get(tag.getTag())).orElse(0)));
}
return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), totalItems);
@@ -254,17 +246,17 @@ public class TagsImpl implements Tags
public CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params)
{
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();
List<Tag> tags = new ArrayList<Tag>(page.size());
for(Pair<NodeRef, String> pair : page)
{
tags.add(new Tag(pair.getFirst(), pair.getSecond()));
}
NodeRef nodeRef = nodes.validateOrLookupNode(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();
List<Tag> tags = new ArrayList<>(page.size());
for(Pair<NodeRef, String> pair : page)
{
tags.add(new Tag(pair.getFirst(), pair.getSecond()));
}
return CollectionWithPagingInfo.asPaged(params.getPaging(), tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
return CollectionWithPagingInfo.asPaged(params.getPaging(), tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
}
@Experimental
@@ -276,7 +268,7 @@ public class TagsImpl implements Tags
.filter(Objects::nonNull)
.map(Tag::getTag)
.distinct()
.collect(Collectors.toList());
.collect(toList());
if (CollectionUtils.isEmpty(tagNames))
{
@@ -290,7 +282,7 @@ public class TagsImpl implements Tags
{
tag.setCount(0);
}
}).collect(Collectors.toList());
}).collect(toList());
}
private void verifyAdminAuthority()

View File

@@ -134,7 +134,7 @@ public class RestRuleModelMapper implements RestModelMapper<Rule, org.alfresco.s
public org.alfresco.service.cmr.rule.Rule toServiceModel(Rule restRuleModel)
{
final org.alfresco.service.cmr.rule.Rule serviceRule = new org.alfresco.service.cmr.rule.Rule();
final NodeRef nodeRef = (restRuleModel.getId() != null) ? nodes.validateOrLookupNode(restRuleModel.getId(), null) : null;
final NodeRef nodeRef = (restRuleModel.getId() != null) ? nodes.validateOrLookupNode(restRuleModel.getId()) : null;
serviceRule.setNodeRef(nodeRef);
serviceRule.setTitle(restRuleModel.getName());
serviceRule.setDescription(restRuleModel.getDescription());

View File

@@ -129,7 +129,7 @@ public class RestRuleSimpleConditionModelMapper implements RestModelMapper<Simpl
parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_ASPECT, ContentModel.ASPECT_GEN_CLASSIFIABLE);
try
{
parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_VALUE, nodes.validateOrLookupNode(parameter, null));
parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_VALUE, nodes.validateOrLookupNode(parameter));
} catch (EntityNotFoundException e) {
throw new InvalidArgumentException(CATEGORY_INVALID_MSG);
}

View File

@@ -173,7 +173,7 @@ public class ActionParameterConverter
}
else if (typeQName.isMatch(DataTypeDefinition.NODE_REF))
{
NodeRef nodeRef = nodes.validateOrLookupNode(stringValue, null);
NodeRef nodeRef = nodes.validateOrLookupNode(stringValue);
if (permissionService.hasReadPermission(nodeRef) != ALLOWED)
{
throw new EntityNotFoundException(stringValue);

View File

@@ -72,7 +72,7 @@ public class NodeValidator
{
try
{
final NodeRef nodeRef = nodes.validateOrLookupNode(folderNodeId, null);
final NodeRef nodeRef = nodes.validateOrLookupNode(folderNodeId);
validatePermission(requireChangePermission, nodeRef);
verifyNodeType(nodeRef, ContentModel.TYPE_FOLDER, null);

View File

@@ -31,13 +31,9 @@ import org.alfresco.rest.framework.resource.RelationshipResource;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.resource.parameters.SortColumn;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.util.ParameterCheck;
import java.util.List;
import java.util.stream.Collectors;
@RelationshipResource(name = "action-definitions", entityResource = NodesEntityResource.class, title = "Node action definitions")
public class NodeActionDefinitionsRelation extends AbstractNodeRelation
implements RelationshipResourceAction.Read<ActionDefinition>
@@ -59,7 +55,7 @@ public class NodeActionDefinitionsRelation extends AbstractNodeRelation
@Override
public CollectionWithPagingInfo<ActionDefinition> readAll(String entityResourceId, Parameters params)
{
NodeRef parentNodeRef = nodes.validateOrLookupNode(entityResourceId, null);
NodeRef parentNodeRef = nodes.validateOrLookupNode(entityResourceId);
return actions.getActionDefinitions(parentNodeRef, params);
}
}

View File

@@ -25,6 +25,11 @@
*/
package org.alfresco.rest.api.nodes;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.Node;
@@ -41,11 +46,6 @@ import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Node Parents
*
@@ -67,7 +67,7 @@ public class NodeParentsRelation extends AbstractNodeRelation implements Relatio
@WebApiDescription(title = "Return a list of parent nodes based on child assocs")
public CollectionWithPagingInfo<Node> readAll(String childNodeId, Parameters parameters)
{
NodeRef childNodeRef = nodes.validateOrLookupNode(childNodeId, null);
NodeRef childNodeRef = nodes.validateOrLookupNode(childNodeId);
QNamePattern assocTypeQNameParam = RegexQNamePattern.MATCH_ALL;

View File

@@ -25,6 +25,8 @@
*/
package org.alfresco.rest.api.nodes;
import java.util.List;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.AssocChild;
import org.alfresco.rest.api.model.Node;
@@ -41,8 +43,6 @@ import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import java.util.List;
/**
* Node Secondary Children
*
@@ -71,7 +71,7 @@ public class NodeSecondaryChildrenRelation extends AbstractNodeRelation implemen
@WebApiDescription(title = "Return a paged list of secondary child nodes based on child assocs")
public CollectionWithPagingInfo<Node> readAll(String parentNodeId, Parameters parameters)
{
NodeRef parentNodeRef = nodes.validateOrLookupNode(parentNodeId, null);
NodeRef parentNodeRef = nodes.validateOrLookupNode(parentNodeId);
QNamePattern assocTypeQNameParam = getAssocTypeFromWhereElseAll(parameters);

View File

@@ -25,6 +25,8 @@
*/
package org.alfresco.rest.api.nodes;
import java.util.List;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.framework.WebApiDescription;
import org.alfresco.rest.framework.resource.RelationshipResource;
@@ -35,8 +37,6 @@ import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QNamePattern;
import java.util.List;
/**
* Node Sources - list node (peer) associations from target to sources
*
@@ -54,7 +54,7 @@ public class NodeSourcesRelation extends AbstractNodeRelation implements Relatio
@WebApiDescription(title = "Return a paged list of sources nodes based on (peer) assocs")
public CollectionWithPagingInfo<Node> readAll(String targetNodeId, Parameters parameters)
{
NodeRef targetNodeRef = nodes.validateOrLookupNode(targetNodeId, null);
NodeRef targetNodeRef = nodes.validateOrLookupNode(targetNodeId);
QNamePattern assocTypeQNameParam = getAssocTypeFromWhereElseAll(parameters);

View File

@@ -1,28 +1,28 @@
/*
* #%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 - 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.rest.api.nodes;
import java.util.List;
@@ -61,7 +61,7 @@ public class NodeTagsRelation implements RelationshipResourceAction.Create<Tag>,
@WebApiDescription(title="Adds one or more tags to the node with id 'nodeId'.")
public List<Tag> create(String nodeId, List<Tag> tagsToCreate, Parameters parameters)
{
return tags.addTags(nodeId, tagsToCreate);
return tags.addTags(nodeId, tagsToCreate, parameters);
}
@Override

View File

@@ -25,6 +25,8 @@
*/
package org.alfresco.rest.api.nodes;
import java.util.List;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.AssocTarget;
import org.alfresco.rest.api.model.Node;
@@ -40,8 +42,6 @@ import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QNamePattern;
import org.alfresco.service.namespace.RegexQNamePattern;
import java.util.List;
/**
* Node Targets
*
@@ -64,7 +64,7 @@ public class NodeTargetsRelation extends AbstractNodeRelation implements
@WebApiDescription(title = "Return a paged list of target nodes based on (peer) assocs")
public CollectionWithPagingInfo<Node> readAll(String sourceNodeId, Parameters parameters)
{
NodeRef sourceNodeRef = nodes.validateOrLookupNode(sourceNodeId, null);
NodeRef sourceNodeRef = nodes.validateOrLookupNode(sourceNodeId);
QNamePattern assocTypeQNameParam = getAssocTypeFromWhereElseAll(parameters);

View File

@@ -25,6 +25,13 @@
*/
package org.alfresco.rest.api.nodes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.directurl.DirectAccessUrlDisabledException;
import org.alfresco.repo.node.integrity.IntegrityException;
@@ -65,13 +72,6 @@ import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.springframework.beans.factory.InitializingBean;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Node Versions - version history
*
@@ -117,7 +117,7 @@ public class NodeVersionsRelation extends AbstractNodeRelation implements
@WebApiDescription(title = "Return version history as a paged list of version node infos")
public CollectionWithPagingInfo<Node> readAll(String nodeId, Parameters parameters)
{
NodeRef nodeRef = nodes.validateOrLookupNode(nodeId, null);
NodeRef nodeRef = nodes.validateOrLookupNode(nodeId);
VersionHistory vh = versionService.getVersionHistory(nodeRef);
@@ -293,7 +293,7 @@ public class NodeVersionsRelation extends AbstractNodeRelation implements
public Version findVersion(String nodeId, String versionLabelId)
{
NodeRef nodeRef = nodes.validateOrLookupNode(nodeId, null);
NodeRef nodeRef = nodes.validateOrLookupNode(nodeId);
VersionHistory vh = versionService.getVersionHistory(nodeRef);
if (vh != null)
{

View File

@@ -128,7 +128,7 @@ public class CategoriesImplTest
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
given(nodesMock.validateNode(CATEGORY_ID)).willReturn(CATEGORY_NODE_REF);
given(nodesMock.validateNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF);
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF);
given(nodesMock.isSubClass(any(), any(), anyBoolean())).willReturn(true);
given(typeConstraint.matches(any())).willReturn(true);
given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.ALLOWED);
@@ -900,7 +900,7 @@ public class CategoriesImplTest
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks, parametersMock);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
then(permissionServiceMock).shouldHaveNoMoreInteractions();
then(typeConstraint).should().matches(CONTENT_NODE_REF);
@@ -1011,12 +1011,12 @@ public class CategoriesImplTest
@Test
public void testLinkNodeToCategories_withInvalidNodeId()
{
given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(permissionServiceMock).shouldHaveNoInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
@@ -1031,7 +1031,7 @@ public class CategoriesImplTest
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
@@ -1118,7 +1118,7 @@ public class CategoriesImplTest
objectUnderTest.unlinkNodeFromCategory(CONTENT_NODE_ID, CATEGORY_ID, parametersMock);
then(nodesMock).should().validateNode(CATEGORY_ID);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
then(permissionServiceMock).shouldHaveNoMoreInteractions();
then(typeConstraint).should().matches(CONTENT_NODE_REF);
@@ -1155,7 +1155,7 @@ public class CategoriesImplTest
// when
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
then(permissionServiceMock).shouldHaveNoMoreInteractions();
then(typeConstraint).should().matches(CONTENT_NODE_REF);
@@ -1176,12 +1176,12 @@ public class CategoriesImplTest
@Test
public void testListCategoriesForNode_withInvalidNodeId()
{
given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(EntityNotFoundException.class);
@@ -1195,7 +1195,7 @@ public class CategoriesImplTest
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodesMock).should().validateOrLookupNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)

View File

@@ -25,6 +25,8 @@
*/
package org.alfresco.rest.api.impl;
import static java.util.stream.Collectors.toList;
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;
@@ -41,7 +43,6 @@ 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;
@@ -50,6 +51,7 @@ 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.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
@@ -63,6 +65,7 @@ import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.util.Pair;
import org.alfresco.util.TypeConstraint;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -77,7 +80,9 @@ public class TagsImplTest
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(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 static final NodeRef TAG_PARENT_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, PARENT_NODE_ID);
private static final String CONTENT_NODE_ID = "content-node-id";
private static final NodeRef CONTENT_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, CONTENT_NODE_ID);
private final RecognizedParamsExtractor queryExtractor = new RecognizedParamsExtractor() {};
@@ -97,6 +102,8 @@ public class TagsImplTest
private Paging pagingMock;
@Mock
private PagingResults<Pair<NodeRef, String>> pagingResultsMock;
@Mock
private TypeConstraint typeConstraintMock;
@InjectMocks
private TagsImpl objectUnderTest;
@@ -122,7 +129,7 @@ public class TagsImplTest
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());
final List<Tag> expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME));
assertEquals(expectedTags, actualTags.getCollection());
}
@@ -140,7 +147,31 @@ public class TagsImplTest
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());
.collect(toList());
assertEquals(expectedTags, actualTags.getCollection());
}
/** Check that we can get counts for two tags - one in use and one not applied to any nodes. */
@Test
public void testGetTags_verifyCountPopulatedCorrectly()
{
NodeRef tagNodeA = new NodeRef("tag://A/");
NodeRef tagNodeB = new NodeRef("tag://B/");
List<Pair<NodeRef, String>> tagPairs = List.of(new Pair<>(tagNodeA, "tagA"), new Pair<>(tagNodeB, "tagB"));
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(tagPairs);
given(parametersMock.getInclude()).willReturn(List.of("count"));
// Only tagA is included in the returned list since tagB is not in use.
given(taggingServiceMock.findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE)).willReturn(List.of(new Pair<>("tagA", 5)));
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE);
final List<Tag> expectedTags = List.of(Tag.builder().tag("tagA").nodeRef(tagNodeA).count(5).create(),
Tag.builder().tag("tagB").nodeRef(tagNodeB).count(0).create());
assertEquals(expectedTags, actualTags.getCollection());
}
@@ -395,7 +426,7 @@ public class TagsImplTest
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).stream()
.peek(tag -> tag.setCount(0))
.collect(Collectors.toList());
.collect(toList());
assertThat(actualCreatedTags)
.isNotNull()
.isEqualTo(expectedTags);
@@ -405,18 +436,83 @@ public class TagsImplTest
public void testGetTagByIdNotFoundValidation()
{
given(primaryParentMock.getParentRef()).willReturn(TAG_NODE_REF);
objectUnderTest.getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,TAG_ID);
objectUnderTest.getTag(STORE_REF_WORKSPACE_SPACESSTORE,TAG_ID);
then(nodeServiceMock).shouldHaveNoInteractions();
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).shouldHaveNoInteractions();
}
@Test
public void testAddTags()
{
NodeRef tagNodeA = new NodeRef("tag://A/");
NodeRef tagNodeB = new NodeRef("tag://B/");
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF);
given(typeConstraintMock.matches(CONTENT_NODE_REF)).willReturn(true);
List<Pair<String, NodeRef>> pairs = List.of(new Pair<>("tagA", new NodeRef("tag://A/")), new Pair<>("tagB", new NodeRef("tag://B/")));
List<String> tagNames = pairs.stream().map(Pair::getFirst).collect(toList());
List<Tag> tags = tagNames.stream().map(name -> Tag.builder().tag(name).create()).collect(toList());
given(taggingServiceMock.addTags(CONTENT_NODE_REF, tagNames)).willReturn(pairs);
given(taggingServiceMock.findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE)).willReturn(List.of(new Pair<>("tagA", 4)));
given(parametersMock.getInclude()).willReturn(List.of("count"));
List<Tag> actual = objectUnderTest.addTags(CONTENT_NODE_ID, tags, parametersMock);
final List<Tag> expected = List.of(Tag.builder().tag("tagA").nodeRef(tagNodeA).count(5).create(),
Tag.builder().tag("tagB").nodeRef(tagNodeB).count(1).create());
assertEquals("Unexpected tags returned.", expected, actual);
}
@Test(expected = InvalidArgumentException.class)
public void testAddTagsToInvalidNode()
{
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willThrow(new InvalidArgumentException());
List<Tag> tags = List.of(Tag.builder().tag("tag1").create());
objectUnderTest.addTags(CONTENT_NODE_ID, tags, parametersMock);
}
@Test(expected = UnsupportedResourceOperationException.class)
public void testAddTagsToWrongTypeOfNode()
{
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF);
given(typeConstraintMock.matches(CONTENT_NODE_REF)).willReturn(false);
List<Tag> tags = List.of(Tag.builder().tag("tag1").create());
objectUnderTest.addTags(CONTENT_NODE_ID, tags, parametersMock);
}
@Test
public void testGetTagsForNode()
{
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF);
given(parametersMock.getPaging()).willReturn(pagingMock);
List<Pair<NodeRef, String>> pairs = List.of(new Pair<>(new NodeRef("tag://A/"), "tagA"), new Pair<>(new NodeRef("tag://B/"), "tagB"));
given(taggingServiceMock.getTags(eq(CONTENT_NODE_REF), any(PagingRequest.class))).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(null, null));
given(pagingResultsMock.getPage()).willReturn(pairs);
CollectionWithPagingInfo<Tag> actual = objectUnderTest.getTags(CONTENT_NODE_ID, parametersMock);
List<Tag> tags = pairs.stream().map(pair -> Tag.builder().tag(pair.getSecond()).nodeRef(pair.getFirst()).create()).collect(toList());
assertEquals(actual.getCollection(), tags);
}
@Test (expected = InvalidArgumentException.class)
public void testGetTagsFromInvalidNode()
{
given(nodesMock.validateOrLookupNode(CONTENT_NODE_ID)).willThrow(new InvalidArgumentException());
objectUnderTest.getTags(CONTENT_NODE_ID, parametersMock);
}
private static List<Pair<String, NodeRef>> createTagAndNodeRefPairs(final List<String> tagNames)
{
return tagNames.stream()
.map(tagName -> createPair(tagName, new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName))))
.collect(Collectors.toList());
.collect(toList());
}
private static Pair<String, NodeRef> createPair(final String tagName, final NodeRef nodeRef)
@@ -426,12 +522,12 @@ public class TagsImplTest
private static List<Tag> createTags(final List<String> tagNames)
{
return tagNames.stream().map(TagsImplTest::createTag).collect(Collectors.toList());
return tagNames.stream().map(TagsImplTest::createTag).collect(toList());
}
private static List<Tag> createTagsWithNodeRefs(final List<String> tagNames)
{
return tagNames.stream().map(TagsImplTest::createTagWithNodeRef).collect(Collectors.toList());
return tagNames.stream().map(TagsImplTest::createTagWithNodeRef).collect(toList());
}
private static Tag createTag(final String tagName)

View File

@@ -134,7 +134,7 @@ public class RestRuleModelMapperTest
// when
final org.alfresco.service.cmr.rule.Rule actualRuleModel = objectUnderTest.toServiceModel(rule);
then(nodesMock).should().validateOrLookupNode(RULE_ID, null);
then(nodesMock).should().validateOrLookupNode(RULE_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(actionMapperMock).should().toServiceModel(List.of(action));
then(actionMapperMock).shouldHaveNoMoreInteractions();

View File

@@ -275,7 +275,7 @@ public class RestRuleSimpleConditionModelMapperTest
{
final SimpleCondition simpleCondition = createSimpleCondition(PARAM_CATEGORY);
final NodeRef defaultNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARAMETER_DEFAULT);
given(nodesMock.validateOrLookupNode(PARAMETER_DEFAULT, null)).willReturn(defaultNodeRef);
given(nodesMock.validateOrLookupNode(PARAMETER_DEFAULT)).willReturn(defaultNodeRef);
// when
final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition);

View File

@@ -44,7 +44,6 @@ import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.alfresco.repo.action.executer.AddFeaturesActionExecuter;
import org.alfresco.repo.action.executer.CheckInActionExecuter;
import org.alfresco.repo.action.executer.CheckOutActionExecuter;
@@ -129,8 +128,8 @@ public class ActionParameterConverterTest
@Before
public void setUp()
{
given(nodes.validateOrLookupNode(DUMMY_FOLDER_NODE_ID, null)).willReturn(DUMMY_FOLDER_NODE);
given(nodes.validateOrLookupNode(DUMMY_SCRIPT_NODE_ID, null)).willReturn(DUMMY_SCRIPT_NODE);
given(nodes.validateOrLookupNode(DUMMY_FOLDER_NODE_ID)).willReturn(DUMMY_FOLDER_NODE);
given(nodes.validateOrLookupNode(DUMMY_SCRIPT_NODE_ID)).willReturn(DUMMY_SCRIPT_NODE);
given(permissionService.hasReadPermission(DUMMY_FOLDER_NODE)).willReturn(ALLOWED);
given(permissionService.hasReadPermission(DUMMY_SCRIPT_NODE)).willReturn(ALLOWED);
}
@@ -598,7 +597,7 @@ public class ActionParameterConverterTest
String permissionDeniedNodeId = "permission://denied/node";
final Map<String, Serializable> params = Map.of(PARAM_DESTINATION_FOLDER, permissionDeniedNodeId);
NodeRef permissionDeniedNode = new NodeRef(permissionDeniedNodeId);
given(nodes.validateOrLookupNode(permissionDeniedNodeId, null)).willReturn(permissionDeniedNode);
given(nodes.validateOrLookupNode(permissionDeniedNodeId)).willReturn(permissionDeniedNode);
given(permissionService.hasReadPermission(permissionDeniedNode)).willReturn(DENIED);
given(actionService.getActionDefinition(name)).willReturn(actionDefinition);

View File

@@ -101,7 +101,7 @@ public class NodeValidatorTest
public void setUp() throws Exception
{
MockitoAnnotations.openMocks(this);
given(nodesMock.validateOrLookupNode(eq(FOLDER_NODE_ID), any())).willReturn(folderNodeRef);
given(nodesMock.validateOrLookupNode(FOLDER_NODE_ID)).willReturn(folderNodeRef);
given(nodesMock.validateNode(RULE_SET_ID)).willReturn(ruleSetNodeRef);
given(nodesMock.validateNode(RULE_ID)).willReturn(ruleNodeRef);
given(nodesMock.nodeMatches(any(), any(), any())).willReturn(true);
@@ -115,7 +115,7 @@ public class NodeValidatorTest
// when
final NodeRef nodeRef = nodeValidator.validateFolderNode(FOLDER_NODE_ID, false);
then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID, null);
then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID);
then(nodesMock).should().nodeMatches(folderNodeRef, Set.of(TYPE_FOLDER), null);
then(nodesMock).shouldHaveNoMoreInteractions();
then(permissionServiceMock).should().hasReadPermission(folderNodeRef);
@@ -128,13 +128,13 @@ public class NodeValidatorTest
@Test
public void testValidateFolderNode_notExistingFolder()
{
given(nodesMock.validateOrLookupNode(any(), any())).willThrow(new EntityNotFoundException(FOLDER_NODE_ID));
given(nodesMock.validateOrLookupNode(FOLDER_NODE_ID)).willThrow(new EntityNotFoundException(FOLDER_NODE_ID));
//when
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(
() -> nodeValidator.validateFolderNode(FOLDER_NODE_ID, false));
then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID, null);
then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(ruleServiceMock).shouldHaveNoInteractions();
}
@@ -148,7 +148,7 @@ public class NodeValidatorTest
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(
() -> nodeValidator.validateFolderNode(FOLDER_NODE_ID, false));
then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID, null);
then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID);
then(nodesMock).should().nodeMatches(folderNodeRef, Set.of(TYPE_FOLDER), null);
then(nodesMock).shouldHaveNoMoreInteractions();
then(ruleServiceMock).shouldHaveNoInteractions();
@@ -431,11 +431,12 @@ public class NodeValidatorTest
then(ruleServiceMock).shouldHaveNoMoreInteractions();
}
private void resetNodesMock() {
private void resetNodesMock()
{
reset(nodesMock);
given(nodesMock.validateOrLookupNode(eq(FOLDER_NODE_ID), any())).willReturn(folderNodeRef);
given(nodesMock.validateOrLookupNode(FOLDER_NODE_ID)).willReturn(folderNodeRef);
given(nodesMock.validateNode(RULE_SET_ID)).willReturn(ruleSetNodeRef);
given(nodesMock.validateNode(RULE_ID)).willReturn(ruleNodeRef);
given(nodesMock.nodeMatches(ruleSetNodeRef, Set.of(ContentModel.TYPE_SYSTEM_FOLDER), null)).willReturn(true);
}
}
}

View File

@@ -37,10 +37,8 @@ import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.notNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.notNull;
import static org.mockito.Mockito.when;
import java.io.Serializable;
@@ -186,7 +184,7 @@ public class ResultMapperTests
when(sr.getVersionService()).thenReturn(versionService);
when(sr.getNodeService()).thenReturn(nodeService);
when(nodes.validateOrLookupNode(nullable(String.class), nullable(String.class))).thenAnswer(invocation ->
when(nodes.validateOrLookupNode(nullable(String.class))).thenAnswer(invocation ->
{
Object[] args = invocation.getArguments();
String aNode = (String)args[0];

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

@@ -734,195 +734,6 @@ public class SharedLinkApiTest extends AbstractBaseApiTest
}
}
/**
* Tests shared links to file (content) in a multi-tenant system.
*
* <p>POST:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links}
*
* <p>DELETE:</p>
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links/<sharedId>}
*
* <p>GET:</p>
* The following do not require authentication
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links/<sharedId>}
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links/<sharedId>/content}
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links/<sharedId>/renditions}
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links/<sharedId>/renditions/<renditionId>}
* {@literal <host>:<port>/alfresco/api/<networkId>/public/alfresco/versions/1/shared-links/<sharedId>/renditions/<renditionId>/content}
*
*/
// TODO now covered by testSharedLinkCreateGetDelete ? (since base class now uses tenant context by default)
@Test
public void testSharedLinkCreateGetDelete_MultiTenant() throws Exception
{
// As user1
setRequestContext(user1);
String docLibNodeId = getSiteContainerNodeId(tSiteId, "documentLibrary");
String folderName = "folder" + System.currentTimeMillis() + "_1";
String folderId = createFolder(docLibNodeId, folderName, null).getId();
// create doc d1 - pdf
String fileName1 = "quick" + RUNID + "_1.pdf";
File file1 = getResourceFile("quick.pdf");
byte[] file1_originalBytes = Files.readAllBytes(Paths.get(file1.getAbsolutePath()));
String file1_MimeType = MimetypeMap.MIMETYPE_PDF;
MultiPartBuilder.MultiPartRequest reqBody = MultiPartBuilder.create()
.setFileData(new MultiPartBuilder.FileData(fileName1, file1, file1_MimeType))
.build();
HttpResponse response = post(getNodeChildrenUrl(folderId), reqBody.getBody(), null, reqBody.getContentType(), 201);
Document doc1 = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class);
String d1Id = doc1.getId();
assertNotNull(d1Id);
// create shared link to document 1
Map<String, String> body = new HashMap<>();
body.put("nodeId", d1Id);
response = post(URL_SHARED_LINKS, toJsonAsStringNonNull(body), 201);
QuickShareLink resp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), QuickShareLink.class);
String shared1Id = resp.getId();
assertNotNull(shared1Id);
assertEquals(d1Id, resp.getNodeId());
assertEquals(fileName1, resp.getName());
assertEquals(file1_MimeType, resp.getContent().getMimeType());
assertEquals(user1, resp.getSharedByUser().getId());
// allowable operations not included - no params
response = getSingle(QuickShareLinkEntityResource.class, shared1Id, null, 200);
resp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), QuickShareLink.class);
assertNull(resp.getAllowableOperations());
setRequestContext(null);
// unauth access to get shared link info
Map<String, String> params = Collections.singletonMap("include", "allowableOperations"); // note: this will be ignore for unauth access
response = getSingle(QuickShareLinkEntityResource.class, shared1Id, params, 200);
resp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), QuickShareLink.class);
assertEquals(shared1Id, resp.getId());
assertEquals(fileName1, resp.getName());
assertEquals(d1Id, resp.getNodeId());
assertNull(resp.getAllowableOperations()); // include is ignored
assertNull(resp.getAllowableOperationsOnTarget()); // include is ignored
// unauth access to file 1 content (via shared link)
response = getSingle(QuickShareLinkEntityResource.class, shared1Id + "/content", null, 200);
assertArrayEquals(file1_originalBytes, response.getResponseAsBytes());
Map<String, String> responseHeaders = response.getHeaders();
assertNotNull(responseHeaders);
assertEquals(file1_MimeType + ";charset=utf-8", responseHeaders.get("Content-Type"));
assertNotNull(responseHeaders.get("Expires"));
assertEquals("attachment; filename=\"" + fileName1 + "\"; filename*=UTF-8''" + fileName1 + "", responseHeaders.get("Content-Disposition"));
String lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER);
assertNotNull(lastModifiedHeader);
// Test 304 response
Map<String, String> headers = Collections.singletonMap(IF_MODIFIED_SINCE_HEADER, lastModifiedHeader);
getSingle(URL_SHARED_LINKS, shared1Id + "/content", null, headers, 304);
// unauth access to file 1 content (via shared link) - without Content-Disposition header (attachment=false)
params = new HashMap<>();
params.put("attachment", "false");
response = getSingle(QuickShareLinkEntityResource.class, shared1Id + "/content", params, 200);
assertArrayEquals(file1_originalBytes, response.getResponseAsBytes());
responseHeaders = response.getHeaders();
assertNotNull(responseHeaders);
assertEquals(file1_MimeType + ";charset=utf-8", responseHeaders.get("Content-Type"));
assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER));
assertNotNull(responseHeaders.get("Expires"));
assertNull(responseHeaders.get("Content-Disposition"));
// -ve shared link rendition tests
{
// -ve test - try to get non-existent rendition content
getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/doclib/content", null, 404);
// -ve test - try to get unregistered rendition content
getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/dummy/content", null, 404);
}
// unauth access to get rendition info for a shared link (available => CREATED rendition only)
// -ve shared link rendition tests
{
// -ve test - try to get not created rendition for the given shared link
getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/doclib", null, 404);
// -ve test - try to get unregistered rendition
getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/dummy", null, 404);
}
// unauth access to get shared link renditions info (available => CREATED renditions only)
response = getAll(URL_SHARED_LINKS + "/" + shared1Id + "/renditions", null, 200);
List<Rendition> renditions = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Rendition.class);
assertEquals(0, renditions.size());
// create rendition of pdf doc - note: for some reason create rendition of txt doc fail on build m/c (TBC) ?
setRequestContext(user1);
Rendition rendition = createAndGetRendition(d1Id, "doclib");
assertNotNull(rendition);
assertEquals(Rendition.RenditionStatus.CREATED, rendition.getStatus());
setRequestContext(null);
// unauth access to get shared link renditions info (available => CREATED renditions only)
response = getAll(URL_SHARED_LINKS + "/" + shared1Id + "/renditions", null, 200);
renditions = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Rendition.class);
assertEquals(1, renditions.size());
assertEquals(Rendition.RenditionStatus.CREATED, renditions.get(0).getStatus());
assertEquals("doclib", renditions.get(0).getId());
// unauth access to get rendition info for a shared link (available => CREATED rendition only)
{
// get a created rendition for the given shared link
getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/doclib", null, 200);
}
// unauth access to get shared link file rendition content
response = getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/doclib/content", null, 200);
assertTrue(response.getResponseAsBytes().length > 0);
responseHeaders = response.getHeaders();
assertNotNull(responseHeaders);
assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=utf-8", responseHeaders.get("Content-Type"));
assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER));
assertNotNull(responseHeaders.get("Expires"));
String docName = "doclib";
assertEquals("attachment; filename=\"" + docName + "\"; filename*=UTF-8''" + docName + "", responseHeaders.get("Content-Disposition"));
// unauth access to get shared link file rendition content - without Content-Disposition header (attachment=false)
params = new HashMap<>();
params.put("attachment", "false");
response = getSingle(QuickShareLinkEntityResource.class, shared1Id + "/renditions/doclib/content", params, 200);
assertTrue(response.getResponseAsBytes().length > 0);
responseHeaders = response.getHeaders();
assertNotNull(responseHeaders);
assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=utf-8", responseHeaders.get("Content-Type"));
assertNotNull(responseHeaders.get("Expires"));
assertNull(responseHeaders.get("Content-Disposition"));
lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER);
assertNotNull(lastModifiedHeader);
// Test 304 response
headers = Collections.singletonMap(IF_MODIFIED_SINCE_HEADER, lastModifiedHeader);
getSingle(URL_SHARED_LINKS, shared1Id + "/renditions/doclib/content", null, headers, 304);
// -ve test - userTwoN1 cannot delete shared link
setRequestContext(user2);
deleteSharedLink(shared1Id, 403);
// -ve test - unauthenticated
setRequestContext(null);
deleteSharedLink(shared1Id, 401);
// delete shared link
setRequestContext(user1);
deleteSharedLink(shared1Id);
}
/**
* Tests shared links to file with expiry date.
* <p>POST:</p>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.111</version>
<version>20.132</version>
</parent>
<dependencies>
@@ -397,6 +397,14 @@
<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>
<artifactId>quartz</artifactId>
@@ -557,69 +565,6 @@
</exclusions>
</dependency>
<!-- Keycloak dependencies -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-spi</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-adapter-spi</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- required by keycloak -->
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${dependency.jboss.logging.version}</version>
</dependency>
<!-- Events dependencies -->
<dependency>
<groupId>org.alfresco</groupId>

View File

@@ -25,6 +25,7 @@
*/
package org.alfresco.repo.content.transform;
import org.alfresco.httpclient.HttpClientConfig;
import org.alfresco.repo.content.metadata.AsynchronousExtractor;
import org.alfresco.repo.rendition2.RenditionDefinition2;
import org.alfresco.service.cmr.repository.ContentReader;
@@ -60,11 +61,12 @@ public class LocalTransformImpl extends AbstractLocalTransform
boolean retryTransformOnDifferentMimeType,
Set<TransformOption> transformsTransformOptions,
LocalTransformServiceRegistry localTransformServiceRegistry, String baseUrl,
HttpClientConfig httpClientConfig,
int startupRetryPeriodSeconds)
{
super(name, transformerDebug, mimetypeService, strictMimeTypeCheck, strictMimetypeExceptions,
retryTransformOnDifferentMimeType, transformsTransformOptions, localTransformServiceRegistry);
remoteTransformerClient = new RemoteTransformerClient(name, baseUrl);
remoteTransformerClient = new RemoteTransformerClient(name, baseUrl, httpClientConfig);
remoteTransformerClient.setStartupRetryPeriodSeconds(startupRetryPeriodSeconds);
checkAvailability();

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2019 - 2022 Alfresco Software Limited
* Copyright (C) 2019 - 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
@@ -35,6 +35,8 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.alfresco.httpclient.HttpClient4Factory;
import org.alfresco.httpclient.HttpClientConfig;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.transform.config.CoreFunction;
import org.alfresco.transform.config.TransformOptionGroup;
@@ -77,6 +79,17 @@ public class LocalTransformServiceRegistry extends TransformServiceRegistryImpl
private boolean strictMimeTypeCheck;
private Map<String, Set<String>> strictMimetypeExceptions;
private boolean retryTransformOnDifferentMimeType;
private HttpClientConfig httpClientConfig;
public HttpClientConfig getHttpClientConfig()
{
return httpClientConfig;
}
public void setHttpClientConfig(HttpClientConfig httpClientConfig)
{
this.httpClientConfig = httpClientConfig;
}
public void setPipelineConfigDir(String pipelineConfigDir)
{
@@ -134,7 +147,7 @@ public class LocalTransformServiceRegistry extends TransformServiceRegistryImpl
@Override
public boolean readConfig() throws IOException
{
CombinedConfig combinedConfig = new CombinedConfig(getLog(), this);
CombinedConfig combinedConfig = new CombinedConfig(getLog(), this, httpClientConfig);
List<String> urls = getTEngineUrlsSortedByName();
boolean successReadingConfig = combinedConfig.addRemoteConfig(urls, "T-Engine");
successReadingConfig &= combinedConfig.addLocalConfig("alfresco/transforms");
@@ -188,7 +201,8 @@ public class LocalTransformServiceRegistry extends TransformServiceRegistryImpl
int startupRetryPeriodSeconds = getStartupRetryPeriodSeconds(name);
localTransform = new LocalTransformImpl(name, transformerDebug, mimetypeService,
strictMimeTypeCheck, strictMimetypeExceptions, retryTransformOnDifferentMimeType,
transformsTransformOptions, this, baseUrl, startupRetryPeriodSeconds);
transformsTransformOptions, this, baseUrl, httpClientConfig,
startupRetryPeriodSeconds);
}
else if (isPipeline)
{

View File

@@ -30,6 +30,8 @@ import java.io.InputStream;
import java.util.StringJoiner;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.httpclient.HttpClient4Factory;
import org.alfresco.httpclient.HttpClientConfig;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.Pair;
@@ -57,6 +59,8 @@ import org.apache.http.util.EntityUtils;
*/
public class RemoteTransformerClient
{
private final HttpClientConfig httpClientConfig;
private final String name;
private final String baseUrl;
@@ -70,10 +74,11 @@ public class RemoteTransformerClient
// Only changed once on success. This is stored so it can always be returned.
private Pair<Boolean, String> checkResult = new Pair<>(null, null);
public RemoteTransformerClient(String name, String baseUrl)
public RemoteTransformerClient(String name, String baseUrl, HttpClientConfig httpClientConfig)
{
this.name = name;
this.baseUrl = baseUrl == null || baseUrl.trim().isEmpty() ? null : baseUrl.trim();
this.httpClientConfig = httpClientConfig;
}
public void setStartupRetryPeriodSeconds(int startupRetryPeriodSeconds)
@@ -129,7 +134,7 @@ public class RemoteTransformerClient
try
{
try (CloseableHttpClient httpclient = HttpClients.createDefault())
try (CloseableHttpClient httpclient = HttpClient4Factory.createHttpClient(httpClientConfig))
{
try (CloseableHttpResponse response = execute(httpclient, httppost))
{
@@ -232,7 +237,7 @@ public class RemoteTransformerClient
try
{
try (CloseableHttpClient httpclient = HttpClients.createDefault())
try (CloseableHttpClient httpclient = HttpClient4Factory.createHttpClient(httpClientConfig))
{
try (CloseableHttpResponse response = execute(httpclient, httpGet))
{

View File

@@ -504,6 +504,21 @@ public class ResetPasswordServiceImpl implements ResetPasswordService
return UrlUtil.replaceShareUrlPlaceholder(url, sysAdminParams);
}
private String getRepoBaseUrl(String url, String propName)
{
if (url == null)
{
LOGGER.warn("The url for the property [" + propName + "] is not configured.");
return "";
}
if (url.endsWith("/"))
{
url = url.substring(0, url.length() - 1);
}
return UrlUtil.replaceRepoBaseUrlPlaceholder(url, sysAdminParams);
}
protected String getResetPasswordEmailTemplate(ClientApp clientApp)
{
return clientApp.getProperty("requestResetPasswordTemplatePath");
@@ -522,7 +537,16 @@ public class ResetPasswordServiceImpl implements ResetPasswordService
StringBuilder sb = new StringBuilder(100);
String pageUrl = clientApp.getProperty("resetPasswordPageUrl");
if (StringUtils.isEmpty(pageUrl))
if(!StringUtils.isEmpty(clientApp.getProperty("workspaceUrl")))
{
String workspaceUrlPlaceholder = clientApp.getProperty("workspaceUrl");
String workSpaceUrl = getRepoBaseUrl(workspaceUrlPlaceholder,"");
sb.append(UrlUtil.replaceWorkSpaceUrlPlaceholder(pageUrl,workSpaceUrl));
LOGGER.warn("Client Name is " + clientApp.getName() + " The url used is " + sb.toString());
}
else if(StringUtils.isEmpty(pageUrl))
{
sb.append(UrlUtil.getShareUrl(sysAdminParams));
@@ -535,7 +559,7 @@ public class ResetPasswordServiceImpl implements ResetPasswordService
sb.append(getUrl(pageUrl, ""));
}
sb.append("?key=").append(key)
sb.append("?key=").append(key)
.append("&id=").append(BPMEngineRegistry.createGlobalId(ActivitiConstants.ENGINE_ID, id));
return sb.toString();

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

@@ -28,32 +28,33 @@ package org.alfresco.repo.security.authentication.identityservice;
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.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* Authenticates a user against Identity Service (Keycloak).
* {@link OAuth2Client} is used to verify provided user credentials. User is set as the current user if the user
* credentials are valid.
* 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 {@link IdentityServiceAuthenticationComponent#oAuth2Client} 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 Authorization Server **/
private OAuth2Client oAuth2Client;
private IdentityServiceFacade identityServiceFacade;
/** enabled flag for the identity service subsystem**/
private boolean active;
private boolean allowGuestLogin;
public void setOAuth2Client(OAuth2Client oAuth2Client)
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
{
this.oAuth2Client = oAuth2Client;
this.identityServiceFacade = identityServiceFacade;
}
public void setAllowGuestLogin(boolean allowGuestLogin)
@@ -63,26 +64,25 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
{
if (oAuth2Client == null)
if (identityServiceFacade == null)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("OAuth2Client 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 OAuth2Client was not set.");
throw new AuthenticationException("User not authenticated because IdentityServiceFacade was not set.");
}
try
{
// Attempt to verify user credentials
oAuth2Client.verifyCredentials(userName, new String(password));
identityServiceFacade.authorize(AuthorizationGrant.password(userName, new String(password)));
// Verification was successful so treat as authenticated user
setCurrentUser(userName);
}
catch (CredentialsVerificationException e)
catch (IdentityServiceFacadeException e)
{
throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", e);
}
@@ -108,32 +108,4 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
{
return allowGuestLogin;
}
/**
* An abstraction for acting as an OAuth2 Client
*/
interface OAuth2Client
{
/**
* 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);
class CredentialsVerificationException extends RuntimeException
{
CredentialsVerificationException(String message)
{
super(message);
}
CredentialsVerificationException(String message, Throwable cause)
{
super(message, cause);
}
}
}
}

View File

@@ -25,14 +25,9 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.TreeMap;
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;
@@ -41,19 +36,31 @@ import org.springframework.web.util.UriComponentsBuilder;
*
* @author Gavin Cornwell
*/
public class IdentityServiceConfig extends AdapterConfig implements InitializingBean
public class IdentityServiceConfig implements InitializingBean
{
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";
private Properties globalProperties;
private int clientConnectionTimeout;
private int clientSocketTimeout;
// client id
private String resource;
private String clientSecret;
private String authServerUrl;
private String realm;
private int connectionPoolSize;
private boolean allowAnyHostname;
private boolean disableTrustManager;
private String truststore;
private String truststorePassword;
private String clientKeystore;
private String clientKeystorePassword;
private String clientKeyPassword;
private String realmKey;
private int publicKeyCacheTtl;
public void setGlobalProperties(Properties globalProperties)
{
this.globalProperties = globalProperties;
@@ -94,52 +101,154 @@ public class IdentityServiceConfig extends AdapterConfig implements Initializing
{
this.clientSocketTimeout = clientSocketTimeout;
}
public void setConnectionPoolSize(int connectionPoolSize)
{
this.connectionPoolSize = connectionPoolSize;
}
public int getConnectionPoolSize()
{
return connectionPoolSize;
}
@Override
public void afterPropertiesSet() throws Exception
{
// 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);
}
String provider = this.globalProperties.getProperty(CREDENTIALS_PROVIDER);
if (provider != null && !provider.isEmpty())
{
credentials.put("provider", provider);
}
// TODO: add support for redirect-rewrite-rules and policy-enforcer if and when we need to support it
if (!credentials.isEmpty())
{
this.setCredentials(credentials);
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Created credentials map from config: " + credentials);
}
}
clientSecret = this.globalProperties.getProperty(CREDENTIALS_SECRET);
}
String getIssuerUrl()
public String getAuthServerUrl()
{
return UriComponentsBuilder.fromUriString(getAuthServerUrl())
.pathSegment(REALMS, getRealm())
.build()
.toString();
return authServerUrl;
}
public void setAuthServerUrl(String authServerUrl)
{
this.authServerUrl = authServerUrl;
}
public String getRealm()
{
return realm;
}
public void setRealm(String realm)
{
this.realm = realm;
}
public String getResource()
{
return resource;
}
public void setResource(String resource)
{
this.resource = resource;
}
public String getClientSecret()
{
return Optional.ofNullable(getCredentials())
.map(c -> c.get(SECRET))
.filter(String.class::isInstance)
.map(String.class::cast)
.orElse("");
return Optional.ofNullable(clientSecret)
.orElse("");
}
public String getIssuerUrl()
{
return UriComponentsBuilder.fromUriString(getAuthServerUrl())
.pathSegment(REALMS, getRealm())
.build()
.toString();
}
public void setAllowAnyHostname(boolean allowAnyHostname)
{
this.allowAnyHostname = allowAnyHostname;
}
public boolean isAllowAnyHostname()
{
return allowAnyHostname;
}
public void setDisableTrustManager(boolean disableTrustManager)
{
this.disableTrustManager = disableTrustManager;
}
public boolean isDisableTrustManager()
{
return disableTrustManager;
}
public void setTruststore(String truststore)
{
this.truststore = truststore;
}
public String getTruststore()
{
return truststore;
}
public void setTruststorePassword(String truststorePassword)
{
this.truststorePassword = truststorePassword;
}
public String getTruststorePassword()
{
return truststorePassword;
}
public void setClientKeystore(String clientKeystore)
{
this.clientKeystore = clientKeystore;
}
public String getClientKeystore()
{
return clientKeystore;
}
public void setClientKeystorePassword(String clientKeystorePassword)
{
this.clientKeystorePassword = clientKeystorePassword;
}
public String getClientKeystorePassword()
{
return clientKeystorePassword;
}
public void setClientKeyPassword(String clientKeyPassword)
{
this.clientKeyPassword = clientKeyPassword;
}
public String getClientKeyPassword()
{
return clientKeyPassword;
}
public void setRealmKey(String realmKey)
{
this.realmKey = realmKey;
}
public String getRealmKey()
{
return realmKey;
}
public void setPublicKeyCacheTtl(int publicKeyCacheTtl)
{
this.publicKeyCacheTtl = publicKeyCacheTtl;
}
public int getPublicKeyCacheTtl()
{
return publicKeyCacheTtl;
}
}

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,212 @@
/*
* #%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.nonNull;
import static java.util.Objects.requireNonNull;
import java.time.Instant;
import java.util.Objects;
/**
* Allows to interact with the Identity Service
*/
public interface IdentityServiceFacade
{
/**
* Returns {@link AccessToken} based authorization for provided {@link AuthorizationGrant}.
* @param grant the OAuth2 grant provided by the Resource Owner.
* @return {@link AccessTokenAuthorization} containing access token and optional refresh token.
* @throws {@link AuthorizationException} when provided grant cannot be exchanged for the access token.
*/
AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException;
/**
* Decodes the access token into the {@link DecodedAccessToken} which contains claims connected with a given token.
* @param token {@link String} with encoded access token value.
* @return {@link DecodedAccessToken} containing decoded claims.
* @throws {@link TokenDecodingException} when token decoding failed.
*/
DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
class IdentityServiceFacadeException extends RuntimeException
{
public IdentityServiceFacadeException(String message)
{
super(message);
}
IdentityServiceFacadeException(String message, Throwable cause)
{
super(message, cause);
}
}
class AuthorizationException extends IdentityServiceFacadeException
{
AuthorizationException(String message)
{
super(message);
}
AuthorizationException(String message, Throwable cause)
{
super(message, cause);
}
}
class TokenDecodingException extends IdentityServiceFacadeException
{
TokenDecodingException(String message)
{
super(message);
}
TokenDecodingException(String message, Throwable cause)
{
super(message, cause);
}
}
/**
* Represents access token authorization with optional refresh token.
*/
interface AccessTokenAuthorization
{
/**
* Required {@link AccessToken}
* @return {@link AccessToken}
*/
AccessToken getAccessToken();
/**
* Optional refresh token.
* @return Refresh token or {@code null}
*/
String getRefreshTokenValue();
}
interface AccessToken {
String getTokenValue();
Instant getExpiresAt();
}
interface DecodedAccessToken extends AccessToken
{
Object getClaim(String claim);
}
class AuthorizationGrant {
private final String username;
private final String password;
private final String refreshToken;
private final String authorizationCode;
private final String redirectUri;
private AuthorizationGrant(String username, String password, String refreshToken, String authorizationCode, String redirectUri)
{
this.username = username;
this.password = password;
this.refreshToken = refreshToken;
this.authorizationCode = authorizationCode;
this.redirectUri = redirectUri;
}
public static AuthorizationGrant password(String username, String password)
{
return new AuthorizationGrant(requireNonNull(username), requireNonNull(password), null, null, null);
}
public static AuthorizationGrant refreshToken(String refreshToken)
{
return new AuthorizationGrant(null, null, requireNonNull(refreshToken), null, null);
}
public static AuthorizationGrant authorizationCode(String authorizationCode, String redirectUri)
{
return new AuthorizationGrant(null, null, null, requireNonNull(authorizationCode), requireNonNull(redirectUri));
}
boolean isPassword()
{
return nonNull(username);
}
boolean isRefreshToken()
{
return nonNull(refreshToken);
}
boolean isAuthorizationCode()
{
return nonNull(authorizationCode);
}
String getUsername()
{
return username;
}
String getPassword()
{
return password;
}
String getRefreshToken()
{
return refreshToken;
}
String getAuthorizationCode()
{
return authorizationCode;
}
String getRedirectUri()
{
return redirectUri;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AuthorizationGrant that = (AuthorizationGrant) o;
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&
Objects.equals(refreshToken, that.refreshToken) &&
Objects.equals(authorizationCode, that.authorizationCode) &&
Objects.equals(redirectUri, that.redirectUri);
}
@Override
public int hashCode()
{
return Objects.hash(username, password, refreshToken, authorizationCode, redirectUri);
}
}
}

View File

@@ -0,0 +1,543 @@
/*
* #%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 static java.util.function.Predicate.not;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.ResourceRetriever;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.converter.RsaKeyConverters;
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.ClientRegistration.Builder;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
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.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
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.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
*
* 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(
new HttpClientProvider(identityServiceConfig)::createHttpClient,
new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder
);
}
@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 Identity Service 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 AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException
{
return getTargetFacade().authorize(grant);
}
@Override
public DecodedAccessToken decodeToken(String token) throws TokenDecodingException
{
return getTargetFacade().decodeToken(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 final Supplier<HttpClient> httpClientProvider;
private final Function<RestOperations, ClientRegistration> clientRegistrationProvider;
private final BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider;
SpringBasedIdentityServiceFacadeFactory(
Supplier<HttpClient> httpClientProvider,
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
{
this.httpClientProvider = Objects.requireNonNull(httpClientProvider);
this.clientRegistrationProvider = Objects.requireNonNull(clientRegistrationProvider);
this.jwtDecoderProvider = Objects.requireNonNull(jwtDecoderProvider);
}
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
final ClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClientProvider.get());
final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate, clientRegistration.getProviderDetails());
return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory), clientRegistration, jwtDecoder);
}
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
{
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
return restTemplate;
}
}
private static class HttpClientProvider
{
private final IdentityServiceConfig config;
private HttpClientProvider(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
}
private HttpClient createHttpClient()
{
try
{
HttpClientBuilder clientBuilder = HttpClients.custom();
applyConnectionConfiguration(clientBuilder);
applySSLConfiguration(clientBuilder);
return clientBuilder.build();
}
catch (Exception e)
{
throw new IllegalStateException("Failed to create ClientHttpRequestFactory. " + e.getMessage(), e);
}
}
private void applyConnectionConfiguration(HttpClientBuilder builder)
{
final RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(config.getClientConnectionTimeout())
.setSocketTimeout(config.getClientSocketTimeout())
.build();
builder.setDefaultRequestConfig(requestConfig)
.setMaxConnTotal(config.getConnectionPoolSize());
}
private void applySSLConfiguration(HttpClientBuilder builder) throws Exception
{
SSLContextBuilder sslContextBuilder = null;
if (config.isDisableTrustManager())
{
sslContextBuilder = SSLContexts.custom()
.loadTrustMaterial(TrustAllStrategy.INSTANCE);
}
else if (isDefined(config.getTruststore()))
{
final char[] truststorePassword = asCharArray(config.getTruststorePassword(), null);
sslContextBuilder = SSLContexts.custom()
.loadTrustMaterial(new File(config.getTruststore()), truststorePassword);
}
if (isDefined(config.getClientKeystore()))
{
if (sslContextBuilder == null)
{
sslContextBuilder = SSLContexts.custom();
}
final char[] keystorePassword = asCharArray(config.getClientKeystorePassword(), null);
final char[] keyPassword = asCharArray(config.getClientKeyPassword(), keystorePassword);
sslContextBuilder.loadKeyMaterial(new File(config.getClientKeystore()), keystorePassword, keyPassword);
}
if (sslContextBuilder != null)
{
builder.setSSLContext(sslContextBuilder.build());
}
if (config.isDisableTrustManager() || config.isAllowAnyHostname())
{
builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
}
}
private char[] asCharArray(String value, char[] nullValue)
{
return Optional.ofNullable(value)
.filter(not(String::isBlank))
.map(String::toCharArray)
.orElse(nullValue);
}
}
private static class ClientRegistrationProvider
{
private final IdentityServiceConfig config;
private ClientRegistrationProvider(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
}
public ClientRegistration createClientRegistration(final RestOperations rest)
{
return possibleMetadataURIs()
.stream()
.map(u -> extractMetadata(rest, u))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.map(this::createBuilder)
.map(Builder::build)
.orElseThrow(() -> new IllegalStateException("Failed to create ClientRegistration."));
}
private ClientRegistration.Builder createBuilder(OIDCProviderMetadata metadata)
{
return ClientRegistration
.withRegistrationId("ids")
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.issuerUri(config.getIssuerUrl())
.clientId(config.getResource())
.clientSecret(config.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
private Optional<OIDCProviderMetadata> extractMetadata(RestOperations rest, URI metadataUri)
{
final String response;
try
{
final ResponseEntity<String> r = rest.exchange(RequestEntity.get(metadataUri).build(), String.class);
if (r.getStatusCode() != HttpStatus.OK || !r.hasBody())
{
LOGGER.warn("Unexpected response from " + metadataUri + ". Status code: " + r.getStatusCode() + ", has body: " + r.hasBody() + ".");
return Optional.empty();
}
response = r.getBody();
}
catch (Exception e)
{
LOGGER.warn("Failed to get response from " + metadataUri + ". " + e.getMessage(), e);
return Optional.empty();
}
try
{
return Optional.of(OIDCProviderMetadata.parse(response));
}
catch (Exception e)
{
LOGGER.warn("Failed to parse metadata. " + e.getMessage(), e);
return Optional.empty();
}
}
private Collection<URI> possibleMetadataURIs()
{
return List.of(UriComponentsBuilder.fromUriString(config.getIssuerUrl())
.pathSegment(".well-known", "openid-configuration")
.build().toUri());
}
}
static class JwtDecoderProvider
{
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
private final IdentityServiceConfig config;
JwtDecoderProvider(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
}
public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
{
try
{
final NimbusJwtDecoder decoder = buildJwtDecoder(rest, providerDetails);
decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
decoder.setClaimSetConverter(new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
return decoder;
} catch (RuntimeException e)
{
LOGGER.warn("Failed to create JwtDecoder.", e);
throw authorizationServerCantBeUsedException(e);
}
}
private NimbusJwtDecoder buildJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
{
if (isDefined(config.getRealmKey()))
{
final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey());
return NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SIGNATURE_ALGORITHM)
.build();
}
final String jwkSetUri = requireValidJwkSetUri(providerDetails);
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.jwsAlgorithm(SIGNATURE_ALGORITHM)
.restOperations(rest)
.jwtProcessorCustomizer(this::reconfigureJWKSCache)
.build();
}
private void reconfigureJWKSCache(ConfigurableJWTProcessor<SecurityContext> jwtProcessor)
{
final Optional<RemoteJWKSet<SecurityContext>> jwkSource = ofNullable(jwtProcessor)
.map(ConfigurableJWTProcessor::getJWSKeySelector)
.filter(JWSVerificationKeySelector.class::isInstance).map(o -> (JWSVerificationKeySelector<SecurityContext>)o)
.map(JWSVerificationKeySelector::getJWKSource)
.filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet<SecurityContext>)o);
if (jwkSource.isEmpty())
{
LOGGER.warn("Not able to reconfigure the JWK Cache. Unexpected JWKSource.");
return;
}
final Optional<URL> jwkSetUrl = jwkSource.map(RemoteJWKSet::getJWKSetURL);
if (jwkSetUrl.isEmpty())
{
LOGGER.warn("Not able to reconfigure the JWK Cache. Unknown JWKSetURL.");
return;
}
final Optional<ResourceRetriever> resourceRetriever = jwkSource.map(RemoteJWKSet::getResourceRetriever);
if (resourceRetriever.isEmpty())
{
LOGGER.warn("Not able to reconfigure the JWK Cache. Unknown ResourceRetriever.");
return;
}
final DefaultJWKSetCache cache = new DefaultJWKSetCache(config.getPublicKeyCacheTtl(), -1, TimeUnit.SECONDS);
final JWKSource<SecurityContext> cachingJWKSource = new RemoteJWKSet<>(jwkSetUrl.get(), resourceRetriever.get(), cache);
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
JWSAlgorithm.parse(SIGNATURE_ALGORITHM.getName()),
cachingJWKSource));
}
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
{
return new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)),
new JwtIssuerValidator(providerDetails.getIssuerUri()),
new JwtClaimValidator<String>("typ", "Bearer"::equals),
new JwtClaimValidator<String>(JwtClaimNames.SUB, Objects::nonNull));
}
private RSAPublicKey parsePublicKey(String pem)
{
try
{
return tryToParsePublicKey(pem);
}
catch (Exception e)
{
if (isPemFormatException(e))
{
//For backward compatibility with Keycloak adapter
return tryToParsePublicKey("-----BEGIN PUBLIC KEY-----\n" + pem + "\n-----END PUBLIC KEY-----");
}
throw e;
}
}
private RSAPublicKey tryToParsePublicKey(String pem)
{
final InputStream pemStream = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8));
return RsaKeyConverters.x509().convert(pemStream);
}
private boolean isPemFormatException(Exception e)
{
return e.getMessage() != null && e.getMessage().contains("-----BEGIN PUBLIC KEY-----");
}
private String requireValidJwkSetUri(ProviderDetails providerDetails)
{
final String uri = providerDetails.getJwkSetUri();
if (!isDefined(uri)) {
OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
"Failed to find a Signature Verifier for: '"
+ providerDetails.getIssuerUri()
+ "'. Check to ensure you have configured the JwkSet URI.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return uri;
}
}
private static boolean isDefined(String value)
{
return value != null && !value.isBlank();
}
}

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.IdentityServiceFacadeException;
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,8 @@ 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);
static final String USERNAME_CLAIM = "preferred_username";
/** Is the mapper enabled */
private boolean isEnabled;
@@ -57,9 +60,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 +94,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 (IdentityServiceFacadeException 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;
}
@@ -159,61 +161,40 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
* Extracts the user name from the JWT in the given request.
*
* @param request The request containing the JWT
* @return The user name or null if it can not be determined
* @return The username or null if it can not be determined
*/
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)
.map(identityServiceFacade::decodeToken)
.map(t -> t.getClaim(USERNAME_CLAIM))
.filter(String.class::isInstance)
.map(String.class::cast);
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 +219,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

@@ -1,270 +0,0 @@
/*
* #%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.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client;
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.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.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
*
* Creates an instance of {@link OAuth2Client}. <br>
* The creation of {@link OAuth2Client} requires connection to the Identity Service (Keycloak), disable this factory if
* the server cannot be reached. <br>
* This factory can return a null if it is disabled.
*
*/
public class OAuth2ClientFactoryBean implements FactoryBean<OAuth2Client>
{
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientFactoryBean.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 OAuth2Client 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;
}
// The OAuth2AuthorizedClientManager isn't created upfront to make the code resilient to Identity Service being down.
// If it's down the Application Context will start and when it's back online it can be used.
return new SpringOAuth2Client(this::createOAuth2AuthorizedClientManager);
}
private OAuth2AuthorizedClientManager createOAuth2AuthorizedClientManager()
{
//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 ClientRegistration clientRegistration = ClientRegistrations
.fromIssuerLocation(identityServiceConfig.getIssuerUrl())
.clientId(identityServiceConfig.getResource())
.clientSecret(identityServiceConfig.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.registrationId(SpringOAuth2Client.CLIENT_REGISTRATION_ID)
.build();
final AuthorizedClientServiceOAuth2AuthorizedClientManager oauth2 =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
new SingleClientRegistration(clientRegistration),
new NoStoredAuthorizedClient());
oauth2.setContextAttributesMapper(OAuth2AuthorizeRequest::getAttributes);
oauth2.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder()
.password(this::configureTimeouts)
.build());
if (LOGGER.isDebugEnabled())
{
LOGGER.debug(" Created OAuth2 Client");
LOGGER.debug(" OAuth2 Issuer URL: " + clientRegistration.getProviderDetails().getIssuerUri());
LOGGER.debug(" OAuth2 ClientId: " + clientRegistration.getClientId());
}
return oauth2;
}
private void configureTimeouts(PasswordGrantBuilder builder)
{
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(identityServiceConfig.getClientConnectionTimeout());
requestFactory.setReadTimeout(identityServiceConfig.getClientSocketTimeout());
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
client.setRestOperations(restTemplate);
builder.accessTokenResponseClient(client);
}
@Override
public Class<?> getObjectType()
{
return OAuth2Client.class;
}
@Override
public boolean isSingleton()
{
return true;
}
static class SpringOAuth2Client implements OAuth2Client
{
private static final String CLIENT_REGISTRATION_ID = "ids";
private final Supplier<OAuth2AuthorizedClientManager> authorizedClientManagerSupplier;
private final AtomicReference<OAuth2AuthorizedClientManager> authorizedClientManager = new AtomicReference<>();
public SpringOAuth2Client(Supplier<OAuth2AuthorizedClientManager> authorizedClientManagerSupplier)
{
this.authorizedClientManagerSupplier = Objects.requireNonNull(authorizedClientManagerSupplier);
}
@Override
public void verifyCredentials(String userName, String password)
{
final OAuth2AuthorizedClientManager clientManager;
try
{
clientManager = getAuthorizedClientManager();
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to instantiate OAuth2AuthorizedClientManager.", e);
throw new CredentialsVerificationException("Unable to use the Authorization Server.", e);
}
final OAuth2AuthorizedClient authorizedClient;
try
{
final OAuth2AuthorizeRequest authRequest = createPasswordCredentialsRequest(userName, password);
authorizedClient = clientManager.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.");
}
}
private OAuth2AuthorizedClientManager getAuthorizedClientManager()
{
final OAuth2AuthorizedClientManager current = authorizedClientManager.get();
if (current != null)
{
return current;
}
return authorizedClientManager
.updateAndGet(prev -> prev != null ? prev : authorizedClientManagerSupplier.get());
}
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();
}
}
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 = Objects.requireNonNull(clientRegistration);
}
@Override
public ClientRegistration findByRegistrationId(String registrationId)
{
return Objects.equals(registrationId, clientRegistration.getRegistrationId()) ? clientRegistration : null;
}
}
}

View File

@@ -0,0 +1,255 @@
/*
* #%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 java.time.Instant;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.web.client.RestOperations;
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
{
private static final Log LOGGER = LogFactory.getLog(SpringBasedIdentityServiceFacade.class);
private static final Instant SOME_INSIGNIFICANT_DATE_IN_THE_PAST = Instant.MIN.plusSeconds(12345);
private final Map<AuthorizationGrantType, OAuth2AccessTokenResponseClient> clients;
private final ClientRegistration clientRegistration;
private final JwtDecoder jwtDecoder;
SpringBasedIdentityServiceFacade(RestOperations restOperations, ClientRegistration clientRegistration, JwtDecoder jwtDecoder)
{
requireNonNull(restOperations);
this.clientRegistration = requireNonNull(clientRegistration);
this.jwtDecoder = requireNonNull(jwtDecoder);
this.clients = Map.of(
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations));
}
@Override
public AccessTokenAuthorization authorize(AuthorizationGrant authorizationGrant)
{
final AbstractOAuth2AuthorizationGrantRequest request = createRequest(authorizationGrant);
final OAuth2AccessTokenResponseClient client = getClient(request);
final OAuth2AccessTokenResponse response;
try
{
response = client.getTokenResponse(request);
}
catch (OAuth2AuthorizationException e)
{
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
throw new AuthorizationException("Failed to obtain access token. " + e.getError(), e);
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
throw new AuthorizationException("Failed to obtain access token.", e);
}
return new SpringAccessTokenAuthorization(response);
}
@Override
public DecodedAccessToken decodeToken(String token)
{
final Jwt validToken;
try
{
validToken = jwtDecoder.decode(token);
}
catch (RuntimeException e)
{
throw new TokenDecodingException("Failed to decode token. " + e.getMessage(), e);
}
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Bearer token outcome: " + validToken.getClaims());
}
return new SpringDecodedAccessToken(validToken);
}
private AbstractOAuth2AuthorizationGrantRequest createRequest(AuthorizationGrant grant)
{
if (grant.isPassword())
{
return new OAuth2PasswordGrantRequest(clientRegistration, grant.getUsername(), grant.getPassword());
}
if (grant.isRefreshToken())
{
final OAuth2AccessToken expiredAccessToken = new OAuth2AccessToken(
TokenType.BEARER,
"JUST_FOR_FULFILLING_THE_SPRING_API",
SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken, clientRegistration.getScopes());
}
if (grant.isAuthorizationCode())
{
final OAuth2AuthorizationExchange authzExchange = new OAuth2AuthorizationExchange(
OAuth2AuthorizationRequest.authorizationCode()
.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(grant.getRedirectUri())
.scopes(clientRegistration.getScopes())
.build(),
OAuth2AuthorizationResponse.success(grant.getAuthorizationCode())
.redirectUri(grant.getRedirectUri())
.build()
);
return new OAuth2AuthorizationCodeGrantRequest(clientRegistration, authzExchange);
}
throw new UnsupportedOperationException("Unsupported grant type.");
}
private OAuth2AccessTokenResponseClient getClient(AbstractOAuth2AuthorizationGrantRequest request)
{
final AuthorizationGrantType grantType = request.getGrantType();
final OAuth2AccessTokenResponseClient client = clients.get(grantType);
if (client == null)
{
throw new UnsupportedOperationException("Unsupported grant type `" + grantType + "`.");
}
return client;
}
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createAuthorizationCodeClient(RestOperations rest)
{
final DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
client.setRestOperations(rest);
return client;
}
private static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> createRefreshTokenClient(RestOperations rest)
{
final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient();
client.setRestOperations(rest);
return client;
}
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest)
{
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
client.setRestOperations(rest);
return client;
}
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
{
private final OAuth2AccessTokenResponse tokenResponse;
private SpringAccessTokenAuthorization(OAuth2AccessTokenResponse tokenResponse)
{
this.tokenResponse = requireNonNull(tokenResponse);
}
@Override
public AccessToken getAccessToken()
{
return new SpringAccessToken(tokenResponse.getAccessToken());
}
@Override
public String getRefreshTokenValue()
{
return Optional.of(tokenResponse)
.map(OAuth2AccessTokenResponse::getRefreshToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
}
}
private static class SpringAccessToken implements AccessToken
{
private final AbstractOAuth2Token token;
private SpringAccessToken(AbstractOAuth2Token token)
{
this.token = requireNonNull(token);
}
@Override
public String getTokenValue()
{
return token.getTokenValue();
}
@Override
public Instant getExpiresAt()
{
return token.getExpiresAt();
}
}
private static class SpringDecodedAccessToken extends SpringAccessToken implements DecodedAccessToken
{
private final Jwt jwt;
private SpringDecodedAccessToken(Jwt jwt)
{
super(jwt);
this.jwt = jwt;
}
@Override
public Object getClaim(String claim)
{
return jwt.getClaim(claim);
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 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
@@ -28,6 +28,8 @@ package org.alfresco.transform.registry;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.httpclient.HttpClient4Factory;
import org.alfresco.httpclient.HttpClientConfig;
import org.alfresco.repo.content.transform.LocalPassThroughTransform;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.transform.config.TransformConfig;
@@ -39,7 +41,6 @@ import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
@@ -67,8 +68,11 @@ public class CombinedConfig extends CombinedTransformConfig
private ConfigFileFinder configFileFinder;
private int tEngineCount;
public CombinedConfig(Log log, AbstractTransformRegistry registry)
private final HttpClientConfig httpClientConfig;
public CombinedConfig(Log log, AbstractTransformRegistry registry, HttpClientConfig httpClientConfig)
{
this.httpClientConfig = httpClientConfig;
this.log = log;
configFileFinder = new ConfigFileFinder(jsonObjectMapper)
@@ -87,88 +91,84 @@ public class CombinedConfig extends CombinedTransformConfig
return configFileFinder.readFiles(path, log);
}
public boolean addRemoteConfig(List<String> urls, String remoteType)
public boolean addRemoteConfig(List<String> urls, String remoteType) throws IOException
{
boolean successReadingConfig = true;
for (String url : urls)
try(CloseableHttpClient httpclient = HttpClient4Factory.createHttpClient(httpClientConfig))
{
if (addRemoteConfig(url, remoteType))
boolean successReadingConfig = true;
for (String url : urls)
{
tEngineCount++ ;
}
else
{
successReadingConfig = false;
if (addRemoteConfig(httpclient, url, remoteType))
{
tEngineCount++;
} else
{
successReadingConfig = false;
}
}
return successReadingConfig;
}
return successReadingConfig;
}
private boolean addRemoteConfig(String baseUrl, String remoteType)
private boolean addRemoteConfig(CloseableHttpClient httpclient, String baseUrl, String remoteType)
{
String url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + ENDPOINT_TRANSFORM_CONFIG_LATEST;
HttpGet httpGet = new HttpGet(url);
boolean successReadingConfig = true;
try
{
try (CloseableHttpClient httpclient = HttpClients.createDefault())
try (CloseableHttpResponse response = execute(httpclient, httpGet))
{
try (CloseableHttpResponse response = execute(httpclient, httpGet))
StatusLine statusLine = response.getStatusLine();
if (statusLine == null)
{
StatusLine statusLine = response.getStatusLine();
if (statusLine == null)
throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned no status ");
}
HttpEntity resEntity = response.getEntity();
if (resEntity != null)
{
int statusCode = statusLine.getStatusCode();
if (statusCode == 200)
{
throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned no status ");
}
HttpEntity resEntity = response.getEntity();
if (resEntity != null)
{
int statusCode = statusLine.getStatusCode();
if (statusCode == 200)
try
{
try
String content = getContent(resEntity);
try (StringReader reader = new StringReader(content))
{
String content = getContent(resEntity);
try (StringReader reader = new StringReader(content))
int transformCount = transformerCount();
configFileFinder.readFile(reader, remoteType+" on "+baseUrl, "json", baseUrl, log);
if (transformCount == transformerCount())
{
int transformCount = transformerCount();
configFileFinder.readFile(reader, remoteType+" on "+baseUrl, "json", baseUrl, log);
if (transformCount == transformerCount())
{
successReadingConfig = false;
}
successReadingConfig = false;
}
}
EntityUtils.consume(resEntity);
}
catch (IOException e)
{
throw new AlfrescoRuntimeException("Failed to read the returned content from "+
remoteType+" on " + url, e);
}
EntityUtils.consume(resEntity);
}
else
catch (IOException e)
{
String message = getErrorMessage(resEntity);
throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned a " + statusCode +
" status " + message);
throw new AlfrescoRuntimeException("Failed to read the returned content from "+
remoteType+" on " + url, e);
}
}
else
{
throw new AlfrescoRuntimeException(remoteType+" on " + url+" did not return an entity " + url);
String message = getErrorMessage(resEntity);
throw new AlfrescoRuntimeException(remoteType+" on " + url+" returned a " + statusCode +
" status " + message);
}
}
catch (IOException e)
else
{
throw new AlfrescoRuntimeException("Failed to connect or to read the response from "+remoteType+
" on " + url, e);
throw new AlfrescoRuntimeException(remoteType+" on " + url+" did not return an entity " + url);
}
}
catch (IOException e)
{
throw new AlfrescoRuntimeException(remoteType+" on " + url+" failed to create an HttpClient", e);
throw new AlfrescoRuntimeException("Failed to connect or to read the response from "+remoteType+
" on " + url, e);
}
}
catch (AlfrescoRuntimeException e)
{

View File

@@ -41,6 +41,12 @@ public class UrlUtil
public static final Pattern PATTERN = Pattern.compile("\\$\\{shareUrl\\}");
// ${alfrescoUrl} placeholder
public static final Pattern REPO_PATTERN = Pattern.compile("\\$\\{alfrescoUrl\\}");
public static final Pattern REPOBASE_PATTERN = Pattern.compile("\\$\\{repoBaseUrl\\}");
public static final Pattern WORKSPACE_PATTERN = Pattern.compile("\\$\\{workspaceUrl\\}");
/**
* Builds up the Url to Alfresco based on the settings in the
* {@link SysAdminParams}.
@@ -146,4 +152,68 @@ public class UrlUtil
url.append(context);
return url.toString();
}
/**
* Builds up the Url to Adw based on the settings in the
* {@link SysAdminParams}.
* @return Adw Url such as https://col.ab.or.ate/#/
* or http://localhost:8081/#/
*/
public static String getWorkspaceUrl(SysAdminParams sysAdminParams)
{
return buildWorkspaceUrl(
sysAdminParams.getAlfrescoProtocol(),
sysAdminParams.getAlfrescoHost(),
sysAdminParams.getAlfrescoPort());
}
protected static String buildWorkspaceUrl(String workSpaceProtocol, String workspaceHost, int workspacePort) {
StringBuilder workspaceUrl = new StringBuilder();
workspaceUrl.append(workSpaceProtocol);
workspaceUrl.append("://");
workspaceUrl.append(workspaceHost);
if ("http".equals(workSpaceProtocol) && workspacePort == 80)
{
// Not needed
}
else if ("https".equals(workSpaceProtocol) && workspacePort == 443)
{
// Not needed
}
else
{
workspaceUrl.append(':');
workspaceUrl.append(workspacePort);
}
return workspaceUrl.toString();
}
/**
* Replaces the repo base url placeholder, namely {@literal ${repoBaseUrl}}, with <b>workspace</b> url.
*
* @param value the string value which contains the repoBase url placeholder
* @param sysAdminParams the {@code SysAdminParams} object
* @return if the given {@code value} contains share url placeholder,
* the placeholder is replaced with share url; otherwise, the given {@code value} is simply returned
*/
public static String replaceRepoBaseUrlPlaceholder(String value, SysAdminParams sysAdminParams)
{
if (value != null)
{
return REPOBASE_PATTERN.matcher(value).replaceAll(getWorkspaceUrl(sysAdminParams));
}
return value;
}
public static String replaceWorkSpaceUrlPlaceholder(String pageUrl,String workspaceUrl)
{
if (pageUrl != null)
{
return WORKSPACE_PATTERN.matcher(pageUrl).replaceAll(workspaceUrl);
}
return pageUrl;
}
}

View File

@@ -20,6 +20,16 @@ repo.client-app.share.resetPasswordPageUrl=${shareUrl}/page/reset-password
repo.client-app.share.confirmResetPasswordTemplatePath=
### Digital workspace template configurations
repo.client-app.workspace.inviteModeratedTemplatePath=
repo.client-app.workspace.workspaceUrl=workspace
repo.client-app.workspace.templateAssetsUrl=${alfrescoUrl}/images
#repo.client-app.workspace.inviteModeratedTemplatePath=
repo.client-app.workspace.workspaceUrl=${repoBaseUrl}/workspace
#repo.client-app.workspace.templateAssetsUrl=${workspaceUrl}/images
repo.client-app.workspace.templateAssetsUrl=alfresco/templates/reset-password-email-templates/images
# reset password request email template path
repo.client-app.workspace.requestResetPasswordTemplatePath=alfresco/templates/reset-password-email-templates/forgot-password-email-template.ftl
# reset password UI page url
repo.client-app.workspace.resetPasswordPageUrl=${workspaceUrl}/reset-password/
# reset password confirmation email template path
repo.client-app.workspace.confirmResetPasswordTemplatePath=

View File

@@ -107,6 +107,13 @@
<!-- Replaced in the enterprise edition -->
<bean id="remoteTransformServiceRegistry" class="org.alfresco.repo.content.transform.DummyTransformServiceRegistry" />
<bean id="httpClientConfigTransform" class="org.alfresco.httpclient.HttpClientConfig" init-method="init" >
<property name="sslEncryptionParameters" ref="sslEncryptionParameters" />
<property name="keyResourceLoader" ref="springKeyResourceLoader" />
<property name="properties" ref="global-properties" />
<property name="serviceName" value="transform" />
</bean>
<bean id="localTransformServiceRegistry" class="org.alfresco.repo.content.transform.LocalTransformServiceRegistry" >
<property name="jsonObjectMapper" ref="localTransformServiceRegistryJsonObjectMapper" />
<property name="pipelineConfigDir" value="${local.transform.pipeline.config.dir}" />
@@ -118,7 +125,8 @@
<property name="mimetypeService" ref="MimetypeService" />
<property name="strictMimeTypeCheck" value="${transformer.strict.mimetype.check}"/>
<property name="retryTransformOnDifferentMimeType" value="${content.transformer.retryOn.different.mimetype}"/>
<property name="shutdownIndicator" ref="shutdownIndicator"></property>
<property name="shutdownIndicator" ref="shutdownIndicator" />
<property name="httpClientConfig" ref="httpClientConfigTransform" />
</bean>
<bean id="localTransformServiceRegistryJsonObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />

View File

@@ -749,6 +749,15 @@ encryption.ssl.truststore.type=JCEKS
# configuration via metadata is deprecated
encryption.ssl.truststore.keyMetaData.location=
## HttpClient config for Transform Service
#enable mtls in HttpClientFactory for Transform Service.
httpclient.config.transform.mTLSEnabled=false
httpclient.config.transform.maxTotalConnections=40
httpclient.config.transform.maxHostConnections=40
httpclient.config.transform.socketTimeout=5000
httpclient.config.transform.connectionRequestTimeout=5000
httpclient.config.transform.connectionTimeout=5000
# Re-encryptor properties
encryption.reencryptor.chunkSize=100
encryption.reencryptor.numThreads=2

View File

@@ -21,12 +21,12 @@
<property name="allowGuestLogin">
<value>${identity-service.authentication.allowGuestLogin}</value>
</property>
<property name="oAuth2Client">
<ref bean="oAuth2Client"/>
<property name="identityServiceFacade">
<ref bean="identityServiceFacade"/>
</property>
</bean>
<bean name="oAuth2Client" class="org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean">
<bean name="identityServiceFacade" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean">
<property name="identityServiceConfig">
<ref bean="identityServiceConfig" />
</property>
@@ -92,54 +92,12 @@
<property name="realm">
<value>${identity-service.realm}</value>
</property>
<property name="realmKey">
<value>${identity-service.realm-public-key:#{null}}</value>
</property>
<property name="authServerUrl">
<value>${identity-service.auth-server-url}</value>
</property>
<property name="sslRequired">
<value>${identity-service.ssl-required:external}</value>
</property>
<property name="confidentialPort">
<value>${identity-service.confidential-port:0}</value>
</property>
<property name="resource">
<value>${identity-service.resource}</value>
</property>
<property name="useResourceRoleMappings">
<value>${identity-service.use-resource-role-mappings:false}</value>
</property>
<property name="cors">
<value>${identity-service.enable-cors:false}</value>
</property>
<property name="corsMaxAge">
<value>${identity-service.cors-max-age:-1}</value>
</property>
<property name="corsAllowedHeaders">
<value>${identity-service.cors-allowed-headers:#{null}}</value>
</property>
<property name="corsAllowedMethods">
<value>${identity-service.cors-allowed-methods:#{null}}</value>
</property>
<property name="corsExposedHeaders">
<value>${identity-service.cors-exposed-headers:#{null}}</value>
</property>
<property name="exposeToken">
<value>${identity-service.expose-token:false}</value>
</property>
<property name="bearerOnly">
<value>${identity-service.bearer-only:false}</value>
</property>
<property name="autodetectBearerOnly">
<value>${identity-service.autodetect-bearer-only:false}</value>
</property>
<property name="enableBasicAuth">
<value>${identity-service.enable-basic-auth:false}</value>
</property>
<property name="publicClient">
<value>${identity-service.public-client:false}</value>
</property>
<property name="allowAnyHostname">
<value>${identity-service.allow-any-hostname:false}</value>
</property>
@@ -164,50 +122,17 @@
<property name="connectionPoolSize">
<value>${identity-service.connection-pool-size:20}</value>
</property>
<property name="alwaysRefreshToken">
<value>${identity-service.always-refresh-token:false}</value>
</property>
<property name="registerNodeAtStartup">
<value>${identity-service.register-node-at-startup:false}</value>
</property>
<property name="registerNodePeriod">
<value>${identity-service.register-node-period:-1}</value>
</property>
<property name="tokenStore">
<value>${identity-service.token-store:#{null}}</value>
</property>
<property name="principalAttribute">
<value>${identity-service.principal-attribute:#{null}}</value>
</property>
<property name="turnOffChangeSessionIdOnLogin">
<value>${identity-service.turn-off-change-session-id-on-login:false}</value>
</property>
<property name="tokenMinimumTimeToLive">
<value>${identity-service.token-minimum-time-to-live:0}</value>
</property>
<property name="minTimeBetweenJwksRequests">
<value>${identity-service.min-time-between-jwks-requests:10}</value>
</property>
<property name="publicKeyCacheTtl">
<value>${identity-service.public-key-cache-ttl:86400}</value>
</property>
<property name="pkce">
<value>${identity-service.enable-pkce:false}</value>
</property>
<property name="ignoreOAuthQueryParameter">
<value>${identity-service.ignore-oauth-query-parameter:false}</value>
</property>
<property name="clientConnectionTimeout">
<value>${identity-service.client-connection-timeout:2000}</value>
</property>
<property name="clientSocketTimeout">
<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 name="realmKey">
<value>${identity-service.realm-public-key:#{null}}</value>
</property>
<property name="publicKeyCacheTtl">
<value>${identity-service.public-key-cache-ttl:86400}</value>
</property>
</bean>
@@ -222,8 +147,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

@@ -2,12 +2,11 @@ identity-service.authentication.enabled=true
identity-service.authentication.validation.failure.silent=true
identity-service.authentication.defaultAdministratorUserNames=admin
identity-service.authentication.allowGuestLogin=true
# The keycloak client required to perform username/password authentication will not be created if false
# The Identity Service client required to perform username/password authentication will not be created if false
identity-service.authentication.enable-username-password-authentication=true
# Identity Service configuration
identity-service.auth-server-url=http://localhost:8180/auth
identity-service.realm=alfresco
identity-service.ssl-required=none
identity-service.resource=alfresco
identity-service.public-client=true

View File

@@ -0,0 +1,428 @@
<html>
<head>
<style type="text/css">
td {
font-family: 'Helvetica Neue', Arial, sans-serif;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% ! important;
height: 100% !important;
color: #727174;
font-weight: 400;
font-size: 18px;
margin: 0;
}
h1 {
margin: 10px 0;
}
h2 {
color: #727174;
font-weight: 600;
font-size: 22px;
}
a {
color: #0c79bf;
text-decoration: underline;
}
a.linkone {
color: #0c79bf;
text-decoration: underline;
}
.appleLinks a {
color: #0c79bf;
}
.appleLinksWhite a {
color: #0c79bf;
}
.force-full-width {
width: 100% !important;
}
.body-padding {
padding: 0 75px
}
.force-width-80 {
width: 80% !important;
}
p {
Margin-bottom: 1em;
}
.button {
text-align: center;
font-size: 18px;
font-family: sans-serif;
font-weight: 400;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
}
.button a {
color: #ffffff;
text-decoration: none;
}
img {
max-width: 600px;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
a img {
border: none;
}
table {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
#outlook a {
padding: 0;
}
.ReadMsgBody {
width: 100%;
}
.ExternalClass {
width: 100%;
}
.backgroundTable {
margin: 0 auto;
padding: 0;
width: 100% !important;
}
table td {
border-collapse: collapse;
}
.ExternalClass * {
line-height: 115%;
}
.ExternalClass {
vertical-align: middle
}
/*]]>*/
</style>
<style type="text/css" media="screen">
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { {
@-ms-viewport {
width: 320px;
}
@viewport {
width: 320px;
}
}
</style>
<style type="text/css" media="screen">
@media screen {
* {
font-family: 'Helvetica Neue', 'Arial', 'sans-serif' !important;
}
.w280 {
width: 280px !important;
}
}
</style>
<style type="text/css" media="only screen and (max-width: 480px)">
@media only screen and (max-width: 480px) {
.full {
display: block;
width: 100%;
}
table[class*="w320"] {
width: 320px !important;
}
td[class*="w320"] {
width: 280px !important;
padding-left: 20px !important;
padding-right: 20px !important;
}
img[class*="w320"] {
width: 250px !important;
height: 67px !important;
}
td[class*="mobile-spacing"] {
padding-top: 10px !important;
padding-bottom: 10px !important;
}
*[class*="mobile-hide"] {
display: none !important;
}
*[class*="mobile-br"] {
font-size: 8px !important;
}
*[class*="mobile-brh"] {
font-size: 1px !important;
}
td[class*="mobile-w20"] {
width: 20px !important;
}
img[class*="mobile-w20"] {
width: 20px !important;
}
td[class*="mobile-center"] {
text-align: center !important;
}
table[class*="w100p"] {
width: 100% !important;
}
td[class*="activate-now"] {
padding-right: 0 !important;
padding-top: 20px !important;
}
td[class*="mobile-resize"] {
font-size: 22px !important;
padding-left: 15px !important;
}
}
</style>
</head>
<body offset="0" class="body" style="padding:0; margin:0; display:block; background:#f3f4f4; -webkit-text-size-adjust:none; " bgcolor="#EEEEEE">
<table align="center" cellpadding="0" cellspacing="0" width="100%" height="100%">
<tr>
<td align="center" valign="top" style="background-color:#f3f4f4; " width="100%">
<table cellspacing="0" cellpadding="0" width="600" class="w320">
<tr>
<td align="center" valign="top">
<!--Snippet Block-->
<table border="0" cellspacing="0" cellpadding="0" align="center" width="100%">
<tr>
<td>
<table border="0" cellspacing="0" cellpadding="10" width="100%" align="center">
<tbody>
<tr>
<td align="left">
<table align="center" width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="right">
<div style="display:none;font-size:1px;color:#333333;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">
-
</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--Header Block-->
<table bgcolor="#FFFFFF" border="0" cellspacing="0" cellpadding="0" align="center" width="100%"
style="border-left:solid 1px #dedee4; border-right:solid 1px #dedee4; border-bottom:solid 1px #dedee4; border-top:solid 1px #dedee4; width:100%!important; min-width:100%;">
<tr style="width:100%;">
<td>
<table border="0" cellspacing="0" cellpadding="10" align="center" width="100%">
<tr>
<td align="left" valign="middle"><a style="color: #ffffff; text-decoration: none;" href=
"http://go.alfresco.com/a0100K050L0000KZjI2d00U" target="_blank"
><span style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; color: #727174;"><img
style="border:none; display:block; border-collapse:collapse; outline:none; text-decoration:none;"
src="${template_assets_url}/hyland_logo.png" border="0"
alt="Alfresco" width="122" height="38"></span></a></td>
</tr>
</table>
</td>
<td align="right" bgcolor="#FFFFFF">
<table border="0" cellspacing="0" cellpadding="10" align="center" width="100%">
<tr>
<td align="right" valign="middle"><span
style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; font-weight:200; color: #727174;">Digital Workspace</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--Break-->
<table width="100%">
<tr>
<td height="10">
<div class="mobile-br">&nbsp;</div>
</td>
</tr>
</table>
<!--Body Block 1-->
<!--Banner Block-->
<table cellspacing="0" cellpadding="0" width="100%" style="border-left:solid 1px #dedee4; border-right:solid 1px #dedee4; ">
<tr>
<td style="border-collapse:collapse; ">
<table cellspacing="0" cellpadding="0" width="100%" style="background-color:#0c79bf; border:none; ">
<tr>
<td style="border-collapse:collapse; ">
<div class="mktEditable" id="Banner Image 1"><span
style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 24px; color: #ffffff;"><img
src="${template_assets_url}/adw_logo.png" alt="Alfresco Products" style="width:100%; border:none;
display:block; border-collapse:collapse; outline:none; text-decoration:none;"></span></div>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--Copy-->
<table cellspacing="0" cellpadding="0" align="center" bgcolor="#FFFFFF"
style="border-left:solid 1px #dedee4; border-right:solid 1px #dedee4; border-bottom:solid 1px #dedee4; " width="100%">
<tr align="center">
<td style="background-color:#ffffff; " align="center">
<table class="force-width-80" cellspacing="0" cellpadding="12" align="center" bgcolor="#FFFFFF" width="80%">
<tbody>
<tr>
<td align="left">
<!--Headline Text-->
<div class="mktEditable" id="Headline Text 1" align="left" style="vertical-align:middle; "><span
style="color: #727174; margin: 0px; font-family: Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 22px; font-weight: 600; text-align: left; text-decoration: none; vertical-align:
middle;"><p>${message("templates.reset-password-email.ftl.title")}</p></span></div>
<div class="mktEditable" id="Body Text 1" align="left">
<span style="color:#727174; font-family:Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size:18px;font-weight:400; text-align:left; text-decoration:none; -webkit-text-size-adjust:none;">
<p>${message("templates.reset-password-email.ftl.detail")}</p>
</span>
</div>
<!--Break-->
<table width="100%">
<tr>
<td height="5">
<div class="mobile-br">&nbsp;</div>
</td>
</tr>
</table>
<!--Button-->
<table style="margin:0 auto; " cellspacing="0" cellpadding="0" width="100%">
<tr>
<td style="text-align:center; margin:0 auto; ">
<div><!--[if mso]>
<v:rect xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:w="urn:schemas-microsoft-com:office:word"
style="height:45px;v-text-anchor:middle;width:220px;" stroke="f"
fillcolor="#47AA2">
<w:anchorlock/>
<center>
<![endif]-->
<div class="mktEditable" id="Button Text"><a href="${reset_password_url}" style="background-color:#47aa42; color:#ffffff; display:inline-block; font-family:Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size:18px; font-weight:400; line-height:45px; text-align:center; text-decoration:none; width:220px;
-webkit-text-size-adjust:none;">${message("templates.reset-password-email.ftl.reset_password_button")}</a></div>
<span style="color:#727174; font-family:Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size:12px;font-weight:400; text-align:left; text-decoration:none; -webkit-text-size-adjust:none;">
<p>${message("templates.reset-password-email.ftl.ignore_message")}</p>
<span style="color:#727174; font-family:Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size:12px;font-weight:400; text-align:left; text-decoration:none; -webkit-text-size-adjust:none;">
<hr>
<p>${message("templates.reset-password-email.ftl.having_trouble_clicking_button")}</p>
<p><a href="${reset_password_url}">${reset_password_url}</a></p>
</span>
<!--[if mso]>
</center>
</v:rect>
<![endif]--></div>
</td>
</tr>
</table>
<!--Break-->
<table width="100%">
<tr>
<td height="5">
<div class="mobile-br">&nbsp;</div>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
<!--Break-->
<table width="100%">
<tr>
<td height="10">
<div class="mobile-br">&nbsp;</div>
</td>
</tr>
</table>
<!--Footer Block-->
<table border="0" cellspacing="0" cellpadding="0" align="center" width="100%">
<tr>
<td align="center">
<a style="color: #0c79bf; text-decoration: underline;" href="http://www.alfresco.com/company/contact"
target="_blank"><span
style="font-family: Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10px; color: #0c79bf;">${message("templates.generic-email.ftl.contact_us")}</span></a>
<span style="font-family: Gotham, 'Helvetica Neue', Helvetica, Arial,
sans-serif; font-size: 10px; color: #b3b3b8;">&copy; ${date?string["yyyy"]} Alfresco Software, Inc.
${message("templates.generic-email.ftl.copy_right")}</span><br>
<span style="font-family: Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10px; color: #b3b3b8;">Bridge Ave, The Place Maidenhead SL6 1AF United Kingdom</span><br>
<span style="font-family: Gotham, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10px; color: #b3b3b8;">1825 S Grant St, Suite 900 San Mateo, CA 94402 USA</span><br>
</td>
</tr>
</table>
<!--Break-->
<table width="100%">
<tr>
<td height="10">
<div class="mobile-br">&nbsp;</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--Litmus Tracking-->
<table style="display: none;">
<tbody>
<tr>
<td style="width: 0px; display: none; overflow: hidden; max-height: 0px;">{{my.Litmus_Code}}</td>
</tr>
</tbody>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 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,6 +25,9 @@
*/
package org.alfresco;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
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,7 +139,9 @@ 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,
org.alfresco.repo.security.authentication.identityservice.SpringOAuth2ClientUnitTest.class,
IdentityServiceFacadeFactoryBeanTest.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

@@ -0,0 +1,39 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 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;
import org.alfresco.repo.security.mtls.LocalTransformClientWithMTLSIntegrationTest;
import org.junit.experimental.categories.Categories;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith (Categories.class)
@Suite.SuiteClasses({
LocalTransformClientWithMTLSIntegrationTest.class
})
public class MTLSTestSuite
{
}

View File

@@ -25,6 +25,7 @@
*/
package org.alfresco.repo.content.transform;
import org.alfresco.httpclient.HttpClientConfig;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.Pair;
@@ -79,7 +80,7 @@ public class RemoteTransformerClientTest
@Mock private StatusLine mockStatusLine;
@Mock private HttpEntity mockReqEntity;
@Spy private RemoteTransformerClient remoteTransformerClient = new RemoteTransformerClient("TRANSFORMER", "http://localhost:1234/test");
@Spy private RemoteTransformerClient remoteTransformerClient = new RemoteTransformerClient("TRANSFORMER", "http://localhost:1234/test", new HttpClientConfig());
private String sourceMimetype = "application/msword";
private String sourceExtension = "doc";

View File

@@ -91,6 +91,7 @@ public class ResetPasswordServiceImplTest
private static TestPerson testPerson;
private static EmailUtil emailUtil;
private static TestPerson testPersonForWorkspace;
@BeforeClass
public static void initStaticData() throws Exception
@@ -114,9 +115,18 @@ public class ResetPasswordServiceImplTest
.setPassword("password")
.setEmail(userName + "@example.com");
String userNameForWorkspace = "shane.doe" + System.currentTimeMillis();
testPersonForWorkspace = new TestPerson()
.setUserName(userNameForWorkspace)
.setFirstName("Shane")
.setLastName("doe")
.setPassword("password")
.setEmail(userNameForWorkspace + "@example.com");
transactionHelper.doInTransaction((RetryingTransactionCallback<Void>) () ->
{
createUser(testPerson);
createUser(testPersonForWorkspace);
return null;
});
@@ -494,4 +504,84 @@ public class ResetPasswordServiceImplTest
}
}
@Test
public void testResetPasswordForClientWorkspace() throws Exception
{
// Try the credential before change of password
authenticateUser(testPersonForWorkspace.userName, testPersonForWorkspace.password);
// Make sure to run as system
AuthenticationUtil.clearCurrentSecurityContext();
AuthenticationUtil.setRunAsUserSystem();
// Request password reset
resetPasswordService.requestReset(testPersonForWorkspace.userName, "workspace");
assertEquals("A reset password email should have been sent.", 1, emailUtil.getSentCount());
// Check the email
MimeMessage msg = emailUtil.getLastEmail();
assertNotNull("There should be an email.", msg);
assertEquals("Should've been only one email recipient.", 1, msg.getAllRecipients().length);
// Check the recipient is the person who requested the reset password
assertEquals(testPersonForWorkspace.email, msg.getAllRecipients()[0].toString());
//Check the sender is what we set as default
assertEquals(DEFAULT_SENDER, msg.getFrom()[0].toString());
// There should be a subject
assertNotNull("There should be a subject.", msg.getSubject());
// Check the default email subject - (check that we are sending the right email)
String emailSubjectKey = getDeclaredField(SendResetPasswordEmailDelegate.class, "EMAIL_SUBJECT_KEY");
assertNotNull(emailSubjectKey);
assertEquals(msg.getSubject(), I18NUtil.getMessage(emailSubjectKey));
// Check the reset password url.
String resetPasswordUrl = (String) emailUtil.getLastEmailTemplateModelValue("reset_password_url");
assertNotNull("Wrong email is sent.", resetPasswordUrl);
// Get the workflow id and key
Pair<String, String> pair = getWorkflowIdAndKeyFromUrl(resetPasswordUrl);
assertNotNull("Workflow Id can't be null.", pair.getFirst());
assertNotNull("Workflow Key can't be null.", pair.getSecond());
emailUtil.reset();
// Now that we have got the email, try to reset the password
ResetPasswordDetails passwordDetails = new ResetPasswordDetails()
.setUserId(testPersonForWorkspace.userName)
.setPassword("newPassword")
.setWorkflowId(pair.getFirst())
.setWorkflowKey(pair.getSecond());
resetPasswordService.initiateResetPassword(passwordDetails);
assertEquals("A reset password confirmation email should have been sent.", 1, emailUtil.getSentCount());
// Check the email
msg = emailUtil.getLastEmail();
assertNotNull("There should be an email.", msg);
assertEquals("Should've been only one email recipient.", 1, msg.getAllRecipients().length);
// Check the recipient is the person who requested the reset password
assertEquals(testPersonForWorkspace.email, msg.getAllRecipients()[0].toString());
// Check the sender is what we set as default
assertEquals(DEFAULT_SENDER, msg.getFrom()[0].toString());
// There should be a subject
assertNotNull("There should be a subject.", msg.getSubject());
// Check the default email subject - (check that we are sending the right email)
emailSubjectKey = getDeclaredField(SendResetPasswordConfirmationEmailDelegate.class, "EMAIL_SUBJECT_KEY");
assertNotNull(emailSubjectKey);
assertEquals(msg.getSubject(), I18NUtil.getMessage(emailSubjectKey));
// Try the old credential
TestHelper.assertThrows(() -> authenticateUser(testPersonForWorkspace.userName, testPersonForWorkspace.password),
AuthenticationException.class,
"As the user changed her password, the authentication should have failed.");
// Try the new credential
authenticateUser(testPersonForWorkspace.userName, "newPassword");
// Make sure to run as system
AuthenticationUtil.clearCurrentSecurityContext();
AuthenticationUtil.setRunAsUserSystem();
emailUtil.reset();
// Try reset again with the used workflow
TestHelper.assertThrows(() -> resetPasswordService.initiateResetPassword(passwordDetails),
InvalidResetPasswordWorkflowException.class,
"The workflow instance is not active (it has already been used).");
assertEquals("No email should have been sent.", 0, emailUtil.getSentCount());
}
}

View File

@@ -25,17 +25,18 @@
*/
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.IdentityServiceAuthenticationComponent.OAuth2Client;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
@@ -65,7 +66,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Autowired
private PersonService personService;
private OAuth2Client mockOAuth2Client;
private IdentityServiceFacade mockIdentityServiceFacade;
@Before
public void setUp()
@@ -76,8 +77,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
authComponent.setNodeService(nodeService);
authComponent.setPersonService(personService);
mockOAuth2Client = mock(OAuth2Client.class);
authComponent.setOAuth2Client(mockOAuth2Client);
mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
}
@After
@@ -89,9 +90,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test (expected=AuthenticationException.class)
public void testAuthenticationFail()
{
doThrow(new CredentialsVerificationException("Failed"))
.when(mockOAuth2Client)
.verifyCredentials("username", "password");
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
doThrow(new AuthorizationException("Failed")).when(mockIdentityServiceFacade).authorize(grant);
authComponent.authenticateImpl("username", "password".toCharArray());
}
@@ -99,9 +100,10 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test(expected = AuthenticationException.class)
public void testAuthenticationFail_connectionException()
{
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
.when(mockOAuth2Client)
.verifyCredentials("username", "password");
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
doThrow(new AuthorizationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
.when(mockIdentityServiceFacade).authorize(grant);
try
{
@@ -118,9 +120,11 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test (expected=AuthenticationException.class)
public void testAuthenticationFail_otherException()
{
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
doThrow(new RuntimeException("Some other errors!"))
.when(mockOAuth2Client)
.verifyCredentials("username", "password");
.when(mockIdentityServiceFacade)
.authorize(grant);
authComponent.authenticateImpl("username", "password".toCharArray());
}
@@ -128,7 +132,10 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test
public void testAuthenticationPass()
{
doNothing().when(mockOAuth2Client).verifyCredentials("username", "password");
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
authComponent.authenticateImpl("username", "password".toCharArray());
@@ -137,9 +144,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
}
@Test (expected= AuthenticationException.class)
public void testFallthroughWhenOAuth2ClientIsNull()
public void testFallthroughWhenIdentityServiceFacadeIsNull()
{
authComponent.setOAuth2Client(null);
authComponent.setIdentityServiceFacade(null);
authComponent.authenticateImpl("username", "password".toCharArray());
}

View File

@@ -0,0 +1,65 @@
/*
* #%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.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Map;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
import org.junit.Test;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
public class IdentityServiceFacadeFactoryBeanTest
{
@Test
public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided()
{
final IdentityServiceConfig config = mock(IdentityServiceConfig.class);
when(config.getRealmKey()).thenReturn("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB");
final ProviderDetails providerDetails = mock(ProviderDetails.class);
when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer");
final JwtDecoderProvider provider = new JwtDecoderProvider(config);
final JwtDecoder decoder = provider.createJwtDecoder(null, providerDetails);
final Jwt decodedToken = decoder.decode("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjIxNDc0ODM2NDcsImp0aSI6IjEyMzQiLCJpc3MiOiJodHRwczovL215Lmlzc3VlciIsInN1YiI6ImFiYzEyMyIsInR5cCI6IkJlYXJlciIsInByZWZlcnJlZF91c2VybmFtZSI6InBpb3RyZWsifQ.k_KaOrLLh3QsT8mKphkcz2vKpulgxp92UoEDccpHJ1mxE3Pa3gFXPKTj4goUBKXieGPZRMvBDhfWNxMvRYZPiQr2NXJKapkh0bTd0qoaSWz9ICe9Nu3eg7_VA_nwUVPz_35wwmrxgVk0_kpUYQN_VtaO7ZgFE2sJzFjbkVls5aqfAMnEjEgQl837hqZvmlW2ZRWebtxXfQxAjtp0gcTg-xtAHKIINYo_1_uAtt_H9L8KqFaioxrVAEDDIlcKnb-Ks3Y62CrZauaGUJeN_aNj2gdOpdkhvCw79yJyZSGZ7okjGbidCNSAf7Bo2Y6h3dP1Gga7kRmD648ftZESrNvbyg");
assertThat(decodedToken).isNotNull();
final Map<String, Object> claims = decodedToken.getClaims();
assertThat(claims).isNotNull()
.isNotEmpty()
.containsEntry(USERNAME_CLAIM, "piotrek");
}
}

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,90 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.ArgumentMatchers.any;
import static java.util.Optional.ofNullable;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
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.time.Instant;
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.DecodedAccessToken;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
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 TokenDecodingException("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 TokenDecodingException("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.decodeToken(anyString()))
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
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 +125,42 @@ public class IdentityServiceRemoteUserMapperTest extends AbstractChainedSubsyste
{
authHeaderValues.add(BEARER_PREFIX + token);
}
when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements());
when(mockRequest.getHeaders(AUTHORIZATION_HEADER))
.thenReturn(authHeaderValues.elements());
when(mockRequest.getHeader(AUTHORIZATION_HEADER))
.thenReturn(authHeaderValues.isEmpty() ? null : authHeaderValues.get(0));
return mockRequest;
}
/**
* Utility method for creating a mocked Servlet request with basic auth.
*
* @return The mocked request object
*/
@SuppressWarnings("unchecked")
private HttpServletRequest createMockBasicRequest()
private static class TestDecodedToken implements DecodedAccessToken
{
// 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)
private final Supplier<String> usernameSupplier;
public TestDecodedToken(Supplier<String> usernameSupplier)
{
token.expiration(Time.currentTime() - 60);
this.usernameSupplier = usernameSupplier;
}
String jwt = new JWSBuilder()
.jsonContent(token)
.rsa256(keyPair.getPrivate());
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);
@Override
public String getTokenValue()
{
return "TEST";
}
@Override
public Instant getExpiresAt()
{
return Instant.now();
}
@Override
public Object getClaim(String claim)
{
return USERNAME_CLAIM.equals(claim) ? usernameSupplier.get() : null;
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* #%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.AuthorizationGrant;
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.decodeToken(TOKEN))
.havingCause().withNoCause().withMessage("Expected failure #1");
verifyNoInteractions(targetFacade);
assertThatExceptionOfType(IdentityServiceFacadeException.class)
.isThrownBy(() -> facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD)))
.havingCause().withNoCause().withMessage("Expected failure #2");
verifyNoInteractions(targetFacade);
assertThatExceptionOfType(IdentityServiceFacadeException.class)
.isThrownBy(() -> facade.decodeToken(TOKEN))
.havingCause().withNoCause().withMessage("Expected failure #3");
verifyNoInteractions(targetFacade);
final AuthorizationGrant grant = AuthorizationGrant.password(USER_NAME, PASSWORD);
facade.authorize(grant);
verify(targetFacade).authorize(grant);
}
@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.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
facade.decodeToken(TOKEN);
facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
facade.decodeToken(TOKEN);
facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
verify(supplier, times(1)).get();
verify(targetFacade, times(3)).authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
verify(targetFacade, times(2)).decodeToken(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,84 @@
/*
* #%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.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
import org.junit.Test;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.web.client.RestOperations;
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 RestOperations restOperations = mock(RestOperations.class);
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
when(restOperations.exchange(any(), any(Class.class))).thenThrow(new RuntimeException("Expected"));
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThatExceptionOfType(AuthorizationException.class)
.isThrownBy(() -> facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD)))
.havingCause().withNoCause().withMessage("Expected");
}
@Test
public void shouldThrowTokenExceptionOnFailure()
{
final RestOperations restOperations = mock(RestOperations.class);
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
when(jwtDecoder.decode(TOKEN)).thenThrow(new RuntimeException("Expected"));
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThatExceptionOfType(TokenDecodingException.class)
.isThrownBy(() -> facade.decodeToken(TOKEN))
.havingCause().withNoCause().withMessage("Expected");
}
private ClientRegistration testRegistration()
{
return ClientRegistration.withRegistrationId("test")
.tokenUri("http://localhost")
.clientId("test")
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build();
}
}

View File

@@ -1,124 +0,0 @@
/*
* #%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.ArgumentMatchers.argThat;
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.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
import org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean.SpringOAuth2Client;
import org.junit.Test;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
public class SpringOAuth2ClientUnitTest
{
private static final String USER_NAME = "user";
private static final String PASSWORD = "password";
@Test
public void shouldRecoverFromInitialAuthorizationServerUnavailability()
{
final OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
when(authorizedClient.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
when(authClientManager.authorize(any())).thenReturn(authorizedClient);
final SpringOAuth2Client client = new SpringOAuth2Client(faultySupplier(3, authClientManager));
assertThatExceptionOfType(CredentialsVerificationException.class)
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
.havingCause().withNoCause().withMessage("Expected failure #1");
verifyNoInteractions(authClientManager);
assertThatExceptionOfType(CredentialsVerificationException.class)
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
.havingCause().withNoCause().withMessage("Expected failure #2");
verifyNoInteractions(authClientManager);
assertThatExceptionOfType(CredentialsVerificationException.class)
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
.havingCause().withNoCause().withMessage("Expected failure #3");
verifyNoInteractions(authClientManager);
client.verifyCredentials(USER_NAME, PASSWORD);
verify(authClientManager).authorize(argThat(r -> r.getPrincipal() != null && USER_NAME.equals(r.getPrincipal().getPrincipal())));
}
@Test
public void shouldThrowVerificationExceptionOnFailure()
{
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
when(authClientManager.authorize(any())).thenThrow(new RuntimeException("Expected"));
final SpringOAuth2Client client = new SpringOAuth2Client(() -> authClientManager);
assertThatExceptionOfType(CredentialsVerificationException.class)
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
.havingCause().withNoCause().withMessage("Expected");
}
@Test
public void shouldAvoidCreatingMultipleInstanceOfOAuth2AuthorizedClientManager()
{
final OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
when(authorizedClient.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
when(authClientManager.authorize(any())).thenReturn(authorizedClient);
final Supplier<OAuth2AuthorizedClientManager> supplier = mock(Supplier.class);
when(supplier.get()).thenReturn(authClientManager);
final SpringOAuth2Client client = new SpringOAuth2Client(supplier);
client.verifyCredentials(USER_NAME, PASSWORD);
client.verifyCredentials(USER_NAME, PASSWORD);
client.verifyCredentials(USER_NAME, PASSWORD);
verify(supplier, times(1)).get();
verify(authClientManager, times(3)).authorize(any());
}
private Supplier<OAuth2AuthorizedClientManager> faultySupplier(int numberOfInitialFailures, OAuth2AuthorizedClientManager authClientManager)
{
final int[] counter = new int[]{0};
return () -> {
if (counter[0]++ < numberOfInitialFailures)
{
throw new RuntimeException("Expected failure #" + counter[0]);
}
return authClientManager;
};
}
}

View File

@@ -0,0 +1,53 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 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.mtls;
import org.alfresco.repo.rendition2.LocalTransformClientIntegrationTest;
import org.alfresco.repo.rendition2.RenditionService2;
import org.junit.BeforeClass;
/**
* Integration tests for {@link RenditionService2} with mtls enabled
*/
public class LocalTransformClientWithMTLSIntegrationTest extends LocalTransformClientIntegrationTest
{
@BeforeClass
public static void before()
{
local();
System.setProperty("localTransform.core-aio.url", "https://localhost:8090/");
System.setProperty("httpclient.config.transform.mTLSEnabled", "true");
System.setProperty("ssl-keystore.password", "password");
System.setProperty("ssl-truststore.password", "password");
System.setProperty("metadata-keystore.password", "password");
System.setProperty("metadata-keystore.aliases", "metadata");
System.setProperty("metadata-keystore.metadata.password", "password");
}
}

View File

@@ -25,6 +25,9 @@
*/
package org.alfresco.repo.tagging;
import static org.alfresco.model.ContentModel.ASPECT_TAGGABLE;
import static org.alfresco.model.ContentModel.PROP_TAGS;
import static org.alfresco.repo.tagging.TaggingServiceImpl.TAG_UPDATES;
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;
@@ -33,16 +36,26 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
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.search.CategoryService;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.util.Pair;
import org.junit.Before;
import org.junit.Test;
@@ -57,11 +70,20 @@ public class TaggingServiceImplUnitTest
private static final String TAG_ID = "tag-node-id";
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 CONTENT_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "content-id");
@Mock
private NodeService nodeServiceMock;
@Mock
private CategoryService categoryServiceMock;
@Mock
private PolicyComponent policyComponentMock;
@Mock
private SearchService searchServiceMock;
@Mock
private ResultSet resultSetMock;
@Mock(extraInterfaces = List.class)
private Serializable currentTagsMock;
@InjectMocks
private TaggingServiceImpl taggingService;
@@ -69,6 +91,7 @@ public class TaggingServiceImplUnitTest
@Before
public void setUp() throws Exception
{
AlfrescoTransactionSupport.bindResource(TAG_UPDATES, new HashMap<>());
taggingService.init();
}
@@ -82,8 +105,8 @@ public class TaggingServiceImplUnitTest
//when
final List<Pair<String, NodeRef>> actualTagPairs = taggingService.createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ContentModel.ASPECT_TAGGABLE, TAG_NAME, false);
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ContentModel.ASPECT_TAGGABLE, TAG_NAME, true);
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, TAG_NAME, false);
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, TAG_NAME, true);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
List<Pair<String, NodeRef>> expectedTagPairs = List.of(new Pair<>(TAG_NAME, TAG_NODE_REF));
assertThat(actualTagPairs)
@@ -99,8 +122,57 @@ public class TaggingServiceImplUnitTest
//when
final Throwable actualException = catchThrowable(() -> taggingService.createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME)));
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ContentModel.ASPECT_TAGGABLE, TAG_NAME, false);
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, TAG_NAME, false);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualException).isInstanceOf(DuplicateChildNodeNameException.class);
}
@Test
@SuppressWarnings("unchecked")
public void testChangeTag()
{
final String newTagName = "new-tag-name";
final NodeRef newTagNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, newTagName);
given(searchServiceMock.query(any(), any(String.class), any(String.class))).willReturn(resultSetMock);
given(resultSetMock.getNodeRefs()).willReturn(List.of(CONTENT_NODE_REF), Collections.emptyList());
given(nodeServiceMock.hasAspect(CONTENT_NODE_REF, ASPECT_TAGGABLE)).willReturn(true);
given(categoryServiceMock.getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, TAG_NAME, false)).willReturn(childAssociationsOf(TAG_NODE_REF));
given(nodeServiceMock.getProperty(CONTENT_NODE_REF, PROP_TAGS)).willReturn(currentTagsMock);
given(((List<NodeRef>) currentTagsMock).size()).willReturn(1);
given(((List<NodeRef>) currentTagsMock).contains(TAG_NODE_REF)).willReturn(true);
given(categoryServiceMock.getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, newTagName, true)).willReturn(childAssociationsOf(newTagNodeRef));
given(((List<NodeRef>) currentTagsMock).contains(newTagNodeRef)).willReturn(false);
//when
taggingService.changeTag(STORE_REF_WORKSPACE_SPACESSTORE, TAG_NAME, newTagName);
then((List<NodeRef>) currentTagsMock).should().remove(TAG_NODE_REF);
then((List<NodeRef>) currentTagsMock).should().add(newTagNodeRef);
then(nodeServiceMock).should(times(2)).setProperty(CONTENT_NODE_REF, PROP_TAGS, currentTagsMock);
then(categoryServiceMock).should().deleteCategory(TAG_NODE_REF);
then(categoryServiceMock).should(times(2)).getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, newTagName, true);
}
@Test
public void testChangeOrphanTag()
{
final String newTagName = "new-tag-name";
given(searchServiceMock.query(any(), any(String.class), any(String.class))).willReturn(resultSetMock);
given(resultSetMock.getNodeRefs()).willReturn(Collections.emptyList());
given(categoryServiceMock.getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, TAG_NAME, false)).willReturn(childAssociationsOf(TAG_NODE_REF));
//when
taggingService.changeTag(STORE_REF_WORKSPACE_SPACESSTORE, TAG_NAME, newTagName);
then(nodeServiceMock).should(never()).setProperty(any(), any(), any());
then(categoryServiceMock).should().deleteCategory(TAG_NODE_REF);
then(categoryServiceMock).should().getRootCategories(STORE_REF_WORKSPACE_SPACESSTORE, ASPECT_TAGGABLE, newTagName, true);
}
private static List<ChildAssociationRef> childAssociationsOf(final NodeRef... childNodeRefs)
{
return Arrays.stream(childNodeRefs)
.map(childNodeRef -> new ChildAssociationRef(null, null, null, childNodeRef))
.collect(Collectors.toList());
}
}

Some files were not shown because too many files have changed in this diff Show More