Compare commits

...

85 Commits

Author SHA1 Message Date
alfresco-build
0bd476968b [maven-release-plugin][skip ci] prepare release 25.2.0.24 2025-05-08 13:20:12 +00:00
Kacper Magdziarz
e51e5e8ca5 [PRODSEC-10123] Bump Httpcore5 to 5.3.4 2025-05-08 14:34:42 +02:00
alfresco-build
46db14d5ff [maven-release-plugin][skip ci] prepare for next development iteration 2025-05-07 17:09:56 +00:00
alfresco-build
b5fa73ca3b [maven-release-plugin][skip ci] prepare release 25.2.0.23 2025-05-07 17:09:54 +00:00
Kacper Magdziarz
c962daae3b [PRODSEC-10123] Bump Httpclient5 to 5.4.4 (#3336) 2025-05-07 18:18:26 +02:00
alfresco-build
8efc559b09 [maven-release-plugin][skip ci] prepare for next development iteration 2025-05-05 07:30:49 +00:00
alfresco-build
08628732fc [maven-release-plugin][skip ci] prepare release 25.2.0.22 2025-05-05 07:30:47 +00:00
tathagta15
7f74bf7b3e Bump camel version to 4.11.0 (#3334) 2025-05-05 12:08:42 +05:30
alfresco-build
390073b153 [maven-release-plugin][skip ci] prepare for next development iteration 2025-05-04 00:08:55 +00:00
alfresco-build
68c87f69c5 [maven-release-plugin][skip ci] prepare release 25.2.0.21 2025-05-04 00:08:53 +00:00
Alfresco CI User
2b936050c8 [force] Force release for 2025-05-04. 2025-05-04 00:05:08 +00:00
alfresco-build
28184ca69a [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-30 06:54:38 +00:00
alfresco-build
e05c74813e [maven-release-plugin][skip ci] prepare release 25.2.0.20 2025-04-30 06:54:36 +00:00
tathagta15
e05a1d9ba9 Bump spring-security to 6.3.8 (#3329) 2025-04-30 11:14:56 +05:30
tathagta15
5aab15a77a Bump poi-ooxml version to 5.4.0 (#3328) 2025-04-29 18:10:26 +05:30
alfresco-build
5d267c8d60 [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-27 00:08:37 +00:00
alfresco-build
18fc9a58b4 [maven-release-plugin][skip ci] prepare release 25.2.0.19 2025-04-27 00:08:34 +00:00
Alfresco CI User
6a7ba876b7 [force] Force release for 2025-04-27. 2025-04-27 00:04:58 +00:00
alfresco-build
65bdb242ec [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-25 08:09:22 +00:00
alfresco-build
5a537b301a [maven-release-plugin][skip ci] prepare release 25.2.0.18 2025-04-25 08:09:20 +00:00
KushalBanik
515b894241 [MNT-24490] Reference for AlfrescoSQLServerDialect changed to SQLServerDialect (#3323) 2025-04-25 12:54:39 +05:30
alfresco-build
4978d9e790 [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-23 07:29:04 +00:00
alfresco-build
bf8f5117ac [maven-release-plugin][skip ci] prepare release 25.2.0.17 2025-04-23 07:29:02 +00:00
mohit-singh4
cfb5cb2c6d [fix/MNT-24172-fixRecreationOfRendition2] Fix of recreation of rendition2 (#3155)
Co-authored-by: mohit-singh4 <mohit.singh@contractors.hyland.com>
2025-04-23 12:16:57 +05:30
alfresco-build
71eed6822d [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-20 00:08:37 +00:00
alfresco-build
b1fe69693c [maven-release-plugin][skip ci] prepare release 25.2.0.16 2025-04-20 00:08:35 +00:00
Alfresco CI User
56d415fe4c [force] Force release for 2025-04-20. 2025-04-20 00:04:49 +00:00
alfresco-build
8f4e617703 [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-16 11:44:50 +00:00
alfresco-build
d8bdb82291 [maven-release-plugin][skip ci] prepare release 25.2.0.15 2025-04-16 11:44:47 +00:00
vedantmehra9
f2927a804e Merge pull request #3195 from Alfresco/MNT-23926_add_sevenpass_dod_5220.22-M_ags_deletion
[MNT-23926] Added Seven Pass algorithm for deletion
2025-04-16 15:53:49 +05:30
alfresco-build
baba2090a8 [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-15 05:44:38 +00:00
alfresco-build
dbb1bc11f4 [maven-release-plugin][skip ci] prepare release 25.2.0.14 2025-04-15 05:44:37 +00:00
SatyamSah5
9b53abdc98 [MNT-24358] deleted outdated mTLS config (#3313) 2025-04-15 10:22:52 +05:30
alfresco-build
58bff2bebf [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-13 00:31:48 +00:00
alfresco-build
63fc98d100 [maven-release-plugin][skip ci] prepare release 25.2.0.13 2025-04-13 00:31:46 +00:00
Alfresco CI User
60763787b4 [force] Force release for 2025-04-13. 2025-04-13 00:28:47 +00:00
alfresco-build
6b6e6264a0 [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-09 09:06:52 +00:00
alfresco-build
656eb16a38 [maven-release-plugin][skip ci] prepare release 25.2.0.12 2025-04-09 09:06:49 +00:00
SatyamSah5
447cf86ba6 Merge pull request #3299 from Alfresco/fix/MNT-24623_unzip_error_accent_fix
[MNT-24623] Fix for unsuccessful unzip when folder name contains accent characters.
2025-04-09 13:53:42 +05:30
Satyam Sah
dbf149779d added supress warning for ImporterActionExecutorTest 2025-04-08 17:58:05 +05:30
alfresco-build
cdcf2a7f9b [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-08 08:56:30 +00:00
alfresco-build
9e4f07a010 [maven-release-plugin][skip ci] prepare release 25.2.0.11 2025-04-08 08:56:28 +00:00
Belal Ansari
c2bae9c53a MNT-24891 - Skip actionContext to classify as an adhoc property in add-features action logic (#3298)
Skip actionContext to classify as an adhoc property in add-features action logic
2025-04-08 13:43:26 +05:30
alfresco-build
7412553930 [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-06 00:08:44 +00:00
alfresco-build
ca214ee58f [maven-release-plugin][skip ci] prepare release 25.2.0.10 2025-04-06 00:08:42 +00:00
Alfresco CI User
bb261a1ddc [force] Force release for 2025-04-06. 2025-04-06 00:05:01 +00:00
Satyam Sah
920d34b289 format fix 2025-04-04 15:36:44 +05:30
Satyam Sah
9c65ffddc9 removed redundant logic 2025-04-04 15:20:59 +05:30
Satyam Sah
e817e7d64f added proper message 2025-04-04 15:05:39 +05:30
alfresco-build
f098334fba [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-04 09:35:08 +00:00
alfresco-build
4d6ee95daf [maven-release-plugin][skip ci] prepare release 25.2.0.9 2025-04-04 09:35:06 +00:00
Satyam Sah
2ded9e6c4b Added unit test case for MNT-24623 2025-04-04 14:58:38 +05:30
Kacper Magdziarz
6954f432ff [ACS-9490] Use FixedThreadPool for ExecutorService (#3301)
Add property `local.transform.threadPoolSize=8` for controlling size of thread pool used for transforms.
2025-04-04 10:52:17 +02:00
Satyam Sah
494545b20d Stripped Accent characters from folder name before directory creation. 2025-04-03 18:33:03 +05:30
alfresco-build
86c52f5d0e [maven-release-plugin][skip ci] prepare for next development iteration 2025-04-01 13:49:11 +00:00
alfresco-build
39ef9442fb [maven-release-plugin][skip ci] prepare release 25.2.0.8 2025-04-01 13:49:09 +00:00
cezary-witkowski
388c12ab3e [MNT-24937] Fix EventTableOutbox messages flooding the logs on bootstrap failure (#3286) 2025-04-01 15:04:58 +02:00
alfresco-build
ebb0355692 [maven-release-plugin][skip ci] prepare for next development iteration 2025-03-30 00:08:37 +00:00
alfresco-build
53abde6bea [maven-release-plugin][skip ci] prepare release 25.2.0.7 2025-03-30 00:08:35 +00:00
Alfresco CI User
4e990f918b [force] Force release for 2025-03-30. 2025-03-30 00:04:44 +00:00
alfresco-build
416774e4a6 [maven-release-plugin][skip ci] prepare for next development iteration 2025-03-29 00:01:55 +00:00
alfresco-build
2c885bdd61 [maven-release-plugin][skip ci] prepare release 25.2.0.6 2025-03-29 00:01:52 +00:00
Tiago Salvado
74380693d0 [MNT-24992] Add method to force renditions content hash code (#3288) 2025-03-28 23:17:58 +00:00
alfresco-build
502ae6c02f [maven-release-plugin][skip ci] prepare for next development iteration 2025-03-26 09:45:09 +00:00
alfresco-build
de880c8273 [maven-release-plugin][skip ci] prepare release 25.2.0.5 2025-03-26 09:45:06 +00:00
Damian Ujma
658de4f7e6 ACS-9433 Bump AOS version to 3.3.0-A1 (#3285) 2025-03-26 09:43:11 +01:00
alfresco-build
b89ddc51c1 [maven-release-plugin][skip ci] prepare for next development iteration 2025-03-25 15:17:15 +00:00
alfresco-build
7c1ca67ef4 [maven-release-plugin][skip ci] prepare release 25.2.0.4 2025-03-25 15:17:12 +00:00
Damian Ujma
2388687eb0 ACS-9414 Enhance the Identity Provider configuration (#3263) 2025-03-25 15:19:34 +01:00
alfresco-build
637c349205 [maven-release-plugin][skip ci] prepare for next development iteration 2025-03-23 00:08:00 +00:00
alfresco-build
21434e1ce1 [maven-release-plugin][skip ci] prepare release 25.2.0.3 2025-03-23 00:07:58 +00:00
Alfresco CI User
f49ce68c9e [force] Force release for 2025-03-23. 2025-03-23 00:04:50 +00:00
vedantmehra9
157430c0d6 formatting issue 2025-03-17 13:25:29 +05:30
Vedant Mehra
9cddeb61c8 Merge branch 'master' of https://github.com/Alfresco/alfresco-community-repo into MNT-23926_add_sevenpass_dod_5220.22-M_ags_deletion 2025-03-17 10:44:52 +05:30
Vedant Mehra
86980cb634 Merge branch 'master' of https://github.com/Alfresco/alfresco-community-repo into MNT-23926_add_sevenpass_dod_5220.22-M_ags_deletion 2025-03-12 13:05:29 +05:30
Vedant Mehra
72fed7f913 Merge branch 'master' of https://github.com/Alfresco/alfresco-community-repo into MNT-23926_add_sevenpass_dod_5220.22-M_ags_deletion 2025-02-27 13:30:42 +05:30
Vedant Mehra
13a83d9e22 reverting properties file change 2025-02-19 15:29:43 +05:30
Vedant Mehra
a2735539ea changes for review comments 2025-02-19 15:25:43 +05:30
Vedant Mehra
1cb3931f21 changes for pmd 2025-02-13 19:20:39 +05:30
Vedant Mehra
0f5653e250 changes for pmd 2025-02-13 18:03:54 +05:30
Vedant Mehra
4a33ad8c3b changes for pmd 2025-02-13 16:25:47 +05:30
Vedant Mehra
85703f4284 changes for pmd 2025-02-13 14:41:43 +05:30
Vedant Mehra
127912aca9 Removed unwanted files from commit 2025-02-13 12:26:52 +05:30
Vedant Mehra
70135ab771 fixed pre commit issues 2025-02-13 11:02:07 +05:30
Vedant Mehra
fa516ef58f [MNT-23926] Added Seven Pass algorithm for deletion 2025-02-12 14:52:44 +05:30
81 changed files with 1379 additions and 583 deletions

View File

@@ -147,7 +147,7 @@ jobs:
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
- uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- uses: Alfresco/ya-pmd-scan@v4.1.0
- uses: Alfresco/ya-pmd-scan@v4.3.0
with:
classpath-build-command: "mvn test-compile -ntp -Pags -pl \"-:alfresco-community-repo-docker\""

View File

@@ -1519,7 +1519,7 @@
"filename": "repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 128,
"line_number": 130,
"is_secret": false
}
],
@@ -1607,7 +1607,7 @@
"filename": "repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 47,
"line_number": 48,
"is_secret": false
}
],
@@ -1868,5 +1868,5 @@
}
]
},
"generated_at": "2025-03-17T14:00:53Z"
"generated_at": "2025-03-27T23:45:41Z"
}

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<modules>

View File

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

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<modules>

View File

@@ -33,5 +33,8 @@
<!-- content cleanser -->
<bean id="contentCleanser.522022M" class="org.alfresco.module.org_alfresco_module_rm.content.cleanser.ContentCleanser522022M"/>
<!-- content cleanser -->
<bean id="contentCleanser.SevenPass" class="org.alfresco.module.org_alfresco_module_rm.content.cleanser.ContentCleanserSevenPass"/>
</beans>

View File

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

View File

@@ -0,0 +1,51 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2025 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.module.org_alfresco_module_rm.content.cleanser;
import java.io.File;
/**
* DoD 5220-22M Seven Pass data cleansing implementation.
*
*/
public class ContentCleanserSevenPass extends ContentCleanser522022M
{
/**
* @see org.alfresco.module.org_alfresco_module_rm.content.cleanser.ContentCleanser#cleanse(java.io.File)
*/
@Override
public void cleanse(File file)
{
super.cleanse(file);
overwrite(file, overwriteZeros);
overwrite(file, overwriteZeros);
overwrite(file, overwriteOnes);
overwrite(file, overwriteRandom);
}
}

View File

@@ -0,0 +1,100 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2025 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.module.org_alfresco_module_rm.content.cleanser;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.File;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.alfresco.module.org_alfresco_module_rm.test.util.BaseUnitTest;
import org.alfresco.service.cmr.repository.ContentIOException;
/**
* Eager content store cleaner unit test.
*
*/
public class ContentCleanserSevenPassUnitTest extends BaseUnitTest
{
@InjectMocks
@Spy
private ContentCleanserSevenPass contentCleanserSevenPass = new ContentCleanserSevenPass()
{
/** dummy implementations */
@Override
protected void overwrite(File file, OverwriteOperation overwriteOperation)
{
// Intentionally left empty
}
};
@Mock
private File mockedFile;
/**
* Given that a file exists When I cleanse it Then the content is overwritten
*/
@Test
public void cleanseFile()
{
when(mockedFile.exists()).thenReturn(true);
when(mockedFile.canWrite()).thenReturn(true);
contentCleanserSevenPass.cleanse(mockedFile);
verify(contentCleanserSevenPass, times(2)).overwrite(mockedFile, contentCleanserSevenPass.overwriteOnes);
verify(contentCleanserSevenPass, times(3)).overwrite(mockedFile, contentCleanserSevenPass.overwriteZeros);
verify(contentCleanserSevenPass, times(2)).overwrite(mockedFile, contentCleanserSevenPass.overwriteRandom);
}
/**
* Given that the file does not exist When I cleanse it Then an exception is thrown
*/
@Test(expected = ContentIOException.class)
public void fileDoesNotExist()
{
when(mockedFile.exists()).thenReturn(false);
when(mockedFile.canWrite()).thenReturn(true);
contentCleanserSevenPass.cleanse(mockedFile);
}
/**
* Given that I can not write to the file When I cleanse it Then an exception is thrown
*/
@Test(expected = ContentIOException.class)
public void cantWriteToFile()
{
when(mockedFile.exists()).thenReturn(true);
when(mockedFile.canWrite()).thenReturn(false);
contentCleanserSevenPass.cleanse(mockedFile);
}
}

View File

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

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<modules>

View File

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

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<dependencies>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<dependencies>

View File

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

View File

@@ -1,141 +0,0 @@
Instructions for Generating Repository SSL Keystores
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<store password> is the keystore password. The file ${dir.keystore}/ssl-keystore-passwords.properties contains passwords for the SSL keystore,
${dir.keystore}/ssl-truststore-passwords.properties contains passwords for the SSL truststore.
These instructions will create an RSA public/private key pair for the repository with a certificate that has been signed by the Alfresco Certificate Authority (CA).
It will also create a truststore for the repository containing the CA certificate; this will be used to authenticate connections to specific repository
URLs from Solr. It assumes the existence of the Alfresco CA key and certificate to sign the repository certificate; for security reasons these are not generally available.
You can either generate your own CA key and certificate (see instructions below) or use a recognised Certificate Authority such as Verisign. For Alfresco employees the key
and certificate are available in svn.
(i) Generate the repository public/private key pair in a keystore:
$ keytool -genkey -alias ssl.repo -keyalg RSA -keystore ssl.keystore -storetype JCEKS -storepass <store password>
Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]: Alfresco Repository
What is the name of your organizational unit?
[Unknown]:
What is the name of your organization?
[Unknown]: Alfresco Software Ltd.
What is the name of your City or Locality?
[Unknown]: Maidenhead
What is the name of your State or Province?
[Unknown]: UK
What is the two-letter country code for this unit?
[Unknown]: GB
Is CN=Alfresco Repository, OU=Unknown, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB correct?
[no]: yes
Enter key password for <ssl.repo>
(RETURN if same as keystore password):
(ii) Generate a certificate request for the repository key
$ keytool -keystore ssl.keystore -alias ssl.repo -certreq -file repo.csr -storetype JCEKS -storepass <store password>
(iii) Alfresco CA signs the certificate request, creating a certificate that is valid for 365 days.
$ openssl x509 -CA ca.crt -CAkey ca.key -CAcreateserial -req -in repo.csr -out repo.crt -days 365
Signature ok
subject=/C=GB/ST=UK/L=Maidenhead/O=Alfresco Software Ltd./OU=Unknown/CN=Alfresco Repository
Getting CA Private Key
Enter pass phrase for ca.key:
(iv) Import the Alfresco CA key into the repository key store
$ keytool -import -alias ssl.alfreco.ca -file ca.crt -keystore ssl.keystore -storetype JCEKS -storepass <store password>
Enter keystore password:
Owner: CN=Alfresco CA, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB
Issuer: CN=Alfresco CA, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB
Serial number: 805ba6dc8f62f8b8
Valid from: Fri Aug 12 13:28:58 BST 2011 until: Mon Aug 09 13:28:58 BST 2021
Certificate fingerprints:
MD5: 4B:45:94:2D:8E:98:E8:12:04:67:AD:AE:48:3C:F5:A0
SHA1: 74:42:22:D0:52:AD:82:7A:FD:37:46:37:91:91:F4:77:89:3A:C9:A3
Signature algorithm name: SHA1withRSA
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 08 42 40 DC FE 4A 50 87 05 2B 38 4D 92 70 8E 51 .B@..JP..+8M.p.Q
0010: 4E 38 71 D6 N8q.
]
]
#2: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
CA:true
PathLen:2147483647
]
#3: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: 08 42 40 DC FE 4A 50 87 05 2B 38 4D 92 70 8E 51 .B@..JP..+8M.p.Q
0010: 4E 38 71 D6 N8q.
]
[CN=Alfresco CA, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB]
SerialNumber: [ 805ba6dc 8f62f8b8]
]
Trust this certificate? [no]: yes
Certificate was added to keystore
(v) Import the CA-signed repository certificate into the repository keystore
$ keytool -import -alias ssl.repo -file repo.crt -keystore ssl.keystore -storetype JCEKS -storepass <store password>
Enter keystore password:
Certificate reply was installed in keystore
(vi) Convert the repository keystore to a pkcs12 keystore (for use in browsers such as Firefox). Give the pkcs12 key store the key store password 'alfresco'.
keytool -importkeystore -srckeystore ssl.keystore -srcstorepass <keystore password> -srcstoretype JCEKS -srcalias ssl.repo -srckeypass kT9X6oe68t -destkeystore firefox.p12 -deststoretype pkcs12 -deststorepass alfresco -destalias ssl.repo -destkeypass alfresco
(vi) Create a repository truststore containing the Alfresco CA certificate
keytool -import -alias ssl.alfreco.ca -file ca.crt -keystore ssl.keystore -storetype JCEKS -storepass <store password>
keytool -import -alias alfreco.ca -file ca.crt -keystore ssl.truststore -storetype JCEKS -storepass <store password>
(vii) Copy the keystore and truststore to the repository keystore location defined by the property 'dir.keystore'.
(viii) Update the SSL properties i.e. properties starting with the prefixes 'alfresco.encryption.ssl.keystore' and 'alfresco.encryption.ssl.truststore'.
Instructions for Generating a Certificate Authority (CA) Key and Certificate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(i) Generate the CA private key
$ openssl genrsa -des3 -out ca.key 1024
Generating RSA private key, 1024 bit long modulus
..........++++++
..++++++
e is 65537 (0x10001)
Enter pass phrase for ca.key:
Verifying - Enter pass phrase for ca.key:
(ii) Generate the CA self-signed certificate
$ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
Enter pass phrase for ca.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:GB
State or Province Name (full name) [Some-State]:UK
Locality Name (eg, city) []:Maidenhead
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Alfresco Software Ltd.
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:Alfresco CA
Email Address []:

View File

@@ -1,58 +0,0 @@
@rem Please edit the variables below to suit your installation
@rem Note: for an installation created by the Alfresco installer, you only need to edit ALFRESCO_HOME
@rem Alfresco installation directory
set ALFRESCO_HOME=C:\Alfresco-5.2
@rem The directory containing the alfresco keystores, as referenced by keystoreFile and truststoreFile attributes in tomcat\conf\server.xml
set ALFRESCO_KEYSTORE_HOME=%ALFRESCO_HOME%\alf_data\keystore
@rem Java installation directory
set JAVA_HOME=%ALFRESCO_HOME%\java
@rem Location in which new keystore files will be generated
set CERTIFICATE_HOME=%USERPROFILE%
@rem The repository server certificate subject name, as specified in tomcat\conf\tomcat-users.xml with roles="repository"
set REPO_CERT_DNAME=CN=Alfresco Repository, OU=Unknown, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB
@rem The SOLR client certificate subject name, as specified in tomcat\conf\tomcat-users.xml with roles="repoclient"
set SOLR_CLIENT_CERT_DNAME=CN=Alfresco Repository Client, OU=Unknown, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB
@rem The number of days before the certificate expires
set CERTIFICATE_VALIDITY=36525
@rem Ensure certificate output dir exists
@if not exist "%CERTIFICATE_HOME%" mkdir "%CERTIFICATE_HOME%"
@rem Remove old output files (note they are backed up elsewhere)
@if exist "%CERTIFICATE_HOME%\ssl.keystore" del "%CERTIFICATE_HOME%\ssl.keystore"
@if exist "%CERTIFICATE_HOME%\ssl.truststore" del "%CERTIFICATE_HOME%\ssl.truststore"
@if exist "%CERTIFICATE_HOME%\browser.p12" del "%CERTIFICATE_HOME%\browser.p12"
@if exist "%CERTIFICATE_HOME%\ssl.repo.client.keystore" del "%CERTIFICATE_HOME%\ssl.repo.client.keystore"
@if exist "%CERTIFICATE_HOME%\ssl.repo.client.truststore" del "%CERTIFICATE_HOME%\ssl.repo.client.truststore"
@rem Generate new self-signed certificates for the repository and solr
"%JAVA_HOME%\bin\keytool" -genkeypair -keyalg RSA -dname "%REPO_CERT_DNAME%" -validity %CERTIFICATE_VALIDITY% -alias ssl.repo -keypass kT9X6oe68t -keystore "%CERTIFICATE_HOME%\ssl.keystore" -storetype JCEKS -storepass kT9X6oe68t
"%JAVA_HOME%\bin\keytool" -exportcert -alias ssl.repo -file "%CERTIFICATE_HOME%\ssl.repo.crt" -keystore "%CERTIFICATE_HOME%\ssl.keystore" -storetype JCEKS -storepass kT9X6oe68t
"%JAVA_HOME%\bin\keytool" -genkeypair -keyalg RSA -dname "%SOLR_CLIENT_CERT_DNAME%" -validity %CERTIFICATE_VALIDITY% -alias ssl.repo.client -keypass kT9X6oe68t -keystore "%CERTIFICATE_HOME%\ssl.repo.client.keystore" -storetype JCEKS -storepass kT9X6oe68t
"%JAVA_HOME%\bin\keytool" -exportcert -alias ssl.repo.client -file "%CERTIFICATE_HOME%\ssl.repo.client.crt" -keystore "%CERTIFICATE_HOME%\ssl.repo.client.keystore" -storetype JCEKS -storepass kT9X6oe68t
@rem Create trust relationship between repository and solr
"%JAVA_HOME%\bin\keytool" -importcert -noprompt -alias ssl.repo.client -file "%CERTIFICATE_HOME%\ssl.repo.client.crt" -keystore "%CERTIFICATE_HOME%\ssl.truststore" -storetype JCEKS -storepass kT9X6oe68t
@rem Create trust relationship between repository and itself - used for searches
"%JAVA_HOME%\bin\keytool" -importcert -noprompt -alias ssl.repo -file "%CERTIFICATE_HOME%\ssl.repo.crt" -keystore "%CERTIFICATE_HOME%\ssl.truststore" -storetype JCEKS -storepass kT9X6oe68t
@rem Create trust relationship between solr and repository
"%JAVA_HOME%\bin\keytool" -importcert -noprompt -alias ssl.repo -file "%CERTIFICATE_HOME%\ssl.repo.crt" -keystore "%CERTIFICATE_HOME%\ssl.repo.client.truststore" -storetype JCEKS -storepass kT9X6oe68t
@rem Export repository keystore to pkcs12 format for browser compatibility
"%JAVA_HOME%\bin\keytool" -importkeystore -srckeystore "%CERTIFICATE_HOME%\ssl.keystore" -srcstorepass kT9X6oe68t -srcstoretype JCEKS -srcalias ssl.repo -srckeypass kT9X6oe68t -destkeystore "%CERTIFICATE_HOME%\browser.p12" -deststoretype pkcs12 -deststorepass alfresco -destalias ssl.repo -destkeypass alfresco
@rem Ensure keystore dir actually exists
@if not exist "%ALFRESCO_KEYSTORE_HOME%" mkdir "%ALFRESCO_KEYSTORE_HOME%"
@rem Install the new files
copy /Y "%CERTIFICATE_HOME%\ssl.keystore" "%ALFRESCO_KEYSTORE_HOME%\ssl.keystore"
copy /Y "%CERTIFICATE_HOME%\ssl.truststore" "%ALFRESCO_KEYSTORE_HOME%\ssl.truststore"
copy /Y "%CERTIFICATE_HOME%\browser.p12" "%ALFRESCO_KEYSTORE_HOME%\browser.p12"
@echo ****************************************
@echo You must copy the following files to the correct location.
@echo %CERTIFICATE_HOME%\ssl.repo.client.keystore
@echo %CERTIFICATE_HOME%\ssl.repo.client.truststore
@echo eg. for Solr 4 the location is SOLR_HOME\workspace-SpacesStore\conf and SOLR_HOME\archive-SpacesStore\conf
@echo Please ensure that you set dir.keystore=%ALFRESCO_KEYSTORE_HOME% in alfresco-global.properties
@echo ****************************************

View File

@@ -1,82 +0,0 @@
#! /bin/sh
# Please edit the variables below to suit your installation
# Note: for an installation created by the Alfresco installer, you only need to edit ALFRESCO_HOME
# Alfresco installation directory
if [ -z "$ALFRESCO_HOME" ]; then
ALFRESCO_HOME=/opt/alfresco-5.2
echo "Setting ALFRESCO_HOME to $ALFRESCO_HOME"
fi
# The directory containing the alfresco keystores, as referenced by keystoreFile and truststoreFile attributes in tomcat/conf/server.xml
ALFRESCO_KEYSTORE_HOME=$ALFRESCO_HOME/alf_data/keystore
# Location in which new keystore files will be generated
if [ -z "$CERTIFICATE_HOME" ]; then
CERTIFICATE_HOME=$HOME
echo "Certificates will be generated in $CERTIFICATE_HOME and then moved to $ALFRESCO_KEYSTORE_HOME"
fi
# Java installation directory
JAVA_HOME=$ALFRESCO_HOME/java
# The repository server certificate subject name, as specified in tomcat/conf/tomcat-users.xml with roles="repository"
REPO_CERT_DNAME="CN=Alfresco Repository, OU=Unknown, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB"
# The SOLR client certificate subject name, as specified in tomcat/conf/tomcat-users.xml with roles="repoclient"
SOLR_CLIENT_CERT_DNAME="CN=Alfresco Repository Client, OU=Unknown, O=Alfresco Software Ltd., L=Maidenhead, ST=UK, C=GB"
# The number of days before the certificate expires
CERTIFICATE_VALIDITY=36525
# Stop
if [ -f "$ALFRESCO_HOME/alfresco.sh" ]; then "$ALFRESCO_HOME/alfresco.sh" stop; fi
# Ensure certificate output dir exists
mkdir -p "$CERTIFICATE_HOME"
# Remove old output files (note they are backed up elsewhere)
if [ -f "$CERTIFICATE_HOME/ssl.keystore" ]; then rm "$CERTIFICATE_HOME/ssl.keystore"; fi
if [ -f "$CERTIFICATE_HOME/ssl.truststore" ]; then rm "$CERTIFICATE_HOME/ssl.truststore"; fi
if [ -f "$CERTIFICATE_HOME/browser.p12" ]; then rm "$CERTIFICATE_HOME/browser.p12"; fi
if [ -f "$CERTIFICATE_HOME/ssl.repo.client.keystore" ]; then rm "$CERTIFICATE_HOME/ssl.repo.client.keystore"; fi
if [ -f "$CERTIFICATE_HOME/ssl.repo.client.truststore" ]; then rm "$CERTIFICATE_HOME/ssl.repo.client.truststore"; fi
# Generate new self-signed certificates for the repository and solr
"$JAVA_HOME/bin/keytool" -genkeypair -keyalg RSA -dname "$REPO_CERT_DNAME" -validity $CERTIFICATE_VALIDITY -alias ssl.repo -keypass kT9X6oe68t -keystore "$CERTIFICATE_HOME/ssl.keystore" -storetype JCEKS -storepass kT9X6oe68t
"$JAVA_HOME/bin/keytool" -exportcert -alias ssl.repo -file "$CERTIFICATE_HOME/ssl.repo.crt" -keystore "$CERTIFICATE_HOME/ssl.keystore" -storetype JCEKS -storepass kT9X6oe68t
"$JAVA_HOME/bin/keytool" -genkeypair -keyalg RSA -dname "$SOLR_CLIENT_CERT_DNAME" -validity $CERTIFICATE_VALIDITY -alias ssl.repo.client -keypass kT9X6oe68t -keystore "$CERTIFICATE_HOME/ssl.repo.client.keystore" -storetype JCEKS -storepass kT9X6oe68t
"$JAVA_HOME/bin/keytool" -exportcert -alias ssl.repo.client -file "$CERTIFICATE_HOME/ssl.repo.client.crt" -keystore "$CERTIFICATE_HOME/ssl.repo.client.keystore" -storetype JCEKS -storepass kT9X6oe68t
# Create trust relationship between repository and solr
"$JAVA_HOME/bin/keytool" -importcert -noprompt -alias ssl.repo.client -file "$CERTIFICATE_HOME/ssl.repo.client.crt" -keystore "$CERTIFICATE_HOME/ssl.truststore" -storetype JCEKS -storepass kT9X6oe68t
# Create trust relationship between repository and itself - used for searches
"$JAVA_HOME/bin/keytool" -importcert -noprompt -alias ssl.repo -file "$CERTIFICATE_HOME/ssl.repo.crt" -keystore "$CERTIFICATE_HOME/ssl.truststore" -storetype JCEKS -storepass kT9X6oe68t
# Create trust relationship between solr and repository
"$JAVA_HOME/bin/keytool" -importcert -noprompt -alias ssl.repo -file "$CERTIFICATE_HOME/ssl.repo.crt" -keystore "$CERTIFICATE_HOME/ssl.repo.client.truststore" -storetype JCEKS -storepass kT9X6oe68t
# Export repository keystore to pkcs12 format for browser compatibility
"$JAVA_HOME/bin/keytool" -importkeystore -srckeystore "$CERTIFICATE_HOME/ssl.keystore" -srcstorepass kT9X6oe68t -srcstoretype JCEKS -srcalias ssl.repo -srckeypass kT9X6oe68t -destkeystore "$CERTIFICATE_HOME/browser.p12" -deststoretype pkcs12 -deststorepass alfresco -destalias ssl.repo -destkeypass alfresco
# Ensure keystore dir actually exists
mkdir -p "$ALFRESCO_KEYSTORE_HOME"
# Back up old files
cp "$ALFRESCO_KEYSTORE_HOME/ssl.keystore" "$ALFRESCO_KEYSTORE_HOME/ssl.keystore.old"
cp "$ALFRESCO_KEYSTORE_HOME/ssl.truststore" "$ALFRESCO_KEYSTORE_HOME/ssl.truststore.old"
cp "$ALFRESCO_KEYSTORE_HOME/browser.p12" "$ALFRESCO_KEYSTORE_HOME/browser.p12.old"
# Install the new files
cp "$CERTIFICATE_HOME/ssl.keystore" "$ALFRESCO_KEYSTORE_HOME/ssl.keystore"
cp "$CERTIFICATE_HOME/ssl.truststore" "$ALFRESCO_KEYSTORE_HOME/ssl.truststore"
cp "$CERTIFICATE_HOME/browser.p12" "$ALFRESCO_KEYSTORE_HOME/browser.p12"
echo " "
echo "*******************************************"
echo "You must copy the following files to the correct location."
echo " "
echo " $CERTIFICATE_HOME/ssl.repo.client.keystore"
echo " $CERTIFICATE_HOME/ssl.repo.client.truststore"
echo " eg. for Solr 4 the location is SOLR_HOME/workspace-SpacesStore/conf/ and SOLR_HOME/archive-SpacesStore/conf/"
echo " "
echo "$ALFRESCO_KEYSTORE_HOME/browser.p12 has also been generated."
echo " "
echo "Please ensure that you set dir.keystore=$ALFRESCO_KEYSTORE_HOME in alfresco-global.properties"
echo "*******************************************"

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<properties>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<modules>

View File

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

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<organization>

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<properties>

View File

@@ -4,7 +4,7 @@
<!-- Resource defaultTransactionIsolation="-1" defaultAutoCommit="false" maxActive="100" initialSize="10" password="alfresco" username="alfresco" url="jdbc:mysql:///alfresco" driverClassName="org.gjt.mm.mysql.Driver" type="javax.sql.DataSource" auth="Container" name="jdbc/dataSource"/-->
<Environment override="false" type="java.lang.Boolean" name="properties/startup.enable" description="A flag that globally enables or disables startup of the major Alfresco subsystems." value="true"/>
<Environment override="false" type="java.lang.String" name="properties/dir.root" description="The filesystem directory below which content and index data is stored. Should be on a shared disk if this is a clustered installation."/>
<Environment override="false" type="java.lang.String" name="properties/hibernate.dialect" description="The fully qualified name of a org.hibernate.dialect.Dialect subclass that allows Hibernate to generate SQL optimized for a particular relational database. Choose from org.hibernate.dialect.DerbyDialect, org.hibernate.dialect.MySQLInnoDBDialect, org.alfresco.repo.domain.hibernate.dialect.AlfrescoOracle9Dialect, org.alfresco.repo.domain.hibernate.dialect.AlfrescoSybaseAnywhereDialect, org.alfresco.repo.domain.hibernate.dialect.AlfrescoSQLServerDialect, org.hibernate.dialect.PostgreSQLDialect"/>
<Environment override="false" type="java.lang.String" name="properties/hibernate.dialect" description="The fully qualified name of a org.hibernate.dialect.Dialect subclass that allows Hibernate to generate SQL optimized for a particular relational database. Choose from org.hibernate.dialect.DerbyDialect, org.hibernate.dialect.MySQLInnoDBDialect, org.alfresco.repo.domain.hibernate.dialect.AlfrescoOracle9Dialect, org.alfresco.repo.domain.hibernate.dialect.AlfrescoSybaseAnywhereDialect, org.alfresco.repo.domain.hibernate.dialect.SQLServerDialect, org.hibernate.dialect.PostgreSQLDialect"/>
<Environment override="false" type="java.lang.String" name="properties/hibernate.query.substitutions" description="Mapping from tokens in Hibernate queries to SQL tokens. For PostgreSQL, set this to &quot;true TRUE, false FALSE&quot;."/>
<Environment override="false" type="java.lang.Boolean" name="properties/hibernate.jdbc.use_get_generated_keys" description="Enable use of JDBC3 PreparedStatement.getGeneratedKeys() to retrieve natively generated keys after insert. Requires JDBC3+ driver. Set to false if your driver has problems with the Hibernate identifier generators. By default, tries to determine the driver capabilities using connection metadata."/>
<Environment override="false" type="java.lang.String" name="properties/hibernate.default_schema" description="Qualify unqualified table names with the given schema/tablespace in generated SQL. It may be necessary to set this when the target database has more than one schema."/>

View File

@@ -500,7 +500,7 @@
org.hibernate.dialect.MySQLInnoDBDialect,
org.alfresco.repo.domain.hibernate.dialect.AlfrescoOracle9Dialect,
org.alfresco.repo.domain.hibernate.dialect.AlfrescoSybaseAnywhereDialect,
org.alfresco.repo.domain.hibernate.dialect.AlfrescoSQLServerDialect, org.hibernate.dialect.PostgreSQLDialect</description>
org.alfresco.repo.domain.hibernate.dialect.SQLServerDialect, org.hibernate.dialect.PostgreSQLDialect</description>
<env-entry-name>properties/hibernate.dialect</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value/> <!-- Empty value included for JBoss compatibility -->

16
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>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
<packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name>
@@ -58,7 +58,7 @@
<dependency.aspectj.version>1.9.22.1</dependency.aspectj.version>
<dependency.spring.version>6.2.2</dependency.spring.version>
<dependency.spring-security.version>6.3.7</dependency.spring-security.version>
<dependency.spring-security.version>6.3.9</dependency.spring-security.version>
<dependency.antlr.version>3.5.3</dependency.antlr.version>
<dependency.jackson.version>2.17.2</dependency.jackson.version>
<dependency.cxf.version>4.1.0</dependency.cxf.version>
@@ -74,8 +74,8 @@
<dependency.guava.version>33.3.1-jre</dependency.guava.version>
<dependency.httpclient.version>4.5.14</dependency.httpclient.version>
<dependency.httpcore.version>4.4.16</dependency.httpcore.version>
<dependency.httpcomponents-httpclient5.version>5.4.1</dependency.httpcomponents-httpclient5.version>
<dependency.httpcomponents-httpcore5.version>5.3.3</dependency.httpcomponents-httpcore5.version>
<dependency.httpcomponents-httpclient5.version>5.4.4</dependency.httpcomponents-httpclient5.version>
<dependency.httpcomponents-httpcore5.version>5.3.4</dependency.httpcomponents-httpcore5.version>
<dependency.commons-httpclient.version>3.1-HTTPCLIENT-1265</dependency.commons-httpclient.version>
<dependency.xercesImpl.version>2.12.2</dependency.xercesImpl.version>
<dependency.slf4j.version>2.0.16</dependency.slf4j.version>
@@ -83,9 +83,9 @@
<dependency.groovy.version>3.0.23</dependency.groovy.version>
<dependency.tika.version>2.9.2</dependency.tika.version>
<dependency.truezip.version>7.7.10</dependency.truezip.version>
<dependency.poi.version>5.3.0</dependency.poi.version>
<dependency.poi.version>5.4.0</dependency.poi.version>
<dependency.jboss.logging.version>3.5.0.Final</dependency.jboss.logging.version>
<dependency.camel.version>4.6.0</dependency.camel.version> <!-- when bumping this version, please keep track/sync with included netty.io dependencies -->
<dependency.camel.version>4.11.0</dependency.camel.version> <!-- when bumping this version, please keep track/sync with included netty.io dependencies -->
<dependency.netty.version>4.1.118.Final</dependency.netty.version> <!-- must be in sync with camels transitive dependencies, e.g.: netty-common -->
<dependency.activemq.version>5.18.6</dependency.activemq.version>
<dependency.apache-compress.version>1.27.1</dependency.apache-compress.version>
@@ -114,7 +114,7 @@
<dependency.jakarta-json-path.version>2.9.0</dependency.jakarta-json-path.version>
<dependency.json-smart.version>2.5.2</dependency.json-smart.version>
<alfresco.googledrive.version>4.1.0</alfresco.googledrive.version>
<alfresco.aos-module.version>3.2.0</alfresco.aos-module.version>
<alfresco.aos-module.version>3.3.0-A1</alfresco.aos-module.version>
<alfresco.api-explorer.version>25.1.0</alfresco.api-explorer.version> <!-- Also in alfresco-enterprise-share -->
<alfresco.maven-plugin.version>2.2.0</alfresco.maven-plugin.version>
@@ -153,7 +153,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>HEAD</tag>
<tag>25.2.0.24</tag>
</scm>
<distributionManagement>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<dependencies>

View File

@@ -205,7 +205,7 @@ public class ProcessesImplTest extends TestCase implements RecognizedParamsExtra
{
// the tests are always run on PostgreSQL only
// Dialect dialect = (Dialect) applicationContext.getBean("dialect");
// if (dialect instanceof AlfrescoSQLServerDialect)
// if (dialect instanceof SQLServerDialect)
// {
// REPO-1104: we do not run this test on MS SQL server because it will fail
// until the Activiti defect related to REPO-1104 will be fixed

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>25.2.0.3-SNAPSHOT</version>
<version>25.2.0.24</version>
</parent>
<dependencies>

View File

@@ -4,21 +4,21 @@
* %%
* 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
* 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%
@@ -31,6 +31,7 @@ import java.util.List;
import java.util.Map;
import org.alfresco.repo.action.ParameterDefinitionImpl;
import org.alfresco.repo.action.access.ActionAccessRestriction;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
@@ -42,7 +43,7 @@ import org.alfresco.service.transaction.TransactionService;
/**
* Add features action executor implementation.
*
*
* @author Roy Wetherall
*/
public class AddFeaturesActionExecuter extends ActionExecuterAbstractBase
@@ -64,7 +65,7 @@ public class AddFeaturesActionExecuter extends ActionExecuterAbstractBase
/**
* Set the node service
*
*
* @param nodeService
* the node service
*/
@@ -75,7 +76,7 @@ public class AddFeaturesActionExecuter extends ActionExecuterAbstractBase
/**
* Set the transaction service
*
*
* @param transactionService
* the transaction service
*/
@@ -115,6 +116,7 @@ public class AddFeaturesActionExecuter extends ActionExecuterAbstractBase
// Build the aspect details
Map<String, Serializable> paramValues = ruleAction.getParameterValues();
removeActionContextParameter(paramValues);
for (Map.Entry<String, Serializable> entry : paramValues.entrySet())
{
if (entry.getKey().equals(PARAM_ASPECT_NAME) == true)
@@ -147,4 +149,12 @@ public class AddFeaturesActionExecuter extends ActionExecuterAbstractBase
paramList.add(new ParameterDefinitionImpl(PARAM_ASPECT_NAME, DataTypeDefinition.QNAME, true, getParamDisplayLabel(PARAM_ASPECT_NAME), false, "ac-aspects"));
}
/**
* Remove actionContext from the parameter values to declassify as an adhoc property
*/
private void removeActionContextParameter(Map<String, Serializable> paramValues)
{
paramValues.remove(ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME);
}
}

View File

@@ -456,7 +456,7 @@ public class ImporterActionExecuter extends ActionExecuterAbstractBase
}
else
{
File newdir = new File(extractDir + entry.getName());
File newdir = new File(extractDir + StringUtils.stripAccents(entry.getName()).replaceAll("\\?", "_"));
newdir.mkdirs();
}
}

View File

@@ -542,10 +542,7 @@ public class EventGenerator extends AbstractLifecycleBean implements Initializin
@Override
protected void onShutdown(ApplicationEvent applicationEvent)
{
if (eventSender != null)
{
eventSender.destroy();
}
// NOOP
}
protected class EventTransactionListener extends TransactionListenerAdapter

View File

@@ -52,7 +52,7 @@ public interface EventSender
}
/**
* It's called when the application context is closing, allowing {@link org.alfresco.repo.event2.EventGenerator} to perform cleanup operations.
* It's called when the bean instance is destroyed, allowing to perform cleanup operations.
*/
default void destroy()
{

View File

@@ -156,4 +156,13 @@ public class EventSenderFactoryBean extends AbstractFactoryBean<EventSender>
{
return event2MessageProducer;
}
@Override
protected void destroyInstance(EventSender eventSender)
{
if (eventSender != null)
{
eventSender.destroy();
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -994,7 +994,29 @@ public abstract class AbstractRenderingEngine extends ActionExecuterAbstractBase
{
String msg = "A rendition of name: " + renditionQName + " should have been created for source node: "
+ sourceNode;
throw new RenditionServiceException(msg);
if (logger.isDebugEnabled())
{
logger.debug(msg);
}
// Check if the node has the applied renditioned aspect, and if it does,
// remove the existing rendition node and assign the newly created rendition node.
if (nodeService.hasAspect(sourceNode, RenditionModel.ASPECT_RENDITIONED))
{
List<ChildAssociationRef> renditions = nodeService.getChildAssocs(sourceNode, RenditionModel.ASSOC_RENDITION, renditionQName);
if (!renditions.isEmpty())
{
ChildAssociationRef existingRendition = renditions.get(0);
nodeService.removeChild(sourceNode, existingRendition.getChildRef());
renditionAssoc = renditionNode;
if (logger.isDebugEnabled())
{
logger.debug("Removing the existing rendition node that doesn't have contentData and "
+ "assigning the newly created rendition node: " + renditionAssoc);
}
}
}
}
// Return the link between the source and the new, final rendition
return renditionAssoc;

View File

@@ -34,6 +34,7 @@ import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
@@ -66,6 +67,7 @@ public class LocalTransformClient implements TransformClient, InitializingBean
private ContentService contentService;
private RenditionService2Impl renditionService2;
private boolean directAccessUrlEnabled;
private int threadPoolSize;
private ExecutorService executorService;
private ThreadLocal<LocalTransform> transform = new ThreadLocal<>();
@@ -95,6 +97,11 @@ public class LocalTransformClient implements TransformClient, InitializingBean
this.directAccessUrlEnabled = directAccessUrlEnabled;
}
public void setThreadPoolSize(int threadPoolSize)
{
this.threadPoolSize = threadPoolSize;
}
public void setExecutorService(ExecutorService executorService)
{
this.executorService = executorService;
@@ -108,9 +115,11 @@ public class LocalTransformClient implements TransformClient, InitializingBean
PropertyCheck.mandatory(this, "contentService", contentService);
PropertyCheck.mandatory(this, "renditionService2", renditionService2);
PropertyCheck.mandatory(this, "directAccessUrlEnabled", directAccessUrlEnabled);
PropertyCheck.mandatory(this, "threadPoolSize", threadPoolSize);
if (executorService == null)
{
executorService = Executors.newCachedThreadPool();
var threadFactory = new ThreadFactoryBuilder().setNameFormat("local-transform-%d").build();
executorService = Executors.newFixedThreadPool(threadPoolSize, threadFactory);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -119,4 +119,13 @@ public interface RenditionService2
* Indicates if renditions are enabled. Set using the {@code system.thumbnail.generate} value.
*/
boolean isEnabled();
/**
* This method forces the content hash code for every {@code sourceNodeRef} renditions.
*
* @param sourceNodeRef
* the source node to update renditions hash code
*/
@NotAuditable
void forceRenditionsContentHashCode(NodeRef sourceNodeRef);
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -936,4 +936,35 @@ public class RenditionService2Impl implements RenditionService2, InitializingBea
}
}
@Override
public void forceRenditionsContentHashCode(NodeRef sourceNodeRef)
{
if (sourceNodeRef != null && nodeService.exists(sourceNodeRef))
{
List<ChildAssociationRef> renditions = getRenditionChildAssociations(sourceNodeRef);
if (renditions != null)
{
int sourceContentHashCode = getSourceContentHashCode(sourceNodeRef);
for (ChildAssociationRef rendition : renditions)
{
NodeRef renditionNode = rendition.getChildRef();
if (nodeService.hasAspect(renditionNode, RenditionModel.ASPECT_RENDITION2))
{
int renditionContentHashCode = getRenditionContentHashCode(renditionNode);
String renditionName = rendition.getQName().getLocalName();
if (sourceContentHashCode != renditionContentHashCode)
{
if (logger.isDebugEnabled())
{
logger.debug("Update content hash code for rendition " + renditionName + " of node "
+ sourceNodeRef);
}
nodeService.setProperty(renditionNode, PROP_RENDITION_CONTENT_HASH_CODE,
sourceContentHashCode);
}
}
}
}
}
}
}

View File

@@ -33,6 +33,7 @@ import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent
import org.alfresco.repo.security.authentication.AuthenticationException;
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.user.OIDCUserInfo;
/**
*

View File

@@ -69,6 +69,13 @@ public class IdentityServiceConfig
private boolean clientIdValidationDisabled;
private String adminConsoleRedirectPath;
private String signatureAlgorithms;
private String adminConsoleScopes;
private String passwordGrantScopes;
private String issuerAttribute;
private String firstNameAttribute;
private String lastNameAttribute;
private String emailAttribute;
private long jwtClockSkewMs;
/**
*
@@ -329,4 +336,78 @@ public class IdentityServiceConfig
{
this.signatureAlgorithms = signatureAlgorithms;
}
public String getIssuerAttribute()
{
return issuerAttribute;
}
public void setIssuerAttribute(String issuerAttribute)
{
this.issuerAttribute = issuerAttribute;
}
public Set<String> getAdminConsoleScopes()
{
return Stream.of(adminConsoleScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setAdminConsoleScopes(String adminConsoleScopes)
{
this.adminConsoleScopes = adminConsoleScopes;
}
public Set<String> getPasswordGrantScopes()
{
return Stream.of(passwordGrantScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setPasswordGrantScopes(String passwordGrantScopes)
{
this.passwordGrantScopes = passwordGrantScopes;
}
public void setFirstNameAttribute(String firstNameAttribute)
{
this.firstNameAttribute = firstNameAttribute;
}
public void setLastNameAttribute(String lastNameAttribute)
{
this.lastNameAttribute = lastNameAttribute;
}
public void setEmailAttribute(String emailAttribute)
{
this.emailAttribute = emailAttribute;
}
public void setJwtClockSkewMs(long jwtClockSkewMs)
{
this.jwtClockSkewMs = jwtClockSkewMs;
}
public String getFirstNameAttribute()
{
return firstNameAttribute;
}
public String getLastNameAttribute()
{
return lastNameAttribute;
}
public String getEmailAttribute()
{
return emailAttribute;
}
public long getJwtClockSkewMs()
{
return jwtClockSkewMs;
}
}

View File

@@ -34,6 +34,9 @@ import java.util.Optional;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
/**
* Allows to interact with the Identity Service
*/
@@ -66,11 +69,11 @@ public interface IdentityServiceFacade
*
* @param token
* {@link String} with encoded access token value.
* @param principalAttribute
* {@link String} the attribute name used to access the user's name from the user info response.
* @return {@link OIDCUserInfo} containing user claims.
* @param userInfoAttrMapping
* {@link UserInfoAttrMapping} containing the mapping of claims.
* @return {@link DecodedTokenUser} containing user claims or {@link Optional#empty()} if the token does not contain a username claim.
*/
Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute);
Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping);
/**
* Gets a client registration

View File

@@ -72,6 +72,7 @@ import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
@@ -96,6 +97,7 @@ import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
@@ -124,6 +126,8 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
/**
* Creates an instance of {@link IdentityServiceFacade}. <br>
@@ -134,6 +138,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
private static final String DEFAULT_ISSUER_ATTR = "issuer";
private boolean enabled;
private SpringBasedIdentityServiceFacadeFactory factory;
@@ -206,9 +211,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
@Override
public Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute)
public Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
{
return getTargetFacade().getUserInfo(token, principalAttribute);
return getTargetFacade().getUserInfo(token, userInfoAttrMapping);
}
@Override
@@ -277,7 +282,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
{
final RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter(), new MappingJackson2HttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
@@ -385,8 +390,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{
private final IdentityServiceConfig config;
private static final Set<String> SCOPES = Set.of("openid", "profile", "email");
ClientRegistrationProvider(IdentityServiceConfig config)
{
this.config = requireNonNull(config);
@@ -456,11 +459,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
.map(URI::toASCIIString)
.orElse(null);
final String issuerUri = Optional.of(metadata)
.map(OIDCProviderMetadata::getIssuer)
.map(Issuer::getValue)
var metadataIssuer = getMetadataIssuer(metadata, config);
final String issuerUri = metadataIssuer
.orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ? config.getAuthServerUrl() : config.getIssuerUrl());
final var usernameAttribute = StringUtils.isNotBlank(config.getPrincipalAttribute()) ? config.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
return ClientRegistration
.withRegistrationId("ids")
.authorizationUri(authUri)
@@ -468,6 +472,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.issuerUri(issuerUri)
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
.userNameAttributeName(usernameAttribute)
.scope(getSupportedScopes(metadata.getScopes()))
.providerConfigurationMetadata(createMetadata(metadata))
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
@@ -501,11 +506,17 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private Set<String> getSupportedScopes(Scope scopes)
{
return scopes.stream().filter(scope -> SCOPES.contains(scope.getValue()))
return scopes.stream()
.filter(this::hasPasswordGrantScope)
.map(Identifier::getValue)
.collect(Collectors.toSet());
}
private boolean hasPasswordGrantScope(Scope.Value scope)
{
return config.getPasswordGrantScopes().contains(scope.getValue());
}
private Optional<OIDCProviderMetadata> extractMetadata(RestOperations rest, URI metadataUri)
{
final String response;
@@ -552,6 +563,18 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
}
private static Optional<String> getMetadataIssuer(OIDCProviderMetadata metadata, IdentityServiceConfig config)
{
return DEFAULT_ISSUER_ATTR.equals(config.getIssuerAttribute()) ? Optional.of(metadata)
.map(OIDCProviderMetadata::getIssuer)
.map(Issuer::getValue)
: Optional.of(metadata)
.map(OIDCProviderMetadata::getCustomParameters)
.map(map -> map.get(config.getIssuerAttribute()))
.filter(String.class::isInstance)
.map(String.class::cast);
}
static class JwtDecoderProvider
{
private static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
@@ -651,7 +674,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
{
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)));
validators.add(new JwtTimestampValidator(Duration.of(config.getJwtClockSkewMs(), ChronoUnit.MILLIS)));
validators.add(new JwtIssuerValidator(providerDetails.getIssuerUri()));
if (!config.isClientIdValidationDisabled())
{

View File

@@ -30,52 +30,33 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapper;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapper;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
/**
* This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo} from {@link IdentityServiceFacade.DecodedAccessToken} or {@link UserInfo} and creates a new user if it does not exist in the repository.
* This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository.
*/
public class IdentityServiceJITProvisioningHandler
{
private final IdentityServiceConfig identityServiceConfig;
private final IdentityServiceFacade identityServiceFacade;
private final PersonService personService;
private final TransactionService transactionService;
private final BiFunction<DecodedAccessToken, String, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = (token, usernameMappingClaim) -> {
Optional<String> firstName = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> lastName = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> email = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
return Optional.ofNullable(token.getClaim(Optional.ofNullable(usernameMappingClaim)
.filter(StringUtils::isNotBlank)
.orElse(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)))
.filter(String.class::isInstance)
.map(String.class::cast)
.map(this::normalizeUserId)
.map(username -> new OIDCUserInfo(username, firstName.orElse(""), lastName.orElse(""), email.orElse("")));
};
private final IdentityServiceConfig identityServiceConfig;
private UserInfoAttrMapping userInfoAttrMapping;
private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
PersonService personService,
@@ -88,92 +69,95 @@ public class IdentityServiceJITProvisioningHandler
this.identityServiceConfig = identityServiceConfig;
}
/**
* Extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository. Call to the UserInfo endpoint is made only if the token does not contain a username claim or if user needs to be created and some of the {@link OIDCUserInfo} fields are empty.
*/
public Optional<OIDCUserInfo> extractUserInfoAndCreateUserIfNeeded(String bearerToken)
{
Optional<OIDCUserInfo> userInfoResponse = Optional.ofNullable(bearerToken)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> extractUserInfoResponseFromAccessToken(token)
.filter(userInfo -> StringUtils.isNotEmpty(userInfo.username()))
.or(() -> extractUserInfoResponseFromEndpoint(token)));
if (transactionService.isReadOnly() || userInfoResponse.isEmpty())
if (userInfoAttrMapping == null)
{
return userInfoResponse;
initMappers(identityServiceConfig);
}
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Optional<OIDCUserInfo>>() {
Optional<OIDCUserInfo> oidcUserInfo = Optional.ofNullable(bearerToken)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> extractUserInfoResponseFromAccessToken(token).filter(decodedTokenUser -> StringUtils.isNotEmpty(decodedTokenUser.username()))
.or(() -> extractUserInfoResponseFromEndpoint(token, userInfoAttrMapping)))
.map(tokenUserToOIDCUserMapper::toOIDCUser);
if (transactionService.isReadOnly() || oidcUserInfo.isEmpty())
{
return oidcUserInfo;
}
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<>() {
@Override
public Optional<OIDCUserInfo> doWork() throws Exception
{
return userInfoResponse.map(userInfo -> {
if (userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username()))
return oidcUserInfo.map(oidcUser -> {
if (userDoesNotExistsAndCanBeCreated(oidcUser))
{
if (!userInfo.allFieldsNotEmpty())
if (!oidcUser.allFieldsNotEmpty())
{
userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo);
oidcUser = extractUserInfoResponseFromEndpoint(bearerToken, userInfoAttrMapping)
.map(tokenUserToOIDCUserMapper::toOIDCUser)
.orElse(oidcUser);
}
Map<QName, Serializable> properties = new HashMap<>();
properties.put(ContentModel.PROP_USERNAME, userInfo.username());
properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName());
properties.put(ContentModel.PROP_EMAIL, userInfo.email());
properties.put(ContentModel.PROP_ORGID, "");
properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null);
properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota
personService.createPerson(properties);
createPerson(oidcUser);
}
return userInfo;
return oidcUser;
});
}
}, AuthenticationUtil.getSystemUserName());
}
private Optional<OIDCUserInfo> extractUserInfoResponseFromAccessToken(String bearerToken)
private void initMappers(IdentityServiceConfig identityServiceConfig)
{
this.userInfoAttrMapping = initUserInfoAttrMapping(identityServiceConfig);
this.tokenUserToOIDCUserMapper = new TokenUserToOIDCUserMapper(personService);
this.tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
}
private boolean userDoesNotExistsAndCanBeCreated(OIDCUserInfo userInfo)
{
return userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username());
}
private Optional<DecodedTokenUser> extractUserInfoResponseFromAccessToken(String bearerToken)
{
return Optional.ofNullable(bearerToken)
.map(identityServiceFacade::decodeToken)
.flatMap(decodedToken -> mapTokenToUserInfoResponse.apply(decodedToken,
identityServiceConfig.getPrincipalAttribute()));
.flatMap(tokenToDecodedTokenUserMapper::toDecodedTokenUser);
}
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
private Optional<DecodedTokenUser> extractUserInfoResponseFromEndpoint(String bearerToken, UserInfoAttrMapping userInfoAttrMapping)
{
return identityServiceFacade.getUserInfo(bearerToken,
StringUtils.isNotBlank(identityServiceConfig.getPrincipalAttribute()) ? identityServiceConfig.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty())
.map(userInfo -> new OIDCUserInfo(normalizeUserId(userInfo.username()),
Optional.ofNullable(userInfo.firstName()).orElse(""),
Optional.ofNullable(userInfo.lastName()).orElse(""),
Optional.ofNullable(userInfo.email()).orElse("")));
return identityServiceFacade.getUserInfo(bearerToken, userInfoAttrMapping)
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty());
}
/**
* Normalizes a user id, taking into account existing user accounts and case sensitivity settings.
*
* @param userId
* the user id
* @return the string
*/
private String normalizeUserId(final String userId)
private void createPerson(OIDCUserInfo userInfo)
{
if (userId == null)
{
return null;
}
Map<QName, Serializable> properties = new HashMap<>();
properties.put(ContentModel.PROP_USERNAME, userInfo.username());
properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName());
properties.put(ContentModel.PROP_EMAIL, userInfo.email());
properties.put(ContentModel.PROP_ORGID, "");
properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null);
properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota
String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>() {
@Override
public String doWork() throws Exception
{
return personService.getUserIdentifier(userId);
}
}, AuthenticationUtil.getSystemUserName());
return normalized == null ? userId : normalized;
personService.createPerson(properties);
}
private UserInfoAttrMapping initUserInfoAttrMapping(IdentityServiceConfig identityServiceConfig)
{
return new UserInfoAttrMapping(identityServiceFacade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
identityServiceConfig.getFirstNameAttribute(),
identityServiceConfig.getLastNameAttribute(),
identityServiceConfig.getEmailAttribute());
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -38,6 +38,7 @@ import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
/**
* A {@link RemoteUserMapper} implementation that detects and validates JWTs issued by the Alfresco Identity Service.

View File

@@ -30,21 +30,12 @@ import static java.util.Objects.requireNonNull;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
@@ -59,27 +50,35 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRe
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
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.OAuth2AuthenticationException;
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.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
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 DefaultOAuth2UserService defaultOAuth2UserService;
private final ClientRegistration clientRegistration;
private final JwtDecoder jwtDecoder;
@@ -93,6 +92,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations, clientRegistration));
this.defaultOAuth2UserService = createOAuth2UserService(restOperations);
}
@Override
@@ -121,51 +121,18 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
}
@Override
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter, String principalAttribute)
public Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
{
return Optional.ofNullable(tokenParameter)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> Optional.ofNullable(clientRegistration)
.map(ClientRegistration::getProviderDetails)
.map(ClientRegistration.ProviderDetails::getUserInfoEndpoint)
.map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUri)
.flatMap(uri -> {
try
{
return Optional.of(
new UserInfoRequest(new URI(uri), new BearerAccessToken(token)).toHTTPRequest().send());
}
catch (IOException | URISyntaxException e)
{
LOGGER.warn("Failed to get user information. Reason: " + e.getMessage());
return Optional.empty();
}
})
.flatMap(httpResponse -> {
try
{
UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
if (userInfoResponse instanceof UserInfoErrorResponse userInfoErrorResponse)
{
String errorMessage = Optional.ofNullable(userInfoErrorResponse.getErrorObject())
.map(ErrorObject::getDescription)
.orElse("No error description found");
LOGGER.warn("User Info Request failed: " + errorMessage);
throw new UserInfoException(errorMessage);
}
return Optional.of(userInfoResponse);
}
catch (ParseException e)
{
LOGGER.warn("Failed to parse user info response. Reason: " + e.getMessage());
return Optional.empty();
}
})
.map(UserInfoResponse::toSuccessResponse)
.map(UserInfoSuccessResponse::getUserInfo))
.map(userInfo -> new OIDCUserInfo(userInfo.getStringClaim(principalAttribute), userInfo.getGivenName(),
userInfo.getFamilyName(), userInfo.getEmailAddress()));
try
{
return Optional.ofNullable(defaultOAuth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, getSpringAccessToken(token))))
.flatMap(oAuth2User -> mapOAuth2UserToDecodedTokenUser(oAuth2User, userInfoAttrMapping));
}
catch (OAuth2AuthenticationException exception)
{
LOGGER.warn("User Info Request failed: " + exception.getMessage());
return Optional.empty();
}
}
@Override
@@ -202,11 +169,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
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 OAuth2AccessToken expiredAccessToken = getSpringAccessToken("JUST_FOR_FULFILLING_THE_SPRING_API");
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken,
@@ -258,6 +221,26 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
return client;
}
private static DefaultOAuth2UserService createOAuth2UserService(RestOperations rest)
{
final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
userService.setRestOperations(rest);
return userService;
}
private Optional<DecodedTokenUser> mapOAuth2UserToDecodedTokenUser(OAuth2User oAuth2User, UserInfoAttrMapping userInfoAttrMapping)
{
var preferredUsername = Optional.ofNullable(oAuth2User.getAttribute(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast)
.filter(StringUtils::isNotEmpty);
var userName = Optional.ofNullable(oAuth2User.getName()).filter(username -> !username.isEmpty()).or(() -> preferredUsername);
return userName.map(name -> DecodedTokenUser.validateAndCreate(name,
oAuth2User.getAttribute(userInfoAttrMapping.firstNameClaim()),
oAuth2User.getAttribute(userInfoAttrMapping.lastNameClaim()),
oAuth2User.getAttribute(userInfoAttrMapping.emailClaim())));
}
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest,
ClientRegistration clientRegistration)
{
@@ -288,6 +271,16 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
};
}
private static OAuth2AccessToken getSpringAccessToken(String token)
{
// Just for fulfilling the Spring API
return new OAuth2AccessToken(
TokenType.BEARER,
token,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
}
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
{
private final OAuth2AccessTokenResponse tokenResponse;

View File

@@ -70,7 +70,6 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
private static final Set<String> SCOPES = Set.of("openid", "profile", "email", "offline_access");
private IdentityServiceConfig identityServiceConfig;
private IdentityServiceFacade identityServiceFacade;
@@ -225,11 +224,16 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set<String> getSupportedScopes(Scope scopes)
{
return scopes.stream()
.filter(scope -> SCOPES.contains(scope.getValue()))
.filter(this::hasAdminConsoleScope)
.map(Identifier::getValue)
.collect(Collectors.toSet());
}
private boolean hasAdminConsoleScope(Scope.Value scope)
{
return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue());
}
private String getRedirectUri(String requestURL)
{
try

View File

@@ -0,0 +1,66 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
public class AccessTokenToDecodedTokenUserMapper
{
private static final String DEFAULT_USERNAME_CLAIM = PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
private final UserInfoAttrMapping userInfoAttrMapping;
public AccessTokenToDecodedTokenUserMapper(UserInfoAttrMapping userInfoAttrMapping)
{
this.userInfoAttrMapping = userInfoAttrMapping;
}
/**
* Maps the given {@link IdentityServiceFacade.DecodedAccessToken} to a {@link DecodedTokenUser}.
*
* @param token
* the token to map
* @return the mapped {@link DecodedTokenUser} or {@link Optional#empty()} if the token does not contain a username claim
*/
public Optional<DecodedTokenUser> toDecodedTokenUser(IdentityServiceFacade.DecodedAccessToken token)
{
Object firstName = token.getClaim(userInfoAttrMapping.firstNameClaim());
Object lastName = token.getClaim(userInfoAttrMapping.lastNameClaim());
Object email = token.getClaim(userInfoAttrMapping.emailClaim());
return Optional.ofNullable(token.getClaim(Optional.ofNullable(userInfoAttrMapping.usernameClaim())
.filter(StringUtils::isNotBlank)
.orElse(DEFAULT_USERNAME_CLAIM)))
.filter(String.class::isInstance)
.map(String.class::cast)
.map(username -> DecodedTokenUser.validateAndCreate(username, firstName, lastName, email));
}
}

