Compare commits

..

30 Commits

Author SHA1 Message Date
alfresco-build
2c41c0ead1 [maven-release-plugin][skip ci] prepare release 23.2.0.24 2023-12-21 11:05:59 +00:00
dependabot[bot]
c957f40a78 Bump org.springframework:spring-webmvc from 6.0.12 to 6.0.14 (#2346)
Bumps [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) from 6.0.12 to 6.0.14.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.0.12...v6.0.14)

---
updated-dependencies:
- dependency-name: org.springframework:spring-webmvc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 11:30:11 +01:00
alfresco-build
f8a8b5c8f3 [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-19 13:20:05 +00:00
alfresco-build
773d9ca9d9 [maven-release-plugin][skip ci] prepare release 23.2.0.23 2023-12-19 13:20:03 +00:00
Maciej Pichura
1af0b72257 ACS-6402: Add tests covering module compatibility check for new ACS versioning schema (#2379)
* ACS-6402: Add tests covering module compatibility check for new ACS versioning schema.

* ACS-6402: Ignoring false PMD issues.
2023-12-19 13:45:07 +01:00
alfresco-build
158fb067d0 [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-17 00:06:29 +00:00
alfresco-build
016c212c65 [maven-release-plugin][skip ci] prepare release 23.2.0.22 2023-12-17 00:06:26 +00:00
Alfresco CI User
c1bd8e71c4 [force] Force release for 2023-12-17. 2023-12-17 00:03:20 +00:00
alfresco-build
f68446b3e5 [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-14 11:06:04 +00:00
alfresco-build
9b26c66dec [maven-release-plugin][skip ci] prepare release 23.2.0.21 2023-12-14 11:06:01 +00:00
Damian Ujma
06c35ea379 MNT-23880 Integrate the Alfresco Admin Console with the IDS (#2362)
* MNT-23880 Integrate IDS with the Admin Console

* MNT-23880 Remove diamond expressions

* MNT-23880 Refactor

* MNT-23880 Move requesting authentication

* MNT-23880 Fix comment

* MNT-23880 Check if AdminConsoleAuthenticator is active

* MNT-23880 Fix DefaultAdminConsoleAuthenticator + PMD issues

* MNT-23880 Fix PMD issues

* MNT-23880 Refactor

* MNT-23880 Refactor RemoteUserAuthenticatorFactory
2023-12-14 11:30:53 +01:00
alfresco-build
5580609010 [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-13 12:23:44 +00:00
alfresco-build
e16a3759ad [maven-release-plugin][skip ci] prepare release 23.2.0.20 2023-12-13 12:23:41 +00:00
Domenico Sibilio
9cce8d54d8 ACS-5925 Switch from IDS 2.0.0 to Keycloak 21.1.2 (#2369) 2023-12-13 12:48:47 +01:00
alfresco-build
856bf011c5 [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-13 10:34:12 +00:00
alfresco-build
d46fc62cdb [maven-release-plugin][skip ci] prepare release 23.2.0.19 2023-12-13 10:34:09 +00:00
mikolajbrzezinski
eff41eef12 ACS-6304 Implement SAST Pipeline Scan (#2304)
ACS-6304 Implement SAST Pipeline Scan
2023-12-13 10:57:43 +01:00
alfresco-build
3d0185574d [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-10 00:06:22 +00:00
alfresco-build
5664b5de78 [maven-release-plugin][skip ci] prepare release 23.2.0.18 2023-12-10 00:06:19 +00:00
Alfresco CI User
82f44122bc [force] Force release for 2023-12-10. 2023-12-10 00:03:41 +00:00
alfresco-build
6c3740c2a6 [maven-release-plugin][skip ci] prepare for next development iteration 2023-12-03 00:06:05 +00:00
alfresco-build
ecdbf41291 [maven-release-plugin][skip ci] prepare release 23.2.0.17 2023-12-03 00:06:03 +00:00
Alfresco CI User
6abf01e083 [force] Force release for 2023-12-03. 2023-12-03 00:03:30 +00:00
alfresco-build
7671d3b7bc [maven-release-plugin][skip ci] prepare for next development iteration 2023-11-30 12:27:33 +00:00
alfresco-build
a8a6b565a7 [maven-release-plugin][skip ci] prepare release 23.2.0.16 2023-11-30 12:27:30 +00:00
Damian Ujma
a660109b73 ACS-6303 Add seamless ACS JIT user provisioning (#2336)
* ACS-6303 Implement JIT User Provisioning

* ACS-6303 Fix AuthenticationsTest

* ACS-6303 Add IT test

* ACS-6303 Fix syntax

* ACS-6303 Revert local change

* ACS-6303 Update IDS version

* ACS-6303 Fix JITProvisioning IT test execution

* ACS-6303 Add new IT scenario

* ACS-6303 Remove AppContext05TestSuite-setup.sh + optimize calling UserInfoEndpoint

* ACS-6303 Fix PMD issues

* ACS-6303 Fix property name

* ACS-6303 Change getUserInfo return type

* Apply suggestions from code review

Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>

* ACS-6303 Move var declaration + use lambda+diamond operator

* ACS-6303 Add a small optimisation

---------

Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
2023-11-30 12:50:24 +01:00
alfresco-build
11cc7fd2cb [maven-release-plugin][skip ci] prepare for next development iteration 2023-11-26 00:06:04 +00:00
alfresco-build
fe356c6135 [maven-release-plugin][skip ci] prepare release 23.2.0.15 2023-11-26 00:06:01 +00:00
Alfresco CI User
8b75f4f961 [force] Force release for 2023-11-26. 2023-11-26 00:03:28 +00:00
alfresco-build
db9d6cc08c [maven-release-plugin][skip ci] prepare for next development iteration 2023-11-20 15:54:34 +00:00
59 changed files with 2258 additions and 195 deletions

View File

@@ -46,12 +46,12 @@ jobs:
- name: "Clean Maven cache"
run: bash ./scripts/ci/cleanup_cache.sh
veracode:
veracode_sca:
name: "Source Clear Scan (SCA)"
runs-on: ubuntu-latest
needs: [prepare]
if: >
((github.ref_name == 'master' || startsWith(github.ref_name, 'release/')) && github.event_name != 'pull_request') &&
(github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request') &&
!contains(github.event.head_commit.message, '[skip tests]') &&
!contains(github.event.head_commit.message, '[force')
steps:
@@ -68,6 +68,56 @@ jobs:
- name: "Clean Maven cache"
run: bash ./scripts/ci/cleanup_cache.sh
veracode_sast:
name: "Pipeline SAST Scan"
runs-on: ubuntu-latest
needs: [prepare]
if: >
(github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request') &&
!contains(github.event.head_commit.message, '[skip tests]') &&
!contains(github.event.head_commit.message, '[force')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.35.2
- uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v1.35.2
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.35.2
- uses: Alfresco/alfresco-build-tools/.github/actions/github-download-file@v5.6.0
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
repository: "Alfresco/veracode-baseline-archive"
file-path: "alfresco-community-repo/alfresco-community-repo-baseline.json"
target: "baseline.json"
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
bash ./scripts/ci/init.sh
bash ./scripts/ci/build.sh
- name: "Run SAST Scan"
uses: veracode/Veracode-pipeline-scan-action@v1.0.10
with:
vid: ${{ secrets.VERACODE_API_ID }}
vkey: ${{ secrets.VERACODE_API_KEY }}
file: "packaging/war/target/alfresco.war"
fail_build: true
project_name: alfresco-community-repo
issue_details: true
veracode_policy_name: Alfresco Default
summary_output: true
summary_output_file: results.json
summary_display: true
baseline_file: baseline.json
- name: Upload scan result
if: success() || failure()
run: zip readable_output.zip results.json
- name: Upload Artifact
if: success() || failure()
uses: actions/upload-artifact@v3
with:
name: Veracode Pipeline-Scan Results (Human Readable)
path: readable_output.zip
- name: "Clean Maven cache"
run: bash ./scripts/ci/cleanup_cache.sh
pmd_scan:
name: "PMD Scan"
runs-on: ubuntu-latest
@@ -356,8 +406,8 @@ jobs:
- testSuite: AppContext04TestSuite
compose-profile: with-transform-core-aio
- testSuite: AppContext05TestSuite
compose-profile: default
mvn-options: '"-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth"'
compose-profile: with-sso
mvn-options: '-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth -Dauthentication.chain=identity-service1:identity-service,alfrescoNtlm1:alfrescoNtlm'
- testSuite: AppContext06TestSuite
compose-profile: with-transform-core-aio
- testSuite: AppContextExtraTestSuite
@@ -381,6 +431,8 @@ jobs:
run: bash ./scripts/ci/init.sh
- name: "Set transformers tag"
run: echo "TRANSFORMERS_TAG=$(mvn help:evaluate -Dexpression=dependency.alfresco-transform-core.version -q -DforceStdout)" >> $GITHUB_ENV
- name: "Set the host IP"
run: echo "HOST_IP=$(hostname -I | cut -f1 -d' ')" >> $GITHUB_ENV
- name: "Generate Keystores and Truststores for Mutual TLS configuration"
if: ${{ matrix.mtls }}
run: |
@@ -393,11 +445,7 @@ jobs:
echo "HOSTNAME_VERIFICATION_DISABLED=false" >> "$GITHUB_ENV"
fi
- name: "Set up the environment"
run: |
if [ -e ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh ]; then
bash ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh
fi
docker-compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d
run: docker-compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d
- name: "Run tests"
run: mvn -B test -pl repository -am -Dtest=${{ matrix.testSuite }} -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco ${{ matrix.mvn-options }}
- name: "Clean Maven cache"

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>23.2.0.14</version>
<version>23.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>23.2.0.14</version>
<version>23.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>23.2.0.14</version>
<version>23.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>23.2.0.14</version>
<version>23.2.0.24</version>
</parent>
<modules>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ import junit.framework.TestCase;
*
* @author Roy Wetherall
*/
@SuppressWarnings({"PMD.DetachedTestCase", "PMD.JUnit4TestShouldUseTestAnnotation"})
public class VersionNumberTest extends TestCase
{
public void testCreate()
@@ -136,4 +137,36 @@ public class VersionNumberTest extends TestCase
assertEquals(-1, version8.compareTo(version9));
assertEquals(-1, version9.compareTo(version10));
}
public void testCompareNewSchema() {
// module min/max repo version is 23, actual ACS version is 23.1.0 which is greater than module
VersionNumber repoVersionMin = new VersionNumber("23");
VersionNumber repoVerisionActual = new VersionNumber("23.1.0");
assertEquals(1, repoVerisionActual.compareTo(repoVersionMin));
// module min/max repo version is 23.2, actual ACS version is 23.1.1 which is lower than module
repoVersionMin = new VersionNumber("23.2");
repoVerisionActual = new VersionNumber("23.1.1");
assertEquals(-1, repoVerisionActual.compareTo(repoVersionMin));
// module min/max repo version is 7.4, actual ACS version is 23.1.0 which is greater than module
repoVersionMin = new VersionNumber("7.4");
repoVerisionActual = new VersionNumber("23.1.0");
assertEquals(1, repoVerisionActual.compareTo(repoVersionMin));
// module min/max repo version is 24, actual ACS version is 24.1.0 which is greater than module
repoVersionMin = new VersionNumber("24");
repoVerisionActual = new VersionNumber("24.1.0");
assertEquals(1, repoVerisionActual.compareTo(repoVersionMin));
// module min/max repo version is 24, actual ACS version is 23.2.0 which is lower than module
repoVersionMin = new VersionNumber("24");
repoVerisionActual = new VersionNumber("23.2.0");
assertEquals(-1, repoVerisionActual.compareTo(repoVersionMin));
// module min/max repo version is 24.2, actual ACS version is 24.2.0 which is equal to module
repoVersionMin = new VersionNumber("24.2");
repoVerisionActual = new VersionNumber("24.2.0");
assertEquals(0, repoVerisionActual.compareTo(repoVersionMin));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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>23.2.0.14</version>
<version>23.2.0.24</version>
<packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name>
@@ -57,7 +57,7 @@
<dependency.acs-event-model.version>0.0.24</dependency.acs-event-model.version>
<dependency.aspectj.version>1.9.20.1</dependency.aspectj.version>
<dependency.spring.version>6.0.12</dependency.spring.version>
<dependency.spring.version>6.0.14</dependency.spring.version>
<dependency.spring-security.version>6.1.4</dependency.spring-security.version>
<dependency.antlr.version>3.5.3</dependency.antlr.version>
<dependency.jackson.version>2.15.2</dependency.jackson.version>
@@ -152,7 +152,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>23.2.0.14</tag>
<tag>23.2.0.24</tag>
</scm>
<distributionManagement>

View File

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

View File

@@ -34,6 +34,7 @@ import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.web.auth.AuthenticationListener;
import org.alfresco.repo.web.auth.TicketCredentials;
@@ -67,16 +68,18 @@ import java.util.Set;
*/
public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactory
{
private static Log logger = LogFactory.getLog(RemoteUserAuthenticatorFactory.class);
private static final Log LOGGER = LogFactory.getLog(RemoteUserAuthenticatorFactory.class);
public static final long GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT = 10000L; // 10 sec
protected RemoteUserMapper remoteUserMapper;
protected AuthenticationComponent authenticationComponent;
protected AdminConsoleAuthenticator adminConsoleAuthenticator;
private boolean alwaysAllowBasicAuthForAdminConsole = true;
List<String> adminConsoleScriptFamilies;
long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT;
public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper)
{
this.remoteUserMapper = remoteUserMapper;
@@ -117,6 +120,12 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
this.getRemoteUserTimeoutMilliseconds = getRemoteUserTimeoutMilliseconds;
}
public void setAdminConsoleAuthenticator(
AdminConsoleAuthenticator adminConsoleAuthenticator)
{
this.adminConsoleAuthenticator = adminConsoleAuthenticator;
}
@Override
public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
{
@@ -140,36 +149,46 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
{
boolean authenticated = false;
if (logger.isTraceEnabled())
if (LOGGER.isTraceEnabled())
{
logger.trace("Authenticate level required: " + required + " is guest: " + isGuest);
LOGGER.trace("Authenticate level required: " + required + " is guest: " + isGuest);
}
String userId = null;
if (isRemoteUserMapperActive())
{
if (isAlwaysAllowBasicAuthForAdminConsole())
{
final boolean useTimeoutForAdminAccessingAdminConsole = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest);
if (useTimeoutForAdminAccessingAdminConsole && isBasicAuthHeaderPresentForAdmin())
{
return callBasicAuthForAdminConsoleAccess(required, isGuest);
}
try
{
userId = getRemoteUserWithTimeout(useTimeoutForAdminAccessingAdminConsole);
}
catch (AuthenticationTimeoutException e)
{
//return basic auth challenge
return false;
}
}
else
if (servletReq.getServiceMatch() != null &&
isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
{
// retrieve the remote user if configured and available - authenticate that user directly
userId = getRemoteUser();
userId = getAdminConsoleUser();
}
if (userId == null)
{
if (isAlwaysAllowBasicAuthForAdminConsole())
{
final boolean useTimeoutForAdminAccessingAdminConsole = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest);
if (useTimeoutForAdminAccessingAdminConsole && isBasicAuthHeaderPresentForAdmin())
{
return callBasicAuthForAdminConsoleAccess(required, isGuest);
}
try
{
userId = getRemoteUserWithTimeout(useTimeoutForAdminAccessingAdminConsole);
}
catch (AuthenticationTimeoutException e)
{
//return basic auth challenge
return false;
}
}
else
{
// retrieve the remote user if configured and available - authenticate that user directly
userId = getRemoteUser();
}
}
}
@@ -208,9 +227,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
{
// Validate the ticket for the current SessionUser
authenticationService.validate(user.getTicket());
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("Ticket is valid. Retaining cached user in session.");
LOGGER.debug("Ticket is valid. Retaining cached user in session.");
}
listener.userAuthenticated(new TicketCredentials(user.getTicket()));
authenticated = true;
@@ -222,9 +241,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
}
catch (AuthenticationException authErr)
{
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("An Authentication error occur. Removing User session.", authErr);
LOGGER.debug("An Authentication error occur. Removing User session.", authErr);
}
session.removeAttribute(AuthenticationDriver.AUTHENTICATION_USER);
session.invalidate();
@@ -236,15 +255,20 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
authenticated = super.authenticate(required, isGuest);
}
}
if(!authenticated && servletReq.getServiceMatch() != null &&
isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
{
adminConsoleAuthenticator.requestAuthentication(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
}
return authenticated;
}
private boolean callBasicAuthForAdminConsoleAccess(RequiredAuthentication required, boolean isGuest)
{
// return REST call, after a timeout/basic auth challenge
if (logger.isTraceEnabled())
if (LOGGER.isTraceEnabled())
{
logger.trace("An Admin Console request has come in with Basic Auth headers present for an admin user.");
LOGGER.trace("An Admin Console request has come in with Basic Auth headers present for an admin user.");
}
// In order to prompt for another password, in case it was not entered correctly,
// the output of this method should be returned by the calling "authenticate" method;
@@ -258,9 +282,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
boolean useTimeoutForAdminAccessingAdminConsole = RequiredAuthentication.admin.equals(required) && !isGuest &&
servletReq.getServiceMatch() != null && isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript());
if (logger.isTraceEnabled())
if (LOGGER.isTraceEnabled())
{
logger.trace("Should ensure that the admins can login with basic auth: " + useTimeoutForAdminAccessingAdminConsole);
LOGGER.trace("Should ensure that the admins can login with basic auth: " + useTimeoutForAdminAccessingAdminConsole);
}
return useTimeoutForAdminAccessingAdminConsole;
}
@@ -270,6 +294,11 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
return remoteUserMapper != null && (!(remoteUserMapper instanceof ActivateableBean) || ((ActivateableBean) remoteUserMapper).isActive());
}
private boolean isAdminConsoleAuthenticatorActive()
{
return adminConsoleAuthenticator != null && (!(adminConsoleAuthenticator instanceof ActivateableBean) || ((ActivateableBean) adminConsoleAuthenticator).isActive());
}
protected boolean isAdminConsoleWebScript(WebScript webScript)
{
if (webScript == null || adminConsoleScriptFamilies == null || webScript.getDescription() == null
@@ -278,9 +307,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
return false;
}
if (logger.isTraceEnabled())
if (LOGGER.isTraceEnabled())
{
logger.trace("WebScript: " + webScript + " has these families: " + webScript.getDescription().getFamilys());
LOGGER.trace("WebScript: " + webScript + " has these families: " + webScript.getDescription().getFamilys());
}
// intersect the "family" sets defined
@@ -288,9 +317,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
families.retainAll(adminConsoleScriptFamilies);
final boolean isAdminConsole = !families.isEmpty();
if (logger.isTraceEnabled() && isAdminConsole)
if (LOGGER.isTraceEnabled() && isAdminConsole)
{
logger.trace("Detected an Admin Console webscript: " + webScript );
LOGGER.trace("Detected an Admin Console webscript: " + webScript);
}
return isAdminConsole;
@@ -316,7 +345,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
}
catch (Exception e)
{
logger.warn("Exception trying to get the remote user: " + e.getMessage(), e);
LOGGER.warn("Exception trying to get the remote user: " + e.getMessage(), e);
}
returnedRemoteUser = getRemoteUserRunnable.getReturnedRemoteUser();
@@ -330,9 +359,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
final String message = "Could not get the remote user in a reasonable time: " + getRemoteUserTimeoutMilliseconds + " milliseconds. "
+ "Adjust the timeout property 'authentication.getRemoteUserTimeoutMilliseconds' if required.";
if (logger.isWarnEnabled())
if (LOGGER.isWarnEnabled())
{
logger.warn("Returning basic auth challenge for Admin Console. Cause: " + message);
LOGGER.warn("Returning basic auth challenge for Admin Console. Cause: " + message);
}
HttpServletResponse res = servletRes.getHttpServletResponse();
res.setStatus(401);
@@ -379,15 +408,29 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
private void logRemoteUserID(String userId)
{
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
String message = (userId == null) ?
"No external user ID in request." :
"Extracted external user ID from request: " + AuthenticationUtil.maskUsername(userId);
logger.debug(message);
LOGGER.debug(message);
}
}
protected String getAdminConsoleUser()
{
String userId = null;
if (isRemoteUserMapperActive())
{
userId = adminConsoleAuthenticator.getAdminConsoleUser(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
}
logRemoteUserID(userId);
return userId;
}
class GetRemoteUserRunnable implements Runnable
{
private volatile String returnedRemoteUser;

View File

@@ -213,6 +213,7 @@
<property name="authenticationComponent" ref="authenticationComponent" />
<property name="authenticationListener" ref="webScriptAuthenticationListener"/>
<property name="remoteUserMapper" ref="RemoteUserMapper" />
<property name="adminConsoleAuthenticator" ref="AdminConsoleAuthenticator" />
<property name="alwaysAllowBasicAuthForAdminConsole">
<value>${authentication.alwaysAllowBasicAuthForAdminConsole.enabled}</value>
</property>

View File

@@ -523,13 +523,13 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest
private RemoteUserMapper createRemoteUserMapperToUseForTheTest(boolean useIdentityService)
{
PersonService personServiceLocal = (PersonService) applicationContext.getBean("PersonService");
RemoteUserMapper remoteUserMapper;
if (useIdentityService)
{
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
interceptingRemoteUserMapper.setActive(true);
interceptingRemoteUserMapper.setPersonService(personServiceLocal);
interceptingRemoteUserMapper.setIdentityServiceFacade(null);
interceptingRemoteUserMapper.setJitProvisioningHandler(null);
interceptingRemoteUserMapper.setUserIdToReturn(user2);
remoteUserMapper = interceptingRemoteUserMapper;
}

View File

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

View File

@@ -0,0 +1,56 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.external;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* An interface for objects capable of extracting an externally authenticated user ID from the HTTP Admin Console webscript request.
*/
public interface AdminConsoleAuthenticator
{
/**
* Gets an externally authenticated user ID from the HTTP Admin Console webscript request.
*
* @param request
* the request
* @param response
* the response
* @return the user ID or <code>null</code> if the user is unauthenticated
*/
String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response);
/**
* Requests an authentication.
*
* @param request
* the request
* @param response
* the response
*/
void requestAuthentication(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -0,0 +1,54 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.external;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.management.subsystems.ActivateableBean;
/**
* A default {@link AdminConsoleAuthenticator} implementation. Returns null to request a basic auth challenge.
*/
public class DefaultAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
{
@Override
public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response)
{
return null;
}
@Override
public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
{
// No implementation
}
@Override
public boolean isActive()
{
return false;
}
}

View File

@@ -50,6 +50,9 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
private IdentityServiceFacade identityServiceFacade;
/** enabled flag for the identity service subsystem**/
private boolean active;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private boolean allowGuestLogin;
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
@@ -62,6 +65,12 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
this.allowGuestLogin = allowGuestLogin;
}
public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
{
this.jitProvisioningHandler = jitProvisioningHandler;
}
@Override
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
{
if (identityServiceFacade == null)
@@ -77,10 +86,13 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
try
{
// Attempt to verify user credentials
identityServiceFacade.authorize(AuthorizationGrant.password(userName, new String(password)));
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.password(userName, String.valueOf(password)));
String normalizedUsername = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(accessTokenAuthorization.getAccessToken().getTokenValue())
.map(OIDCUserInfo::username)
.orElseThrow(() -> new AuthenticationException("Failed to extract username from token and user info endpoint."));
// Verification was successful so treat as authenticated user
setCurrentUser(userName);
setCurrentUser(normalizedUsername);
}
catch (IdentityServiceFacadeException e)
{

View File

@@ -30,6 +30,10 @@ import static java.util.Objects.requireNonNull;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
/**
* Allows to interact with the Identity Service
@@ -52,6 +56,19 @@ public interface IdentityServiceFacade
*/
DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
/**
* Gets claims about the authenticated user,
* such as name and email address, via the UserInfo endpoint of the OpenID provider.
* @param token {@link String} with encoded access token value.
* @return {@link OIDCUserInfo} containing user claims.
*/
Optional<OIDCUserInfo> getUserInfo(String token);
/**
* Gets a client registration
*/
ClientRegistration getClientRegistration();
class IdentityServiceFacadeException extends RuntimeException
{
public IdentityServiceFacadeException(String message)
@@ -78,6 +95,20 @@ public interface IdentityServiceFacade
}
}
class UserInfoException extends IdentityServiceFacadeException
{
UserInfoException(String message)
{
super(message);
}
UserInfoException(String message, Throwable cause)
{
super(message, cause);
}
}
class TokenDecodingException extends IdentityServiceFacadeException
{
TokenDecodingException(String message)
@@ -193,8 +224,14 @@ public interface IdentityServiceFacade
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
AuthorizationGrant that = (AuthorizationGrant) o;
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&

View File

@@ -188,6 +188,18 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
return getTargetFacade().decodeToken(token);
}
@Override
public Optional<OIDCUserInfo> getUserInfo(String token)
{
return getTargetFacade().getUserInfo(token);
}
@Override
public ClientRegistration getClientRegistration()
{
return getTargetFacade().getClientRegistration();
}
private IdentityServiceFacade getTargetFacade()
{
return ofNullable(targetFacade.get())
@@ -224,9 +236,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
{
this.httpClientProvider = Objects.requireNonNull(httpClientProvider);
this.clientRegistrationProvider = Objects.requireNonNull(clientRegistrationProvider);
this.jwtDecoderProvider = Objects.requireNonNull(jwtDecoderProvider);
this.httpClientProvider = requireNonNull(httpClientProvider);
this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider);
this.jwtDecoderProvider = requireNonNull(jwtDecoderProvider);
}
private IdentityServiceFacade createIdentityServiceFacade()
@@ -259,7 +271,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private HttpClientProvider(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
this.config = requireNonNull(config);
}
private HttpClient createHttpClient()
@@ -341,7 +353,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private char[] asCharArray(String value, char[] nullValue)
{
return Optional.ofNullable(value)
return ofNullable(value)
.filter(not(String::isBlank))
.map(String::toCharArray)
.orElse(nullValue);
@@ -354,7 +366,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private ClientRegistrationProvider(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
this.config = requireNonNull(config);
}
public ClientRegistration createClientRegistration(final RestOperations rest)
@@ -389,6 +401,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.issuerUri(issuerUri)
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
.scope("openid", "profile", "email")
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
}
@@ -448,7 +462,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
JwtDecoderProvider(IdentityServiceConfig config)
{
this.config = Objects.requireNonNull(config);
this.config = requireNonNull(config);
}
public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)

View File

@@ -0,0 +1,171 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.lang3.StringUtils;
/**
* 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.
*/
public class IdentityServiceJITProvisioningHandler
{
private final IdentityServiceFacade identityServiceFacade;
private final PersonService personService;
private final TransactionService transactionService;
private final Function<IdentityServiceFacade.DecodedAccessToken, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = token -> {
Optional<String> firstName = Optional.ofNullable(token.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> lastName = Optional.ofNullable(token.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> email = Optional.ofNullable(token.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
return Optional.ofNullable(token.getClaim(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("")));
};
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
PersonService personService,
TransactionService transactionService)
{
this.identityServiceFacade = identityServiceFacade;
this.personService = personService;
this.transactionService = transactionService;
}
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())
{
return userInfoResponse;
}
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Optional<OIDCUserInfo>>()
{
@Override
public Optional<OIDCUserInfo> doWork() throws Exception
{
return userInfoResponse.map(userInfo -> {
if (userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username()))
{
if (!userInfo.allFieldsNotEmpty())
{
userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo);
}
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);
}
return userInfo;
});
}
}, AuthenticationUtil.getSystemUserName());
}
private Optional<OIDCUserInfo> extractUserInfoResponseFromAccessToken(String bearerToken)
{
return Optional.ofNullable(bearerToken)
.map(identityServiceFacade::decodeToken)
.flatMap(mapTokenToUserInfoResponse);
}
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
{
return identityServiceFacade.getUserInfo(bearerToken)
.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("")));
}
/**
* 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)
{
if (userId == null)
{
return null;
}
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;
}
}

View File

@@ -32,10 +32,8 @@ import java.util.Optional;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.service.cmr.security.PersonService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -50,19 +48,16 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolv
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
{
private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
static final String USERNAME_CLAIM = "preferred_username";
/** Is the mapper enabled */
private boolean isEnabled;
/** Are token validation failures handled silently? */
private boolean isValidationFailureSilent;
/** The person service. */
private PersonService personService;
private BearerTokenResolver bearerTokenResolver;
private IdentityServiceFacade identityServiceFacade;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
/**
* Sets the active flag
@@ -83,26 +78,15 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
{
this.isValidationFailureSilent = silent;
}
/**
* Sets the person service.
*
* @param personService
* the person service
*/
public void setPersonService(PersonService personService)
{
this.personService = personService;
}
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
{
this.bearerTokenResolver = bearerTokenResolver;
}
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
{
this.identityServiceFacade = identityServiceFacade;
this.jitProvisioningHandler = jitProvisioningHandler;
}
/*
@@ -121,14 +105,13 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
}
try
{
String headerUserId = extractUserFromHeader(request);
String normalizedUserId = extractUserFromHeader(request);
if (headerUserId != null)
if (normalizedUserId != null)
{
// Normalize the user ID taking into account case sensitivity settings
String normalizedUserId = normalizeUserId(headerUserId);
LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
return normalizedUserId;
}
}
@@ -179,11 +162,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
return null;
}
final Optional<String> possibleUsername = Optional.ofNullable(bearerToken)
.map(identityServiceFacade::decodeToken)
.map(t -> t.getClaim(USERNAME_CLAIM))
.filter(String.class::isInstance)
.map(String.class::cast);
final Optional<String> possibleUsername = jitProvisioningHandler
.extractUserInfoAndCreateUserIfNeeded(bearerToken)
.map(OIDCUserInfo::username);
if (possibleUsername.isEmpty())
{
@@ -191,39 +172,10 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
return null;
}
String username = possibleUsername.get();
LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(username));
String normalizedUsername = possibleUsername.get();
LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(normalizedUsername));
return username;
}
/**
* 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)
{
if (userId == null)
{
return null;
}
String normalized = AuthenticationUtil.runAs(new RunAsWork<String>()
{
public String doWork() throws Exception
{
return personService.getUserIdentifier(userId);
}
}, AuthenticationUtil.getSystemUserName());
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized));
}
return normalized == null ? userId : normalized;
return normalizedUsername;
}
}

View File

@@ -0,0 +1,73 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.util.stream.Stream;
/**
* Contains a set of required claims about the authentication of an End-User.
*/
public class OIDCUserInfo
{
private final String username;
private final String firstName;
private final String lastName;
private final String email;
public OIDCUserInfo(String username, String firstName, String lastName, String email)
{
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public String username()
{
return username;
}
public String firstName()
{
return firstName;
}
public String lastName()
{
return lastName;
}
public String email()
{
return email;
}
public boolean allFieldsNotEmpty()
{
return Stream.of(username, firstName, lastName, email).allMatch(field -> field != null && !field.isEmpty());
}
}

View File

@@ -23,13 +23,24 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import static java.util.Objects.requireNonNull;
import java.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.ParseException;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -100,6 +111,48 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
return new SpringAccessTokenAuthorization(response);
}
@Override
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter)
{
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
{
return Optional.of(UserInfoResponse.parse(httpResponse));
}
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.getPreferredUsername(), userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress()));
}
@Override
public ClientRegistration getClientRegistration()
{
return clientRegistration;
}
@Override
public DecodedAccessToken decodeToken(String token)
{

View File

@@ -0,0 +1,107 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.admin;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.admin.SysAdminParams;
/**
* Service to handle Admin Console authentication-related cookies.
*/
public class AdminConsoleAuthenticationCookiesService
{
private final SysAdminParams sysAdminParams;
private final int cookieLifetime;
public AdminConsoleAuthenticationCookiesService(SysAdminParams sysAdminParams, int cookieLifetime)
{
this.sysAdminParams = sysAdminParams;
this.cookieLifetime = cookieLifetime;
}
/**
* Get the cookie with the given name.
*
* @param name the name of the cookie
* @param request the request that might contain the cookie
* @return the cookie, or null if the cookie cannot be found
*/
public String getCookie(String name, HttpServletRequest request)
{
String result = null;
Cookie[] cookies = request.getCookies();
if (cookies != null)
{
for (Cookie cookie : cookies)
{
if (cookie.getName().equals(name))
{
result = cookie.getValue();
break;
}
}
}
return result;
}
/**
* Add a cookie to the response.
*
* @param name the name of the cookie
* @param value the value of the cookie
* @param servletResponse the response to add the cookie to
*/
public void addCookie(String name, String value, HttpServletResponse servletResponse)
{
internalAddCookie(name, value, cookieLifetime, servletResponse);
}
/**
* Issue a cookie reset within the given response.
*
* @param name the cookie to reset
* @param servletResponse the response to issue the cookie reset
*/
public void resetCookie(String name, HttpServletResponse servletResponse)
{
internalAddCookie(name, "", 0, servletResponse);
}
private void internalAddCookie(String name, String value, int maxAge, HttpServletResponse servletResponse)
{
Cookie authCookie = new Cookie(name, value);
authCookie.setPath("/");
authCookie.setMaxAge(maxAge);
authCookie.setSecure(sysAdminParams.getAlfrescoProtocol().equalsIgnoreCase("https"));
authCookie.setHttpOnly(true);
servletResponse.addCookie(authCookie);
}
}

View File

@@ -0,0 +1,91 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.admin;
import static java.util.Arrays.asList;
import static java.util.Collections.enumeration;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.alfresco.util.PropertyCheck;
public class AdminConsoleHttpServletRequestWrapper extends HttpServletRequestWrapper
{
private final Map<String, String> additionalHeaders;
private final HttpServletRequest wrappedRequest;
/**
* Constructs a request object wrapping the given request.
*
* @param request the request to wrap
* @throws IllegalArgumentException if the request is null
*/
public AdminConsoleHttpServletRequestWrapper(Map<String, String> additionalHeaders, HttpServletRequest request)
{
super(request);
PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders);
this.additionalHeaders = additionalHeaders;
this.wrappedRequest = request;
}
@Override
public Enumeration<String> getHeaderNames()
{
List<String> result = new ArrayList<>();
Enumeration<String> originalHeaders = wrappedRequest.getHeaderNames();
if (originalHeaders != null)
{
while (originalHeaders.hasMoreElements())
{
String header = originalHeaders.nextElement();
if (!additionalHeaders.containsKey(header))
{
result.add(header);
}
}
}
result.addAll(additionalHeaders.keySet());
return enumeration(result);
}
@Override
public String getHeader(String name)
{
return additionalHeaders.getOrDefault(name, super.getHeader(name));
}
@Override
public Enumeration<String> getHeaders(String name)
{
return enumeration(asList(additionalHeaders.getOrDefault(name, super.getHeader(name))));
}
}

View File

@@ -0,0 +1,253 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.admin;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant.authorizationCode;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID
* or to initiate the OIDC authorization code flow.
*/
public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
{
private static final Logger LOGGER = LoggerFactory.getLogger(IdentityServiceAdminConsoleAuthenticator.class);
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 IdentityServiceFacade identityServiceFacade;
private AdminConsoleAuthenticationCookiesService cookiesService;
private RemoteUserMapper remoteUserMapper;
private boolean isEnabled;
@Override
public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response)
{
// Try to extract username from the authorization header
String username = remoteUserMapper.getRemoteUser(request);
if (username != null)
{
return username;
}
String bearerToken = cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request);
if (bearerToken != null)
{
bearerToken = refreshTokenIfNeeded(request, response, bearerToken);
}
else
{
String code = request.getParameter("code");
if (code != null)
{
bearerToken = retrieveTokenUsingAuthCode(request, response, code);
}
}
if (bearerToken == null)
{
return null;
}
return remoteUserMapper.getRemoteUser(decorateBearerHeader(bearerToken, request));
}
@Override
public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
{
respondWithAuthChallenge(request, response);
}
private void respondWithAuthChallenge(HttpServletRequest request, HttpServletResponse response)
{
try
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Responding with the authentication challenge");
}
response.sendRedirect(getAuthenticationRequest(request));
}
catch (IOException e)
{
LOGGER.error("Error while trying to respond with the authentication challenge: {}", e.getMessage(), e);
throw new AuthenticationException(e.getMessage(), e);
}
}
private String retrieveTokenUsingAuthCode(HttpServletRequest request, HttpServletResponse response, String code)
{
String bearerToken = null;
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Retrieving a response using the Authorization Code at the Token Endpoint");
}
try
{
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
authorizationCode(code, request.getRequestURL().toString()));
addCookies(response, accessTokenAuthorization);
bearerToken = accessTokenAuthorization.getAccessToken().getTokenValue();
}
catch (AuthorizationException exception)
{
if (LOGGER.isWarnEnabled())
{
LOGGER.warn(
"Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}",
exception.getMessage());
}
}
return bearerToken;
}
private String refreshTokenIfNeeded(HttpServletRequest request, HttpServletResponse response, String bearerToken)
{
String refreshToken = cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request);
String authTokenExpiration = cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request);
try
{
if (isAuthTokenExpired(authTokenExpiration))
{
bearerToken = refreshAuthToken(refreshToken, response);
}
}
catch (Exception e)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Error while trying to refresh Auth Token: {}", e.getMessage());
}
bearerToken = null;
resetCookies(response);
}
return bearerToken;
}
private void addCookies(HttpServletResponse response, AccessTokenAuthorization accessTokenAuthorization)
{
cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken().getTokenValue(), response);
cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf(
accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response);
cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response);
}
private String getAuthenticationRequest(HttpServletRequest request)
{
return identityServiceFacade.getClientRegistration().getProviderDetails().getAuthorizationUri()
+ "?client_id="
+ identityServiceFacade.getClientRegistration().getClientId()
+ "&redirect_uri="
+ request.getRequestURL()
+ "&response_type=code"
+ "&scope=openid";
}
private void resetCookies(HttpServletResponse response)
{
cookiesService.resetCookie(ALFRESCO_TOKEN_EXPIRATION, response);
cookiesService.resetCookie(ALFRESCO_ACCESS_TOKEN, response);
cookiesService.resetCookie(ALFRESCO_REFRESH_TOKEN, response);
}
private String refreshAuthToken(String refreshToken, HttpServletResponse response)
{
AccessTokenAuthorization accessTokenAuthorization = doRefreshAuthToken(refreshToken);
addCookies(response, accessTokenAuthorization);
return accessTokenAuthorization.getAccessToken().getTokenValue();
}
private AccessTokenAuthorization doRefreshAuthToken(String refreshToken)
{
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
AuthorizationGrant.refreshToken(refreshToken));
if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
{
throw new AuthenticationException("AccessTokenResponse is null or empty");
}
return accessTokenAuthorization;
}
private static boolean isAuthTokenExpired(String authTokenExpiration)
{
return Instant.now().compareTo(Instant.ofEpochMilli(Long.parseLong(authTokenExpiration))) >= 0;
}
private HttpServletRequest decorateBearerHeader(String authToken, HttpServletRequest servletRequest)
{
Map<String, String> additionalHeaders = new HashMap<>();
additionalHeaders.put("Authorization", "Bearer " + authToken);
return new AdminConsoleHttpServletRequestWrapper(additionalHeaders, servletRequest);
}
public void setIdentityServiceFacade(
IdentityServiceFacade identityServiceFacade)
{
this.identityServiceFacade = identityServiceFacade;
}
public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper)
{
this.remoteUserMapper = remoteUserMapper;
}
public void setCookiesService(
AdminConsoleAuthenticationCookiesService cookiesService)
{
this.cookiesService = cookiesService;
}
@Override
public boolean isActive()
{
return this.isEnabled;
}
public void setActive(boolean isEnabled)
{
this.isEnabled = isEnabled;
}
}