View File

@@ -0,0 +1,44 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import java.util.Optional;
public record DecodedTokenUser(String username, String firstName, String lastName, String email)
{
private static final String EMPTY_STRING = "";
public static DecodedTokenUser validateAndCreate(String username, Object firstName, Object lastName, Object email)
{
return new DecodedTokenUser(username, getStringVal(firstName), getStringVal(lastName), getStringVal(email));
}
private static String getStringVal(Object firstName)
{
return Optional.ofNullable(firstName).filter(String.class::isInstance).map(String.class::cast).orElse(EMPTY_STRING);
}
}

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
package org.alfresco.repo.security.authentication.identityservice.user;
import java.util.stream.Stream;

View File

@@ -0,0 +1,76 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.cmr.security.PersonService;
public class TokenUserToOIDCUserMapper
{
private final PersonService personService;
public TokenUserToOIDCUserMapper(PersonService personService)
{
this.personService = personService;
}
/**
* Maps a decoded token user to an OIDC user where the user id (username) is normalized.
*
* @param decodedTokenUser
* the decoded token user
* @return the OIDC user
*/
public OIDCUserInfo toOIDCUser(DecodedTokenUser decodedTokenUser)
{
return new OIDCUserInfo(usernameToUserId(decodedTokenUser.username()), decodedTokenUser.firstName(), decodedTokenUser.lastName(), decodedTokenUser.email());
}
/**
* Normalizes a username, taking into account existing user accounts and case sensitivity settings.
*
* @param caseSensitiveUserName
* the case-sensitive username
* @return the string
*/
private String usernameToUserId(final String caseSensitiveUserName)
{
if (caseSensitiveUserName == null)
{
return null;
}
String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>() {
@Override
public String doWork() throws Exception
{
return personService.getUserIdentifier(caseSensitiveUserName);
}
}, AuthenticationUtil.getSystemUserName());
return normalized == null ? caseSensitiveUserName : normalized;
}
}

View File

@@ -0,0 +1,41 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
/**
* The UserInfoAttrMapping record represents the mapping of claims fetched from the UserInfo endpoint to create an Alfresco user.
*
* @param usernameClaim
* the claim that represents the username
* @param firstNameClaim
* the claim that represents the first name
* @param lastNameClaim
* the claim that represents the last name
* @param emailClaim
* the claim that represents the email
*/
public record UserInfoAttrMapping(String usernameClaim, String firstNameClaim, String lastNameClaim, String emailClaim)
{}

View File

@@ -141,9 +141,10 @@
<property name="childByNameCache" ref="node.childByNameCache"/>
<property name="cachingThreshold" value="${nodes.bulkLoad.cachingThreshold}"/>
</bean>
<bean id="nodeDAO.org.alfresco.repo.domain.dialect.Dialect" class="org.alfresco.repo.domain.node.ibatis.NodeDAOImpl" parent="nodeDAObase" />
<bean id="nodeDAO.org.alfresco.repo.domain.dialect.MySQLInnoDBDialect" class="org.alfresco.repo.domain.node.ibatis.NodeDAOImpl$MySQL" parent="nodeDAO.org.alfresco.repo.domain.dialect.Dialect" />
<bean id="nodeDAO.org.alfresco.repo.domain.dialect.AlfrescoSQLServerDialect" class="org.alfresco.repo.domain.node.ibatis.NodeDAOImpl$MSSQL" parent="nodeDAO.org.alfresco.repo.domain.dialect.Dialect" />
<bean id="nodeDAO.org.alfresco.repo.domain.dialect.SQLServerDialect" class="org.alfresco.repo.domain.node.ibatis.NodeDAOImpl$MSSQL" parent="nodeDAO.org.alfresco.repo.domain.dialect.Dialect" />
<!-- WARNING: Experimental/unsupported - see MySQLClusterNDBDialect ! -->
<bean id="nodeDAO.org.alfresco.repo.domain.dialect.AlfrescoMySQLClusterNDBDialect" class="org.alfresco.repo.domain.node.ibatis.NodeDAOImpl$MySQLClusterNDB" parent="nodeDAO.org.alfresco.repo.domain.dialect.Dialect" />