View File

@@ -128,6 +128,22 @@
</property>
</bean>
<bean id="AdminConsoleAuthenticator"
class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
<property name="applicationContextManager">
<ref bean="Authentication" />
</property>
<property name="interfaces">
<list>
<value>org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator</value>
<value>org.alfresco.repo.management.subsystems.ActivateableBean</value>
</list>
</property>
<property name="sourceBeanName">
<value>adminConsoleAuthenticator</value>
</property>
</bean>
<!-- Passwords are encoded using MD4 -->
<!-- This is not ideal and only done to be compatible with NTLM -->
<!-- authentication against the default authentication mechanism. -->

View File

@@ -89,6 +89,8 @@
</property>
</bean>
<bean id="adminConsoleAuthenticator" class="org.alfresco.repo.security.authentication.external.DefaultAdminConsoleAuthenticator" />
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
<property name="nodeService" ref="nodeService" />
<property name="authorityService" ref="authorityService" />

View File

@@ -24,6 +24,9 @@
<property name="identityServiceFacade">
<ref bean="identityServiceFacade"/>
</property>
<property name="jitProvisioningHandler">
<ref bean="jitProvisioningHandler" />
</property>
</bean>
<bean name="identityServiceFacade" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean">
@@ -138,7 +141,7 @@
<value>${identity-service.public-client:false}</value>
</property>
</bean>
<!-- Enable control over mapping between request and user ID -->
<bean id="remoteUserMapper" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper">
<property name="active">
@@ -147,17 +150,40 @@
<property name="validationFailureSilent">
<value>${identity-service.authentication.validation.failure.silent}</value>
</property>
<property name="personService">
<ref bean="PersonService" />
</property>
<property name="bearerTokenResolver">
<bean class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver" />
</property>
<property name="identityServiceFacade">
<ref bean="identityServiceFacade" />
<property name="jitProvisioningHandler">
<ref bean="jitProvisioningHandler" />
</property>
</bean>
<bean id="adminConsoleAuthenticationCookiesService" class="org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesService">
<constructor-arg ref="sysAdminParams" />
<constructor-arg value="${admin.console.cookie.lifetime:86400}" />
</bean>
<bean id="adminConsoleAuthenticator" class="org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticator">
<property name="active">
<value>${identity-service.authentication.enabled}</value>
</property>
<property name="identityServiceFacade">
<ref bean="identityServiceFacade"/>
</property>
<property name="cookiesService">
<ref bean="adminConsoleAuthenticationCookiesService" />
</property>
<property name="remoteUserMapper">
<ref bean="remoteUserMapper" />
</property>
</bean>
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
<constructor-arg ref="PersonService"/>
<constructor-arg ref="identityServiceFacade"/>
<constructor-arg ref="transactionService"/>
</bean>
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
<property name="nodeService" ref="nodeService" />
<property name="authorityService" ref="authorityService" />