View File

@@ -82,6 +82,7 @@
<property name="contentService" ref="contentService" />
<property name="renditionService2" ref="renditionService2" />
<property name="directAccessUrlEnabled" value="${local.transform.directAccessUrl.enabled}"/>
<property name="threadPoolSize" value="${local.transform.threadPoolSize}" />
</bean>
<bean id="synchronousTransformClient" parent="localSynchronousTransformClient" />

View File

@@ -1351,6 +1351,9 @@ restApi.directAccessUrl.defaultExpiryTimeInSec=30
# Controls whether direct access url URLs may be used in transforms.
local.transform.directAccessUrl.enabled=true
# Controls size of thread pool used for transforms.
local.transform.threadPoolSize=8
# Creates additional indexes on alf_node and alf_transaction. Recommended for large repositories.
system.new-node-transaction-indexes.ignored=true

View File

@@ -149,6 +149,15 @@
<property name="principalAttribute">
<value>${identity-service.principal-attribute:preferred_username}</value>
</property>
<property name="firstNameAttribute">
<value>${identity-service.first-name-attribute:given_name}</value>
</property>
<property name="lastNameAttribute">
<value>${identity-service.last-name-attribute:family_name}</value>
</property>
<property name="emailAttribute">
<value>${identity-service.email-attribute:email}</value>
</property>
<property name="clientIdValidationDisabled">
<value>${identity-service.client-id.validation.disabled:true}</value>
</property>
@@ -158,6 +167,18 @@
<property name="signatureAlgorithms">
<value>${identity-service.signature-algorithms:RS256,PS256}</value>
</property>
<property name="adminConsoleScopes">
<value>${identity-service.admin-console.scopes:openid,profile,email,offline_access}</value>
</property>
<property name="passwordGrantScopes">
<value>${identity-service.password-grant.scopes:openid,profile,email}</value>
</property>
<property name="issuerAttribute">
<value>${identity-service.issuer-attribute:issuer}</value>
</property>
<property name="jwtClockSkewMs">
<value>${identity-service.jwt-clock-skew-ms:0}</value>
</property>
</bean>
<!-- Enable control over mapping between request and user ID -->
@@ -219,4 +240,4 @@
<ref bean="transactionService" />
</property>
</bean>
</beans>
</beans>