View File

@@ -26,8 +26,12 @@
package org.alfresco;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
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.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
import org.junit.experimental.categories.Categories;
@@ -143,6 +147,10 @@ import org.junit.runners.Suite;
IdentityServiceFacadeFactoryBeanTest.class,
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
SpringBasedIdentityServiceFacadeUnitTest.class,
IdentityServiceJITProvisioningHandlerUnitTest.class,
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2017 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -60,6 +60,7 @@ import org.junit.runners.Suite;
org.alfresco.repo.security.authentication.external.DefaultRemoteUserMapperTest.class,
org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponentTest.class,
org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapperTest.class,
org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerTest.class,
org.alfresco.repo.security.authentication.subsystems.SubsystemChainingFtpAuthenticatorTest.class,
org.alfresco.repo.security.authentication.external.LocalAuthenticationServiceTest.class,
org.alfresco.repo.domain.contentdata.ContentDataDAOTest.class,

View File

@@ -30,6 +30,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.ConnectException;
import java.util.Optional;
import org.alfresco.error.ExceptionStackUtil;
import org.alfresco.repo.security.authentication.AuthenticationContext;
@@ -66,6 +67,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Autowired
private PersonService personService;
private IdentityServiceJITProvisioningHandler jitProvisioning;
private IdentityServiceFacade mockIdentityServiceFacade;
@Before
@@ -77,7 +80,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
authComponent.setNodeService(nodeService);
authComponent.setPersonService(personService);
jitProvisioning = mock(IdentityServiceJITProvisioningHandler.class);
mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
authComponent.setJitProvisioningHandler(jitProvisioning);
authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
}
@@ -134,8 +139,13 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
{
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
IdentityServiceFacade.AccessToken accessToken = mock(IdentityServiceFacade.AccessToken.class);
when(authorization.getAccessToken()).thenReturn(accessToken);
when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
when(jitProvisioning.extractUserInfoAndCreateUserIfNeeded("JWT_TOKEN"))
.thenReturn(Optional.of(new OIDCUserInfo("username", "", "", "")));
authComponent.authenticateImpl("username", "password".toCharArray());

View File

@@ -25,7 +25,6 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -33,6 +32,7 @@ import static org.mockito.Mockito.when;
import java.util.Map;
import java.util.UUID;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
import org.junit.Test;
@@ -64,7 +64,7 @@ public class IdentityServiceFacadeFactoryBeanTest
final Map<String, Object> claims = decodedToken.getClaims();
assertThat(claims).isNotNull()
.isNotEmpty()
.containsEntry(USERNAME_CLAIM, "piotrek");
.containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "piotrek");
}
@Test

View File

@@ -0,0 +1,162 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.Optional;
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.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.BaseSpringTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@SuppressWarnings("PMD.AvoidAccessibilityAlteration")
public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
{
private static final String IDS_USERNAME = "johndoe123";
private PersonService personService;
private NodeService nodeService;
private TransactionService transactionService;
private IdentityServiceFacade identityServiceFacade;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
@Before
public void setup()
{
personService = (PersonService) applicationContext.getBean("personService");
nodeService = (NodeService) applicationContext.getBean("nodeService");
transactionService = (TransactionService) applicationContext.getBean("transactionService");
DefaultChildApplicationContextManager childApplicationContextManager = (DefaultChildApplicationContextManager) applicationContext
.getBean("Authentication");
ChildApplicationContextFactory childApplicationContextFactory = childApplicationContextManager.getChildApplicationContextFactory(
"identity-service1");
identityServiceFacade = (IdentityServiceFacade) childApplicationContextFactory.getApplicationContext()
.getBean("identityServiceFacade");
jitProvisioningHandler = (IdentityServiceJITProvisioningHandler) childApplicationContextFactory.getApplicationContext()
.getBean("jitProvisioningHandler");
IdentityServiceConfig identityServiceConfig = (IdentityServiceConfig) childApplicationContextFactory.getApplicationContext()
.getBean("identityServiceConfig");
identityServiceConfig.setAllowAnyHostname(true);
identityServiceConfig.setClientKeystore(null);
identityServiceConfig.setDisableTrustManager(true);
}
@Test
public void shouldCreateNonExistingUserInRepo()
{
assertFalse(personService.personExists(IDS_USERNAME));
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
accessTokenAuthorization.getAccessToken().getTokenValue());
NodeRef person = personService.getPerson(IDS_USERNAME);
assertTrue(userInfoOptional.isPresent());
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
assertEquals("John", userInfoOptional.get().firstName());
assertEquals("Doe", userInfoOptional.get().lastName());
assertEquals("johndoe@test.com", userInfoOptional.get().email());
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
}
@Test
public void shouldCallUserInfoEndpointAndCreateUser() throws IllegalAccessException, NoSuchFieldException
{
assertFalse(personService.personExists(IDS_USERNAME));
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
when(idsServiceFacadeMock.getUserInfo(accessToken)).thenReturn(identityServiceFacade.getUserInfo(accessToken));
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
Field declaredField = jitProvisioningHandler.getClass()
.getDeclaredField("identityServiceFacade");
declaredField.setAccessible(true);
declaredField.set(jitProvisioningHandler, idsServiceFacadeMock);
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
accessToken);
declaredField.set(jitProvisioningHandler, identityServiceFacade);
NodeRef person = personService.getPerson(IDS_USERNAME);
assertTrue(userInfoOptional.isPresent());
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
assertEquals("John", userInfoOptional.get().firstName());
assertEquals("Doe", userInfoOptional.get().lastName());
assertEquals("johndoe@test.com", userInfoOptional.get().email());
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
verify(idsServiceFacadeMock).decodeToken(accessToken);
verify(idsServiceFacadeMock).getUserInfo(accessToken);
}
@After
public void tearDown()
{
AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork<Void>()
{
@Override
public Void doWork() throws Exception
{
transactionService.getRetryingTransactionHelper()
.doInTransaction((RetryingTransactionCallback<Void>) () -> {
personService.deletePerson(IDS_USERNAME);
return null;
});
return null;
}
});
}
}