View File

@@ -12,4 +12,11 @@ identity-service.resource=alfresco
identity-service.credentials.secret=
identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
identity-service.signature-algorithms=RS256,PS256
identity-service.signature-algorithms=RS256,PS256
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.admin-console.scopes=openid,profile,email,offline_access
identity-service.password-grant.scopes=openid,profile,email
identity-service.issuer-attribute=issuer
identity-service.jwt-clock-skew-ms=0

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -54,6 +54,7 @@ import org.alfresco.util.testing.category.NonBuildTests;
// From AppContext05TestSuite
org.alfresco.repo.domain.node.NodeDAOTest.class,
org.alfresco.repo.domain.subscriptions.SubscriptionDAOTest.class,
org.alfresco.repo.security.permissions.impl.AclDaoComponentTest.class,
org.alfresco.repo.domain.contentdata.ContentDataDAOTest.class,
org.alfresco.repo.domain.encoding.EncodingDAOTest.class,

View File

@@ -37,6 +37,8 @@ import org.alfresco.repo.security.authentication.identityservice.SpringBasedIden
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest;
import org.alfresco.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
@@ -149,6 +151,8 @@ import org.alfresco.util.testing.category.NonBuildTests;
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
SpringBasedIdentityServiceFacadeUnitTest.class,
IdentityServiceJITProvisioningHandlerUnitTest.class,
AccessTokenToDecodedTokenUserMapperUnitTest.class,
TokenUserToOIDCUserMapperUnitTest.class,
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,