View File

@@ -0,0 +1,196 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
public class IdentityServiceJITProvisioningHandlerUnitTest
{
@Mock
private IdentityServiceFacade identityServiceFacade;
@Mock
private PersonService personService;
@Mock
private IdentityServiceFacade.DecodedAccessToken decodedAccessToken;
@Mock
private TransactionService transactionService;
@Mock
private OIDCUserInfo userInfo;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private static final String JWT_TOKEN = "myToken";
@Before
public void setup()
{
initMocks(this);
when(transactionService.isReadOnly()).thenReturn(false);
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
when(personService.createMissingPeople()).thenReturn(true);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
personService, transactionService);
}
@Test
public void shouldExtractUserInfoForExistingUser()
{
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
}
@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");
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());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
}
@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)).thenReturn(Optional.of(userInfo));
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());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
}
@Test
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
{
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertFalse(result.isPresent());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
}
@Test
public void shouldCallUserInfoEndpointToGetUsername()
{
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", 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);
}
@Test
public void shouldNotCallUserInfoEndpointIfTokenIsNullOrEmpty()
{
jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(null);
jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded("");
verify(personService, never()).createPerson(any());
verify(identityServiceFacade, never()).decodeToken(null);
verify(identityServiceFacade, never()).decodeToken("");
verify(identityServiceFacade, never()).getUserInfo(null);
verify(identityServiceFacade, never()).getUserInfo("");
}
}