View File

@@ -35,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.ActionImpl;
import org.alfresco.repo.action.access.ActionAccessRestriction;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.service.cmr.action.ActionDefinition;
import org.alfresco.service.cmr.action.ParameterDefinition;
@@ -162,4 +163,20 @@ public class AddFeaturesActionExecuterTest extends BaseSpringTest
I18NUtil.setLocale(Locale.getDefault());
}
/**
* Test check actionContext param is removed from adhoc properties
*/
@Test
public void testCheckActionContext()
{
// Execute the action
ActionImpl action = new ActionImpl(null, ID, AddFeaturesActionExecuter.NAME, null);
action.setParameterValue(ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME, ActionAccessRestriction.V1_ACTION_CONTEXT);
action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_CLASSIFIABLE);
this.executer.execute(action, this.nodeRef);
// Ensure the actionContext parameter has been removed
assertFalse(nodeService.getProperties(this.nodeRef).containsKey(QName.createQName(ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME)));
}
}

View File

@@ -62,6 +62,7 @@ import org.alfresco.util.test.junitrules.ApplicationContextInit;
*
* @author abalmus
*/
@SuppressWarnings("PMD.UnitTestShouldIncludeAssert")
public class ImporterActionExecuterTest
{
// Rule to initialise the default Alfresco spring configuration
@@ -299,6 +300,46 @@ public class ImporterActionExecuterTest
});
}
@Test
public void testUnzipZipFileHavingAccentCharInFolderName() throws IOException
{
final RetryingTransactionHelper retryingTransactionHelper = serviceRegistry.getRetryingTransactionHelper();
retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback<Void>() {
@Override
public Void execute() throws Throwable
{
NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
// create test data
NodeRef zipFileNodeRef = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_CONTENT).getChildRef();
NodeRef targetFolderNodeRef = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_FOLDER).getChildRef();
putContent(zipFileNodeRef, "import-archive-test/accentCharTestZip.zip");
Action action = createAction(zipFileNodeRef, "ImporterActionExecuterTestActionDefinition", targetFolderNodeRef);
try
{
importerActionExecuter.setUncompressedBytesLimit("100000");
importerActionExecuter.execute(action, zipFileNodeRef);
NodeRef importedFolder = nodeService.getChildByName(targetFolderNodeRef, ContentModel.ASSOC_CONTAINS, "accentCharTestZip");
assertNotNull("unzip action failed", importedFolder);
assertTrue("multiple folder structure created", nodeService.getChildAssocs(importedFolder).size() == 1);
}
finally
{
// clean test data
nodeService.deleteNode(targetFolderNodeRef);
nodeService.deleteNode(zipFileNodeRef);
}
return null;
}
});
}
private void putContent(NodeRef zipFileNodeRef, String resource)
{
URL url = AbstractContentTransformerTest.class.getClassLoader().getResource(resource);

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -43,9 +43,9 @@ import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.testing.category.NeverRunsTests;
import org.alfresco.util.testing.category.DBTests;
@Category({OwnJVMTestsCategory.class, NeverRunsTests.class})
@Category({OwnJVMTestsCategory.class, DBTests.class})
public class SubscriptionDAOTest extends TestCase
{
private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -55,6 +55,7 @@ import org.alfresco.repo.thumbnail.ThumbnailRegistry;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
@@ -62,6 +63,7 @@ import org.alfresco.service.cmr.repository.MimetypeService;
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.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService;
@@ -129,6 +131,7 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
protected static final String ADMIN = "admin";
protected static final String DOC_LIB = "doclib";
protected static final String PDF = "pdf";
private CronExpression origLocalTransCron;
private CronExpression origRenditionCron;
@@ -554,4 +557,28 @@ public abstract class AbstractRenditionIntegrationTest extends BaseSpringTest
return renditionContentHashCode;
}
/**
* Helper method which gets the content hash code from the supplied source node (the equivalent method from {@link RenditionService2Impl} is not public)
*
* @param sourceNodeRef
* the source node
*
* @return -1 in case of there is no content, otherwise, the actual content hash code otherwise
*/
protected int getSourceContentHashCode(NodeRef sourceNodeRef)
{
int hashCode = -1;
ContentData contentData = DefaultTypeConverter.INSTANCE.convert(ContentData.class, nodeService.getProperty(sourceNodeRef, PROP_CONTENT));
if (contentData != null)
{
// Originally we used the contentData URL, but that is not enough if the mimetype changes.
String contentString = contentData.getContentUrl() + contentData.getMimetype();
if (contentString != null)
{
hashCode = contentString.hashCode();
}
}
return hashCode;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -445,6 +445,61 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
assertEquals(TOTAL_NODES, countModifier(nodes, user1));
}
@Test
public void testForceRenditionsContentHashCode()
{
// Create a node
NodeRef sourceNodeRef = createSource(ADMIN, "quick.docx");
assertNotNull("Node not generated", sourceNodeRef);
// Get content hash code for the source node
int sourceNodeContentHashCode = getSourceContentHashCode(sourceNodeRef);
// Trigger the pdf rendition
render(ADMIN, sourceNodeRef, PDF);
NodeRef pdfRenditionNodeRef = waitForRendition(ADMIN, sourceNodeRef, PDF, true);
assertNotNull("pdf rendition was not generated", pdfRenditionNodeRef);
assertNotNull("pdf rendition was not generated", nodeService.getProperty(pdfRenditionNodeRef, PROP_CONTENT));
// Check the pdf rendition content hash code is valid
int pdfRenditionContentHashCode = getRenditionContentHashCode(pdfRenditionNodeRef);
assertEquals("pdf rendition content hash code is different from source node content hash code", sourceNodeContentHashCode, pdfRenditionContentHashCode);
// Trigger the doc lib rendition
render(ADMIN, sourceNodeRef, DOC_LIB);
NodeRef docLibRenditionNodeRef = waitForRendition(ADMIN, sourceNodeRef, DOC_LIB, true);
assertNotNull("doc lib rendition was not generated", docLibRenditionNodeRef);
assertNotNull("doc lib rendition was not generated", nodeService.getProperty(docLibRenditionNodeRef, PROP_CONTENT));
// Check the doc lib rendition content hash code is valid
int docLibenditionContentHashCode = getRenditionContentHashCode(docLibRenditionNodeRef);
assertEquals("doc lib rendition content hash code is different from source node content hash code", sourceNodeContentHashCode, docLibenditionContentHashCode);
// Update the source node content
updateContent(ADMIN, sourceNodeRef, "quick.docx");
// Get source node content hash code after update
int sourceNodeContentHashCode2 = getSourceContentHashCode(sourceNodeRef);
// Check content hash code are different after content update
assertNotEquals("Source node content hash code is the same after content update", sourceNodeContentHashCode, sourceNodeContentHashCode2);
assertNotEquals("pdf rendition content hash code is the same after content update", sourceNodeContentHashCode2, pdfRenditionContentHashCode);
assertNotEquals("doc lib rendition content hash code is the same after content update", sourceNodeContentHashCode2, docLibenditionContentHashCode);
// Forces the content hash code for every source node renditions
AuthenticationUtil.runAs(() -> {
renditionService2.forceRenditionsContentHashCode(sourceNodeRef);
return null;
}, ADMIN);
// Check the renditions content hash code are now the same as the latest source node content hash code
int pdfRenditionContentHashCode2 = getRenditionContentHashCode(pdfRenditionNodeRef);
int docLibenditionContentHashCode2 = getRenditionContentHashCode(docLibRenditionNodeRef);
assertEquals("pdf rendition content hash code is different from latest source node content hash code", sourceNodeContentHashCode2, pdfRenditionContentHashCode2);
assertEquals("doc lib rendition content hash code is different from latest source node content hash code", sourceNodeContentHashCode2, docLibenditionContentHashCode2);
}
private int countModifier(List<NodeRef> nodes, String user)
{
int count = 0;
@@ -662,4 +717,63 @@ public class RenditionService2IntegrationTest extends AbstractRenditionIntegrati
renditionService2.setEnabled(true);
}
}
@Test
public void testRecreationOfRendition2()
{
renditionService2.setEnabled(true);
try
{
NodeRef sourceNodeRef = createSource(ADMIN, "quick.docx");
assertNotNull("Node not generated", sourceNodeRef);
// Get content hash code for the source node.
int sourceNodeContentHashCode = getSourceContentHashCode(sourceNodeRef);
// Trigger the pdf rendition.
render(ADMIN, sourceNodeRef, PDF);
NodeRef pdfRenditionNodeRef = waitForRendition(ADMIN, sourceNodeRef, PDF, true);
assertNotNull("pdf rendition was not generated", pdfRenditionNodeRef);
assertNotNull("pdf rendition was not generated",
nodeService.getProperty(pdfRenditionNodeRef, PROP_CONTENT));
// Check the pdf rendition content hash code is valid
int pdfRenditionContentHashCode = getRenditionContentHashCode(pdfRenditionNodeRef);
assertEquals("pdf rendition content hash code is different from source node content hash code",
sourceNodeContentHashCode, pdfRenditionContentHashCode);
// Calling 'clearRenditionContentData' method directly so that rendition content will be cleaned.
AuthenticationUtil.runAs(
(AuthenticationUtil.RunAsWork<Void>) () -> transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> {
renditionService2.clearRenditionContentData(sourceNodeRef, PDF);
return null;
}),
ADMIN);
assertNull("Rendition has content", nodeService.getProperty(pdfRenditionNodeRef, PROP_CONTENT));
pdfRenditionContentHashCode = getRenditionContentHashCode(pdfRenditionNodeRef);
assertFalse("Rendition has content hash code",
isValidRenditionContentHashCode(pdfRenditionContentHashCode));
renditionService2.setEnabled(false);
final QName pdfRendDefQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "pdf");
transactionService.getRetryingTransactionHelper()
.doInTransaction(() -> AuthenticationUtil.runAs(
() -> renditionService.render(sourceNodeRef, pdfRendDefQName), ADMIN));
renditionService2.setEnabled(true);
NodeRef pdfRecreatedRenditionNodeRef = waitForRendition(ADMIN, sourceNodeRef, PDF, true);
assertNotEquals(" Rendition before deletion and after previewing are identical",
pdfRenditionNodeRef.getId(), pdfRecreatedRenditionNodeRef.getId());
}
finally
{
renditionService2.setEnabled(true);
}
}
}