View File

@@ -25,9 +25,6 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static java.util.Optional.ofNullable;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -38,13 +35,15 @@ import java.util.Map;
import java.util.Vector;
import java.util.function.Supplier;
import jakarta.servlet.http.HttpServletRequest;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import jakarta.servlet.http.HttpServletRequest;
import junit.framework.TestCase;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
/**
@@ -92,16 +91,19 @@ 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 PersonService personService = mock(PersonService.class);
when(transactionService.isReadOnly()).thenReturn(true);
when(facade.decodeToken(anyString()))
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
final PersonService personService = mock(PersonService.class);
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService);
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
mapper.setIdentityServiceFacade(facade);
mapper.setPersonService(personService);
mapper.setJitProvisioningHandler(jitProvisioning);
mapper.setActive(true);
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
@@ -160,7 +162,7 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
@Override
public Object getClaim(String claim)
{
return USERNAME_CLAIM.equals(claim) ? usernameSupplier.get() : null;
return PersonClaims.PREFERRED_USERNAME_CLAIM_NAME.equals(claim) ? usernameSupplier.get() : null;
}
}
}

View File

@@ -25,6 +25,7 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -73,11 +74,24 @@ public class SpringBasedIdentityServiceFacadeUnitTest
.havingCause().withNoCause().withMessage("Expected");
}
@Test
public void shouldReturnEmptyOptionalOnFailure()
{
final RestOperations restOperations = mock(RestOperations.class);
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThat(facade.getUserInfo(TOKEN).isEmpty()).isTrue();
}
private ClientRegistration testRegistration()
{
return ClientRegistration.withRegistrationId("test")
.tokenUri("http://localhost")
.clientId("test")
.userInfoUri("http://localhost/userinfo")
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build();
}

View File

@@ -0,0 +1,185 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.admin.SysAdminParams;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
public class AdminConsoleAuthenticationCookiesServiceUnitTest
{
private static final int DEFAULT_COOKIE_LIFETIME = 86400;
private static final String COOKIE_NAME = "cookie";
private static final String COOKIE_VALUE = "value";
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Mock
private SysAdminParams sysAdminParams;
@Captor
private ArgumentCaptor<Cookie> cookieCaptor;
private AdminConsoleAuthenticationCookiesService cookiesService;
@Before
public void setUp()
{
initMocks(this);
cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, DEFAULT_COOKIE_LIFETIME);
}
@Test
public void cookieShouldBeFoundInRequestThatContainsIt()
{
when(request.getCookies()).thenReturn(new Cookie[] { new Cookie(COOKIE_NAME, COOKIE_VALUE) });
String cookie = cookiesService.getCookie(COOKIE_NAME, request);
assertNotNull("The cookie should not be null", cookie);
assertEquals("The cookie's value should match", COOKIE_VALUE, cookie);
verify(request).getCookies();
}
@Test
public void cookieShouldNotBeFoundInRequestThatDoesNotContainIt()
{
when(request.getCookies()).thenReturn(new Cookie[] { new Cookie(COOKIE_NAME, COOKIE_VALUE) });
assertNull("The cookie should be null", cookiesService.getCookie("non-contained-cookie", request));
verify(request).getCookies();
}
@Test
public void cookieShouldNotBeFoundInRequestWithoutCookies()
{
when(request.getCookies()).thenReturn(null);
assertNull("The cookie should be null", cookiesService.getCookie(COOKIE_NAME, request));
verify(request).getCookies();
}
@Test
public void cookieShouldBeAddedToTheResponseWithDefaultParams()
{
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("http");
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
verify(sysAdminParams).getAlfrescoProtocol();
verify(response).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertNotNull("The cookie should not be null", cookie);
assertEquals("Cookie's name should match", COOKIE_NAME, cookie.getName());
assertEquals("Cookie's value should match", COOKIE_VALUE, cookie.getValue());
assertEquals("Cookie's path should be the root", "/", cookie.getPath());
assertEquals("Cookie's maxAge should match the default lifetime", DEFAULT_COOKIE_LIFETIME, cookie.getMaxAge());
assertFalse("Cookie's secure flag should be false", cookie.getSecure());
}
@Test
public void secureCookieShouldBeAddedToTheResponseWhenAlfrescoProtocolIsHttps()
{
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
verify(sysAdminParams).getAlfrescoProtocol();
verify(response).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertNotNull("The cookie should not be null", cookie);
assertTrue("Cookie's secure flag should be true", cookie.getSecure());
}
@Test
public void cookieWithCustomMaxAgeShouldBeAddedToTheResponse()
{
int customMaxAge = 60;
cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, customMaxAge);
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
verify(sysAdminParams).getAlfrescoProtocol();
verify(response).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertNotNull("The cookie should not be null", cookie);
assertEquals("Cookie's maxAge should match the custom lifetime", customMaxAge, cookie.getMaxAge());
}
@Test
public void cookieShouldBeReset()
{
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("http");
cookiesService.resetCookie(COOKIE_NAME, response);
verify(sysAdminParams).getAlfrescoProtocol();
verify(response).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertNotNull("The cookie should not be null", cookie);
assertEquals("Cookie's name should match", COOKIE_NAME, cookie.getName());
assertEquals("Cookie's value should be reset", "", cookie.getValue());
assertEquals("Cookie's path should be the root", "/", cookie.getPath());
assertEquals("Cookie's maxAge should be 0", 0, cookie.getMaxAge());
assertFalse("Cookie's secure flag should be false", cookie.getSecure());
}
@Test
public void secureCookieShouldBeReset()
{
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
cookiesService.resetCookie(COOKIE_NAME, response);
verify(sysAdminParams).getAlfrescoProtocol();
verify(response).addCookie(cookieCaptor.capture());
Cookie cookie = cookieCaptor.getValue();
assertNotNull("The cookie should not be null", cookie);
assertTrue("Cookie's secure flag should be true", cookie.getSecure());
}
}

View File

@@ -0,0 +1,213 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.admin;
import static java.util.Collections.enumeration;
import static java.util.Collections.list;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.alfresco.error.AlfrescoRuntimeException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
@SuppressWarnings("PMD.UseDiamondOperator")
public class AdminConsoleHttpServletRequestWrapperUnitTest
{
private static final String DEFAULT_HEADER = "default_header";
private static final String DEFAULT_HEADER_VALUE = "default_value";
private static final String ADDITIONAL_HEADER = "additional_header";
private static final String ADDITIONAL_HEADER_VALUE = "additional_value";
private static final Map<String, String> DEFAULT_HEADERS = new HashMap<String, String>()
{{
put(DEFAULT_HEADER, DEFAULT_HEADER_VALUE);
}};
private static final Map<String, String> ADDITIONAL_HEADERS = new HashMap<String, String>()
{{
put(ADDITIONAL_HEADER, ADDITIONAL_HEADER_VALUE);
}};
@Mock
private HttpServletRequest request;
private AdminConsoleHttpServletRequestWrapper requestWrapper;
@Before
public void setUp()
{
initMocks(this);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(ADDITIONAL_HEADERS, request);
}
@Test(expected = AlfrescoRuntimeException.class)
public void wrapperShouldNotBeInstancedWithoutAdditionalHeaders()
{
new AdminConsoleHttpServletRequestWrapper(null, request);
}
@Test(expected = IllegalArgumentException.class)
public void wrapperShouldNotBeInstancedWithoutRequestsToWrap()
{
new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), null);
}
@Test
public void wrapperShouldReturnAdditionalHeaderNamesOnTopOfDefaultOnes()
{
when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet()));
Enumeration<String> headerNames = requestWrapper.getHeaderNames();
assertNotNull("headerNames should not be null", headerNames);
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
List<String> headers = list(headerNames);
assertEquals("There should be 2 headers", 2, headers.size());
assertTrue("The default header should be included", headers.contains(DEFAULT_HEADER));
assertTrue("The additional header should be included", headers.contains(ADDITIONAL_HEADER));
verify(request).getHeaderNames();
}
@Test
public void wrapperShouldReturnDefaultHeaderNamesIfNoAdditionalHeaders()
{
when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet()));
requestWrapper = new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), request);
Enumeration<String> headerNames = requestWrapper.getHeaderNames();
assertNotNull("headerNames should not be null", headerNames);
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
assertEquals("The returned header should be the default header", DEFAULT_HEADER, headerNames.nextElement());
assertFalse("There should be no additional headers", headerNames.hasMoreElements());
verify(request).getHeaderNames();
}
@Test
public void wrapperShouldReturnAdditionalHeaderNamesIfNoDefaultHeaders()
{
when(request.getHeaderNames()).thenReturn(null);
Enumeration<String> headerNames = requestWrapper.getHeaderNames();
assertNotNull("headerNames should not be null", headerNames);
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
assertEquals("The returned header should be the additional header", ADDITIONAL_HEADER,
headerNames.nextElement());
assertFalse("There should be no more headers", headerNames.hasMoreElements());
verify(request).getHeaderNames();
}
@Test
public void wrapperShouldReturnDefaultHeaderValues()
{
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
String header = requestWrapper.getHeader(DEFAULT_HEADER);
assertEquals("The header should be the default one", DEFAULT_HEADER_VALUE, header);
verify(request).getHeader(DEFAULT_HEADER);
}
@Test
public void wrapperShouldReturnAdditionalHeaderValues()
{
String header = requestWrapper.getHeader(ADDITIONAL_HEADER);
assertEquals("The header should be the additional one", ADDITIONAL_HEADER_VALUE, header);
}
@Test
public void wrapperShouldPreferAdditionalHeaderValuesToDefaultOnes()
{
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
String overrideHeaderValue = "override";
Map<String, String> overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request);
String header = requestWrapper.getHeader(DEFAULT_HEADER);
assertEquals("The header should have the overridden value", overrideHeaderValue, header);
verify(request).getHeader(DEFAULT_HEADER);
}
@Test
public void wrapperShouldReturnDefaultHeaderEnumeration()
{
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
Enumeration<String> headers = requestWrapper.getHeaders(DEFAULT_HEADER);
assertNotNull("The headers enumeration should not be null", headers);
assertTrue("The headers enumeration should not be empty", headers.hasMoreElements());
assertEquals("The header should be the default one", DEFAULT_HEADER_VALUE, headers.nextElement());
assertFalse("There should be no more headers", headers.hasMoreElements());
verify(request).getHeader(DEFAULT_HEADER);
}
@Test
public void wrapperShouldReturnAdditionalHeaderEnumeration()
{
Enumeration<String> headers = requestWrapper.getHeaders(ADDITIONAL_HEADER);
assertNotNull("The headers enumeration should not be null", headers);
assertTrue("The headers enumeration should not be empty", headers.hasMoreElements());
assertEquals("The header should be the additional one", ADDITIONAL_HEADER_VALUE, headers.nextElement());
assertFalse("There should be no more headers", headers.hasMoreElements());
}
@Test
public void wrapperShouldPreferAdditionalHeaderEnumerationValuesToDefaultOnes()
{
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
String overrideHeaderValue = "override";
Map<String, String> overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request);
Enumeration<String> headers = requestWrapper.getHeaders(DEFAULT_HEADER);
assertNotNull("The headers enumeration should not be null", headers);
assertTrue("The headers enumeration should not be empty", headers.hasMoreElements());
assertEquals("The header should be the overridden one", overrideHeaderValue, headers.nextElement());
assertFalse("There should be no more headers", headers.hasMoreElements());
verify(request).getHeader(DEFAULT_HEADER);
}
}

View File

@@ -0,0 +1,200 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.IOException;
import java.time.Instant;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
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.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
@SuppressWarnings("PMD.AvoidStringBufferField")
public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
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";
@Mock
HttpServletRequest request;
@Mock
HttpServletResponse response;
@Mock
IdentityServiceFacade identityServiceFacade;
@Mock
AdminConsoleAuthenticationCookiesService cookiesService;
@Mock
RemoteUserMapper remoteUserMapper;
@Mock
AccessTokenAuthorization accessTokenAuthorization;
@Mock
AccessToken accessToken;
@Captor
ArgumentCaptor<AdminConsoleHttpServletRequestWrapper> requestCaptor;
IdentityServiceAdminConsoleAuthenticator authenticator;
StringBuffer adminConsoleURL = new StringBuffer("http://localhost:8080/admin-console");
@Before
public void setup()
{
initMocks(this);
ClientRegistration clientRegistration = mock(ClientRegistration.class);
ProviderDetails providerDetails = mock(ProviderDetails.class);
when(clientRegistration.getProviderDetails()).thenReturn(providerDetails);
when(clientRegistration.getClientId()).thenReturn("alfresco");
when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth");
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
when(request.getRequestURL()).thenReturn(adminConsoleURL);
when(remoteUserMapper.getRemoteUser(request)).thenReturn(null);
authenticator = new IdentityServiceAdminConsoleAuthenticator();
authenticator.setActive(true);
authenticator.setIdentityServiceFacade(identityServiceFacade);
authenticator.setCookiesService(cookiesService);
authenticator.setRemoteUserMapper(remoteUserMapper);
}
@Test
public void shouldCallRemoteMapperIfTokenIsInCookies()
{
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
assertEquals("admin", username);
assertTrue(authenticator.isActive());
}
@Test
public void shouldRefreshExpiredTokenAndCallRemoteMapper()
{
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
when(accessToken.getTokenValue()).thenReturn("REFRESHED_JWT_TOKEN");
when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60));
when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN");
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "REFRESHED_JWT_TOKEN", response);
verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
assertEquals("Bearer REFRESHED_JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
assertEquals("admin", username);
}
@Test
public void shouldCallAuthChallenge() throws IOException
{
String authenticationRequest = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=" + adminConsoleURL
+ "&response_type=code&scope=openid";
authenticator.requestAuthentication(request, response);
verify(response).sendRedirect(authenticationRequest);
}
@Test
public void shouldResetCookiesAndCallAuthChallenge() throws IOException
{
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class);
String username = authenticator.getAdminConsoleUser(request, response);
verify(cookiesService).resetCookie(ALFRESCO_ACCESS_TOKEN, response);
verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response);
verify(cookiesService).resetCookie(ALFRESCO_TOKEN_EXPIRATION, response);
assertNull(username);
}
@Test
public void shouldAuthorizeCodeAndSetCookies()
{
when(request.getParameter("code")).thenReturn("auth_code");
when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60));
when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN");
when(identityServiceFacade.authorize(
AuthorizationGrant.authorizationCode("auth_code", adminConsoleURL.toString())))
.thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "JWT_TOKEN", response);
verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
assertEquals("admin", username);
}
@Test
public void shouldExtractUsernameFromAuthorizationHeader()
{
when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
assertEquals("admin", username);
}
}

View File

@@ -1,15 +1,8 @@
# Test identity service authentication overrides
#identity-service.auth-server-url=http://192.168.0.1:8180/auth
identity-service.realm=alfresco
identity-service.realm-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l\
BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM\
uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W\
ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD\
P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j\
QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx\
jwIDAQAB
identity-service.ssl-required=external
identity-service.resource=test
identity-service.resource=alfresco
identity-service.public-client=false
identity-service.confidential-port=100
identity-service.use-resource-role-mappings=true
@@ -41,11 +34,11 @@ identity-service.min-time-between-jwks-requests=60
identity-service.public-key-cache-ttl=3600
identity-service.enable-pkce=true
identity-service.ignore-oauth-query-parameter=true
identity-service.credentials.secret=11111
identity-service.credentials.secret=
identity-service.credentials.provider=secret
identity-service.client-socket-timeout=1000
identity-service.client-connection-timeout=3000
identity-service.authentication.enable-username-password-authentication=false
identity-service.authentication.enable-username-password-authentication=true
# Use a date in the past, so data is read straight away rather than being scheduled in tests. A few ms is too late.
mimetype.config.cronExpression=0 0 0 ? JAN * 1970

View File

@@ -1866,6 +1866,46 @@
],
"totp": false,
"username": "testuser"
},
{
"clientRoles": {
"account": [
"manage-account",
"view-profile"
]
},
"credentials": [
{
"algorithm": "pbkdf2",
"counter": 0,
"digits": 0,
"hashIterations": 20000,
"hashedSaltedValue": "+A2UrlK6T33IHVutjQj9k8S8kMco1IMnmCTngEg+PE+2vO4jJScux6wcltsRIYILv5ggcS3PI7tbsynq5u39sQ==",
"period": 0,
"salt": "IyVlItIo27bmACSLi4yQkA==",
"type": "password"
}
],
"disableableCredentialTypes": [
"password"
],
"email": "johndoe@test.com",
"emailVerified": false,
"enabled": true,
"firstName": "John",
"groups": [
"/admin",
"/testgroup"
],
"lastName": "Doe",
"realmRoles": [
"uma_authorization",
"user",
"offline_access",
"test_role"
],
"totp": false,
"username": "johndoe123"
}
],
"keycloakVersion": "8.0.1",

View File

@@ -10,7 +10,7 @@ services:
- "8090:8090"
postgres:
image: postgres:15.4
profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio"]
profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio", "with-sso"]
environment:
- POSTGRES_PASSWORD=alfresco
- POSTGRES_USER=alfresco
@@ -19,7 +19,7 @@ services:
ports:
- "5433:5432"
activemq:
profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio"]
profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio", "with-sso"]
image: alfresco/alfresco-activemq:5.18.3-jre17-rockylinux8
ports:
- "5672:5672" # AMQP
@@ -57,3 +57,15 @@ services:
CLIENT_SSL_TRUST_STORE: "file:/tengineAIO.truststore"
CLIENT_SSL_TRUST_STORE_PASSWORD: "password"
CLIENT_SSL_TRUST_STORE_TYPE: "JCEKS"
keycloak:
profiles: ["with-sso"]
image: quay.io/keycloak/keycloak:21.1.2
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
- KC_DB=dev-mem
command: ["start-dev", "--import-realm", "--http-relative-path=/auth", "--hostname=localhost", "--http-enabled=true"]
volumes:
- ../../../repository/src/test/resources/realms/alfresco-realm.json:/opt/keycloak/data/import/alfresco-realm.json
ports:
- 8999:8080

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
echo "=========================== Starting AppContext05TestSuite setup ==========================="
PS4="\[\e[35m\]+ \[\e[m\]"
set -vex
pushd "$(dirname "${BASH_SOURCE[0]}")/../../../"
mkdir -p "${HOME}/tmp"
cp repository/src/test/resources/realms/alfresco-realm.json "${HOME}/tmp"
echo "HOST_IP=$(hostname -I | cut -f1 -d' ')" >> $GITHUB_ENV
docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e DB_VENDOR=h2 -p 8999:8080 -e KEYCLOAK_IMPORT=/tmp/alfresco-realm.json -v $HOME/tmp/alfresco-realm.json:/tmp/alfresco-realm.json alfresco/alfresco-identity-service:1.2
popd
set +vex
echo "=========================== Finishing AppContext05TestSuite setup =========================="