View File

@@ -38,6 +38,7 @@ import java.util.Set;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import net.minidev.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -57,6 +58,9 @@ public class ClientRegistrationProviderUnitTest
private static final String OPENID_CONFIGURATION = "{\"token_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.serviceonline.alfresco/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.serviceonline.alfresco/alfresco/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.service.alfresco/oidc/userinfo\",\"authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.serviceonline.alfresco/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"serviceonline.alfresco\",\"cloud_graph_host_name\":\"graph.oidc.net\",\"msgraph_host\":\"graph.service.alfresco\",\"rbac_url\":\"https://pas.oidc.alfresco\"}";
private static final String DISCOVERY_PATH_SEGMENTS = "/.well-known/openid-configuration";
private static final String AUTH_SERVER = "https://login.serviceonline.alfresco";
private static final String ADMIN_CONSOLE_SCOPES = "openid,email,profile,offline_access";
private static final String PSSWD_GRANT_SCOPES = "openid,email,profile";
private static final String ISSUER_ATRR = "issuer";
private IdentityServiceConfig config;
private RestTemplate restTemplate;
@@ -70,6 +74,9 @@ public class ClientRegistrationProviderUnitTest
config = new IdentityServiceConfig();
config.setAuthServerUrl(AUTH_SERVER);
config.setResource(CLIENT_ID);
config.setAdminConsoleScopes(ADMIN_CONSOLE_SCOPES);
config.setPasswordGrantScopes(PSSWD_GRANT_SCOPES);
config.setIssuerAttribute(ISSUER_ATRR);
restTemplate = mock(RestTemplate.class);
ResponseEntity responseEntity = mock(ResponseEntity.class);
@@ -263,4 +270,42 @@ public class ClientRegistrationProviderUnitTest
"https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
}
}
@Test
public void shouldUseDefaultIssuerAttribute()
{
config.setIssuerUrl(null);
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0");
}
}
@Test
public void shouldUseCustomIssuerAttribute()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
config.setIssuerAttribute("access_token_issuer");
when(oidcResponse.getCustomParameters()).thenReturn(createJSONObject("access_token_issuer", "https://login.serviceonline.alfresco/alfresco/v2.0/at_trust"));
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0/at_trust");
}
}
private static JSONObject createJSONObject(String fieldName, String fieldValue)
{
JSONObject jsonObject = new JSONObject();
jsonObject.appendField(fieldName, fieldValue);
return jsonObject;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -43,6 +43,7 @@ import org.alfresco.repo.security.authentication.AuthenticationException;
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.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;

View File

@@ -25,10 +25,7 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import java.lang.reflect.Field;
import java.util.Optional;
@@ -37,11 +34,14 @@ import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
@@ -126,11 +126,15 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(principalAttribute, "given_name", "family_name", "email");
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
ClientRegistration clientRegistration = mock(ClientRegistration.class, RETURNS_DEEP_STUBS);
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(principalAttribute);
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
when(idsServiceFacadeMock.getUserInfo(accessToken, principalAttribute)).thenReturn(identityServiceFacade.getUserInfo(accessToken, principalAttribute));
when(idsServiceFacadeMock.getUserInfo(accessToken, userInfoAttrMapping)).thenReturn(identityServiceFacade.getUserInfo(accessToken, userInfoAttrMapping));
when(idsServiceFacadeMock.getClientRegistration()).thenReturn(clientRegistration);
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
Field declaredField = jitProvisioningHandler.getClass()
@@ -151,7 +155,7 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertEquals("johndoe123@alfresco.com", userInfoOptional.get().email());
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
verify(idsServiceFacadeMock).decodeToken(accessToken);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, principalAttribute);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, userInfoAttrMapping);
if (!isAuth0Enabled)
{
assertEquals("John", userInfoOptional.get().firstName());

View File

@@ -40,8 +40,13 @@ import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.Mock;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
@@ -51,6 +56,9 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
@Mock
private IdentityServiceFacade identityServiceFacade;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ClientRegistration clientRegistration;
@Mock
private PersonService personService;
@@ -64,11 +72,22 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
private IdentityServiceConfig identityServiceConfig;
@Mock
private OIDCUserInfo userInfo;
private DecodedTokenUser decodedTokenUser;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private UserInfoAttrMapping expectedMapping;
private static final String JWT_TOKEN = "myToken";
private static final String USERNAME = "johny123";
private static final String FIRST_NAME = "John";
private static final String LAST_NAME = "Doe";
private static final String EMAIL = "johny123@email.com";
public static final String USERNAME_CLAIM = "nickname";
public static final String EMAIL_CLAIM = "email";
public static final String FIRST_NAME_CLAIM = "given_name";
public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
@@ -78,149 +97,147 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
when(transactionService.isReadOnly()).thenReturn(false);
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
when(personService.createMissingPeople()).thenReturn(true);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
personService, transactionService, identityServiceConfig);
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(USERNAME_CLAIM);
when(identityServiceConfig.getEmailAttribute()).thenReturn(EMAIL_CLAIM);
when(identityServiceConfig.getFirstNameAttribute()).thenReturn(FIRST_NAME_CLAIM);
when(identityServiceConfig.getLastNameAttribute()).thenReturn(LAST_NAME_CLAIM);
expectedMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
}
@Test
public void shouldExtractUserInfoForExistingUser()
{
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoForExistingUserWithProviderPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).thenReturn("johny123");
when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(USERNAME);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, "nickname");
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoFromAccessTokenAndCreateUser()
{
when(personService.personExists("johny123")).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn("John");
when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn("Doe");
when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn("johny123@email.com");
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.personExists(USERNAME)).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn(FIRST_NAME);
when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn(LAST_NAME);
when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn(EMAIL);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("John", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("johny123@email.com", result.get().email());
assertEquals(USERNAME, result.get().username());
assertEquals(FIRST_NAME, result.get().firstName());
assertEquals(LAST_NAME, result.get().lastName());
assertEquals(EMAIL, result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoFromUserInfoEndpointAndCreateUser()
{
when(userInfo.username()).thenReturn("johny123");
when(userInfo.firstName()).thenReturn("John");
when(userInfo.lastName()).thenReturn("Doe");
when(userInfo.email()).thenReturn("johny123@email.com");
when(personService.personExists("johny123")).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
when(decodedTokenUser.username()).thenReturn(USERNAME);
when(decodedTokenUser.firstName()).thenReturn(FIRST_NAME);
when(decodedTokenUser.lastName()).thenReturn(LAST_NAME);
when(decodedTokenUser.email()).thenReturn(EMAIL);
when(personService.personExists(USERNAME)).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("John", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("johny123@email.com", result.get().email());
assertEquals(USERNAME, result.get().username());
assertEquals(FIRST_NAME, result.get().firstName());
assertEquals(LAST_NAME, result.get().lastName());
assertEquals(EMAIL, result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
{
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertFalse(result.isPresent());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldCallUserInfoEndpointToGetUsername()
{
when(personService.personExists("johny123")).thenReturn(true);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertEquals("", result.get().firstName());
assertEquals("", result.get().lastName());
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldCallUserInfoEndpointToGetUsernameWithProvidedPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, "nickname")).thenReturn(Optional.of(userInfo));
when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertEquals("", result.get().firstName());
assertEquals("", result.get().lastName());
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, "nickname");
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
@@ -232,8 +249,8 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
verify(personService, never()).createPerson(any());
verify(identityServiceFacade, never()).decodeToken(null);
verify(identityServiceFacade, never()).decodeToken("");
verify(identityServiceFacade, never()).getUserInfo(null, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo("", PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
}
}

View File

@@ -38,6 +38,7 @@ import jakarta.servlet.http.HttpServletRequest;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import junit.framework.TestCase;
import org.mockito.Mockito;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.alfresco.repo.security.authentication.AuthenticationException;
@@ -96,12 +97,14 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
{
final TransactionService transactionService = mock(TransactionService.class);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class, Mockito.RETURNS_DEEP_STUBS);
final PersonService personService = mock(PersonService.class);
final IdentityServiceConfig identityServiceConfig = mock(IdentityServiceConfig.class);
when(transactionService.isReadOnly()).thenReturn(true);
when(facade.decodeToken(anyString()))
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
when(facade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
.thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));

View File

@@ -40,12 +40,14 @@ import org.springframework.web.client.RestOperations;
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.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
public class SpringBasedIdentityServiceFacadeUnitTest
{
private static final String USER_NAME = "user";
private static final String PASSWORD = "password";
private static final String TOKEN = "tEsT-tOkEn";
private static final UserInfoAttrMapping USER_INFO_ATTR_MAPPING = new UserInfoAttrMapping("preferred_username", "given_name", "family_name", "email");
@Test
public void shouldThrowVerificationExceptionOnFailure()
@@ -82,7 +84,7 @@ public class SpringBasedIdentityServiceFacadeUnitTest
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThat(facade.getUserInfo(TOKEN, "preferred_username").isEmpty()).isTrue();
assertThat(facade.getUserInfo(TOKEN, USER_INFO_ATTR_MAPPING).isEmpty()).isTrue();
}
private ClientRegistration testRegistration()

View File

@@ -38,6 +38,7 @@ import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -155,6 +156,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
@@ -178,6 +180,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAudience()).thenReturn(audience);
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(redirectPath);
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
.formatted("http://localhost:8080", redirectPath);

View File

@@ -0,0 +1,109 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
public class AccessTokenToDecodedTokenUserMapperUnitTest
{
@Mock
private IdentityServiceFacade.DecodedAccessToken decodedAccessToken;
private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
public static final String USERNAME_CLAIM = "nickname";
public static final String EMAIL_CLAIM = "email";
public static final String FIRST_NAME_CLAIM = "given_name";
public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
{
initMocks(this);
UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
}
@Test
public void shouldMapToDecodedTokenUserWithAllFieldsPopulated()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("John", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("johny123@email.com", result.get().email());
}
@Test
public void shouldMapToDecodedTokenUserWithSomeFieldsEmpty()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("", result.get().email());
}
@Test
public void shouldReturnEmptyOptionalForNullUsername()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(null);
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertFalse(result.isPresent());
}
}

View File

@@ -0,0 +1,95 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.alfresco.service.cmr.security.PersonService;
public class TokenUserToOIDCUserMapperUnitTest
{
@Mock
private PersonService personService;
@InjectMocks
private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
@Before
public void setup()
{
initMocks(this);
}
@Test
public void shouldMapToOIDCUserWithAllFieldsPopulated()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser("JOHNY123", "John", "Doe", "johny123@email.com");
when(personService.getUserIdentifier("JOHNY123")).thenReturn("johny123");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertEquals("johny123", oidcUserInfo.username());
assertEquals("John", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("johny123@email.com", oidcUserInfo.email());
}
@Test
public void shouldMapToOIDCUserWithSomeFieldsEmpty()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser("johny123", "", "Doe", "");
when(personService.getUserIdentifier("johny123")).thenReturn("johny123");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertEquals("johny123", oidcUserInfo.username());
assertEquals("", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("", oidcUserInfo.email());
}
@Test
public void shouldReturnNullForNullUsername()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser(null, "John", "Doe", "johny123@email.com");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertNull(oidcUserInfo.username());
assertEquals("John", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("johny123@email.com", oidcUserInfo.email());
}
}

View File

@@ -28,6 +28,9 @@ identity-service.register-node-at-startup=true
identity-service.register-node-period=50
identity-service.token-store=SESSION
identity-service.principal-attribute=preferred_username
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.turn-off-change-session-id-on-login=true
identity-service.token-minimum-time-to-live=10
identity-service.min-time-between-jwks-requests=60

View File

@@ -31,7 +31,7 @@
</onException>
<transacted />
<loadBalance>
<roundRobin/>
<roundRobinLoadBalancer/>
<to uri="bean:mockExceptionThrowingConsumer"/>
<to uri="bean:mockConsumer"/>
</loadBalance>