mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-09-24 14:32:01 +00:00
Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2c41c0ead1 | ||
|
c957f40a78 | ||
|
f8a8b5c8f3 | ||
|
773d9ca9d9 | ||
|
1af0b72257 | ||
|
158fb067d0 | ||
|
016c212c65 | ||
|
c1bd8e71c4 | ||
|
f68446b3e5 | ||
|
9b26c66dec | ||
|
06c35ea379 | ||
|
5580609010 | ||
|
e16a3759ad | ||
|
9cce8d54d8 | ||
|
856bf011c5 | ||
|
d46fc62cdb | ||
|
eff41eef12 | ||
|
3d0185574d | ||
|
5664b5de78 | ||
|
82f44122bc | ||
|
6c3740c2a6 | ||
|
ecdbf41291 | ||
|
6abf01e083 | ||
|
7671d3b7bc | ||
|
a8a6b565a7 | ||
|
a660109b73 | ||
|
11cc7fd2cb | ||
|
fe356c6135 | ||
|
8b75f4f961 | ||
|
db9d6cc08c | ||
|
975503bf88 | ||
|
9f4ec29f9c | ||
|
5490228d71 | ||
|
408800ba89 | ||
|
773dd1d3e5 | ||
|
0d8b6ef853 | ||
|
63ef313ad5 | ||
|
a63cfecbcc | ||
|
d8e71ad38a | ||
|
d6bf9a5748 | ||
|
75a3e03dfa | ||
|
e0239cd48f | ||
|
a490b8d437 | ||
|
1c1a2183a3 | ||
|
0f0c143c6f | ||
|
9d41373752 | ||
|
5d4471bbb3 | ||
|
8451df09a8 | ||
|
935fcf1c16 | ||
|
f4a1cb5726 | ||
|
7d8cc58063 | ||
|
2005aa0754 | ||
|
c4b5da9452 | ||
|
c2bba964cb | ||
|
4b42ecf000 | ||
|
b042fbb3ca | ||
|
617eb7bb93 | ||
|
a3ccafb035 | ||
|
6065c6feec |
73
.github/workflows/ci.yml
vendored
73
.github/workflows/ci.yml
vendored
@@ -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
|
||||
@@ -78,7 +128,12 @@ jobs:
|
||||
!contains(github.event.head_commit.message, '[skip tests]') &&
|
||||
!contains(github.event.head_commit.message, '[force]')
|
||||
steps:
|
||||
- uses: Alfresco/ya-pmd-scan@v2.0.5
|
||||
- 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/ya-pmd-scan@v3.0.0
|
||||
with:
|
||||
classpath-build-command: "mvn test-compile -ntp -Pags -pl \"-:alfresco-community-repo-docker\""
|
||||
|
||||
all_unit_tests_suite:
|
||||
name: "Core, Data-Model, Repository - AllUnitTestsSuite - Build and test"
|
||||
@@ -351,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
|
||||
@@ -376,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: |
|
||||
@@ -388,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"
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,6 +39,9 @@ dependency-reduced-pom.xml
|
||||
|
||||
hs_err_pid*
|
||||
|
||||
# Development
|
||||
repository/scripts/hazelcast-init/alfresco-hazelcast-config.xml
|
||||
|
||||
# Alfresco runtime
|
||||
alf_data
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-amps</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-governance-services-community-parent</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-governance-services-automation-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<build>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-governance-services-community-parent</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
SOLR6_TAG=2.0.8.1
|
||||
POSTGRES_TAG=15.4
|
||||
ACTIVEMQ_TAG=5.18.2-jre17-rockylinux8
|
||||
ACTIVEMQ_TAG=5.18.3-jre17-rockylinux8
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<build>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-amps</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
|
@@ -9,6 +9,6 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-packaging</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
</project>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-packaging</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
SOLR6_TAG=2.0.8.1
|
||||
POSTGRES_TAG=15.4
|
||||
ACTIVEMQ_TAG=5.18.2-jre17-rockylinux8
|
||||
ACTIVEMQ_TAG=5.18.3-jre17-rockylinux8
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-packaging</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
|
@@ -9,13 +9,27 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If MTLS enabled configure keystore/truststore for curl command
|
||||
if [[ $ALFRESCO_URL == https* ]]; then
|
||||
KEYSTORE_TRUSTSTORE_PATH="${CI_WORKSPACE}/keystores/testClient"
|
||||
KEYSTORE_PASSWORD="password"
|
||||
|
||||
ADDITIONAL_MTLS_CONFIG="--key $KEYSTORE_TRUSTSTORE_PATH/client-key.pem --cert $KEYSTORE_TRUSTSTORE_PATH/client-cert.pem:$KEYSTORE_PASSWORD --cacert $KEYSTORE_TRUSTSTORE_PATH/testClient_truststore.pem"
|
||||
if [[ ${HOSTNAME_VERIFICATION_DISABLED} == true ]]; then
|
||||
ADDITIONAL_MTLS_CONFIG=$ADDITIONAL_MTLS_CONFIG" -k"
|
||||
fi
|
||||
else
|
||||
ADDITIONAL_MTLS_CONFIG=""
|
||||
fi
|
||||
|
||||
WAIT_INTERVAL=1
|
||||
COUNTER=0
|
||||
TIMEOUT=300
|
||||
t0=$(date +%s)
|
||||
|
||||
echo "Waiting for alfresco to start"
|
||||
until $(curl --output /dev/null --silent --head --fail ${ALFRESCO_URL}) || [ "$COUNTER" -eq "$TIMEOUT" ]; do
|
||||
echo curl --output /dev/null --silent --head --fail ${ADDITIONAL_MTLS_CONFIG} ${ALFRESCO_URL}
|
||||
until $(curl --output /dev/null --silent --head --fail ${ADDITIONAL_MTLS_CONFIG} ${ALFRESCO_URL}) || [ "$COUNTER" -eq "$TIMEOUT" ]; do
|
||||
printf '.'
|
||||
sleep $WAIT_INTERVAL
|
||||
COUNTER=$(($COUNTER+$WAIT_INTERVAL))
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-tests</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<organization>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-tests</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<developers>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-tests</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<developers>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-tests</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
|
@@ -4,18 +4,14 @@ import org.alfresco.dataprep.CMISUtil.DocumentType;
|
||||
import org.alfresco.rest.RestTest;
|
||||
import org.alfresco.rest.exception.JsonToModelConversionException;
|
||||
import org.alfresco.rest.model.RestProcessDefinitionModel;
|
||||
import org.alfresco.rest.model.RestProcessModel;
|
||||
import org.alfresco.rest.model.RestProcessModelsCollection;
|
||||
import org.alfresco.utility.model.*;
|
||||
import org.alfresco.utility.testrail.ExecutionType;
|
||||
import org.alfresco.utility.testrail.annotation.TestRail;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.testng.annotations.BeforeClass;
|
||||
import org.testng.annotations.Ignore;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Cristina Axinte
|
||||
@@ -48,18 +44,16 @@ public class GetProcessesCoreTests extends RestTest
|
||||
|
||||
@TestRail(section = { TestGroup.REST_API, TestGroup.WORKFLOW,TestGroup.PROCESSES }, executionType = ExecutionType.REGRESSION,
|
||||
description = "Verify user gets all processes started by him ordered descending by id")
|
||||
@Test(groups = { TestGroup.REST_API, TestGroup.WORKFLOW, TestGroup.PROCESSES, TestGroup.REGRESSION }, enabled = false)
|
||||
@Ignore("Until ACS-6234 is done")
|
||||
@Test(groups = { TestGroup.REST_API, TestGroup.WORKFLOW, TestGroup.PROCESSES, TestGroup.REGRESSION })
|
||||
public void getProcessesOrderedByIdDESC()
|
||||
{
|
||||
RestProcessModelsCollection processes = restClient.authenticateUser(userWhoStartsTask).withParams("orderBy=id DESC")
|
||||
.withWorkflowAPI().getProcesses();
|
||||
|
||||
restClient.assertStatusCodeIs(HttpStatus.OK);
|
||||
processes.assertThat().entriesListIsNotEmpty();
|
||||
List<RestProcessModel> processesList = processes.getEntries();
|
||||
processesList.get(0).onModel().assertThat().field("id").is(process3.getId());
|
||||
processesList.get(1).onModel().assertThat().field("id").is(task2.getProcessId());
|
||||
processesList.get(2).onModel().assertThat().field("id").is(task1.getProcessId());
|
||||
processes.assertThat().entriesListIsNotEmpty()
|
||||
.and().entriesListIsSortedDescBy("id")
|
||||
.and().entrySetContains("id", task1.getProcessId(), task2.getProcessId(), process3.getId());
|
||||
}
|
||||
|
||||
@TestRail(section = { TestGroup.REST_API, TestGroup.WORKFLOW,TestGroup.PROCESSES }, executionType = ExecutionType.REGRESSION,
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-tests</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<developers>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo-packaging</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
|
15
pom.xml
15
pom.xml
@@ -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.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Alfresco Community Repo Parent</name>
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
<dependency.alfresco-server-root.version>7.0.1</dependency.alfresco-server-root.version>
|
||||
<dependency.activiti-engine.version>5.23.0</dependency.activiti-engine.version>
|
||||
<dependency.activiti.version>5.23.0</dependency.activiti.version>
|
||||
<dependency.alfresco-transform-core.version>5.0.0</dependency.alfresco-transform-core.version>
|
||||
<dependency.alfresco-transform-service.version>4.0.0</dependency.alfresco-transform-service.version>
|
||||
<dependency.alfresco-transform-core.version>5.0.1</dependency.alfresco-transform-core.version>
|
||||
<dependency.alfresco-transform-service.version>4.0.1</dependency.alfresco-transform-service.version>
|
||||
<dependency.alfresco-greenmail.version>7.0</dependency.alfresco-greenmail.version>
|
||||
<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>
|
||||
@@ -88,7 +88,7 @@
|
||||
<dependency.jboss.logging.version>3.5.0.Final</dependency.jboss.logging.version>
|
||||
<dependency.camel.version>4.0.0</dependency.camel.version> <!-- when bumping this version, please keep track/sync with included netty.io dependencies -->
|
||||
<dependency.netty.version>4.1.96.Final</dependency.netty.version> <!-- must be in sync with camels transitive dependencies, e.g.: netty-common -->
|
||||
<dependency.activemq.version>5.18.2</dependency.activemq.version>
|
||||
<dependency.activemq.version>5.18.3</dependency.activemq.version>
|
||||
<dependency.apache-compress.version>1.24.0</dependency.apache-compress.version>
|
||||
<dependency.awaitility.version>4.2.0</dependency.awaitility.version>
|
||||
<dependency.swagger-ui.version>3.38.0</dependency.swagger-ui.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.5</tag>
|
||||
<tag>23.2.0.24</tag>
|
||||
</scm>
|
||||
|
||||
<distributionManagement>
|
||||
@@ -1036,10 +1036,9 @@
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<failOnMissing>true</failOnMissing>
|
||||
<excludedScopes>provided,test</excludedScopes>
|
||||
<excludedGroups>^(org\.alfresco|com\.alfresco|org\.activiti).*</excludedGroups>
|
||||
<failIfWarning>true</failIfWarning>
|
||||
<failOnMissing>true</failOnMissing>
|
||||
<includedLicenses>
|
||||
https://raw.githubusercontent.com/Alfresco/third-party-license-overrides/master/includedLicenses.txt
|
||||
</includedLicenses>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
@@ -45,7 +45,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.santuario</groupId>
|
||||
<artifactId>xmlsec</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
<!-- newer version, see REPO-3133 -->
|
||||
<dependency>
|
||||
|
@@ -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;
|
||||
|
@@ -104,13 +104,9 @@ public class JacksonHelper implements InitializingBean
|
||||
JsonGenerator generator = objectMapper.getFactory().createGenerator(outStream, encoding);
|
||||
writer.writeContents(generator, objectMapper);
|
||||
}
|
||||
catch (JsonMappingException error)
|
||||
catch (JsonMappingException | JsonGenerationException error)
|
||||
{
|
||||
logger.error("Failed to write Json output", error);
|
||||
}
|
||||
catch (JsonGenerationException generror)
|
||||
{
|
||||
logger.error("Failed to write Json output", generror);
|
||||
throw new IOException("Failed to write Json output", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -262,13 +262,15 @@ public class ExecutionTests extends AbstractContextTest implements ResponseWrite
|
||||
{
|
||||
AbstractResourceWebScript executor = getExecutor();
|
||||
Map<String, String> templateVars = new HashMap();
|
||||
|
||||
WebScriptResponse response = mockResponse();
|
||||
templateVars.put("apiScope", "private");
|
||||
templateVars.put("apiVersion", "1");
|
||||
templateVars.put("apiName", "alfrescomock");
|
||||
templateVars.put(ResourceLocator.COLLECTION_RESOURCE, "sheep");
|
||||
executor.execute(ApiAssistant.determineApi(templateVars), mockRequest(templateVars,new HashMap<String, List<String>>(1)), mock(WebScriptResponse.class));
|
||||
executor.execute(ApiAssistant.determineApi(templateVars), mockRequest(templateVars,new HashMap<String, List<String>>(1)), response);
|
||||
|
||||
WebScriptResponse response = mockResponse();
|
||||
response = mockResponse();
|
||||
templateVars.put(ResourceLocator.COLLECTION_RESOURCE, "bad");
|
||||
executor.execute(api, mockRequest(templateVars,new HashMap<String, List<String>>(1)), response);
|
||||
//throws a runtime exception so INTERNAL_SERVER_ERROR
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-community-repo</artifactId>
|
||||
<version>23.2.0.5</version>
|
||||
<version>23.2.0.24</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
|
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<hazelcast xmlns="http://www.hazelcast.com/schema/config"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config
|
||||
http://www.hazelcast.com/schema/config/hazelcast-config-5.3.xsd">
|
||||
|
||||
<cluster-name>Replace this with a secure value</cluster-name>
|
||||
<management-center data-access-enabled="${alfresco.hazelcast.mancenter.enabled}">
|
||||
<trusted-interfaces>
|
||||
<interface>${alfresco.hazelcast.mancenter.url}</interface>
|
||||
</trusted-interfaces>
|
||||
</management-center>
|
||||
<!-- Ephemeral lock store map definition -->
|
||||
<map name="lockStore">
|
||||
<backup-count>1</backup-count>
|
||||
<!-- No overall size limit, since this would result in ephemeral locks being evicted. -->
|
||||
<merge-policy>com.hazelcast.spi.merge.PutIfAbsentMergePolicy</merge-policy>
|
||||
<eviction eviction-policy="NONE" max-size-policy="PER_NODE" size="0"/>
|
||||
<!-- TTL here must match LockServiceImpl.MAX_EPHEMERAL_LOCK_SECONDS -->
|
||||
<time-to-live-seconds>172800</time-to-live-seconds>
|
||||
<max-idle-seconds>0</max-idle-seconds>
|
||||
</map>
|
||||
<!-- CACHES DEFINITION (DO NOT REMOVE THIS PLACEHOLDER) -->
|
||||
</hazelcast>
|
21
repository/scripts/hazelcast-init/ci-caches.properties
Normal file
21
repository/scripts/hazelcast-init/ci-caches.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# These caches definitions are automatically adapted to XML configuration
|
||||
# and used for integration test purposes. Be mindful when changing these!
|
||||
|
||||
# cache to test max items setting
|
||||
cache.maxItemsCache.maxItems=1000
|
||||
cache.maxItemsCache.timeToLiveSeconds=0
|
||||
cache.maxItemsCache.maxIdleSeconds=0
|
||||
cache.maxItemsCache.cluster.type=fully-distributed
|
||||
cache.maxItemsCache.backup-count=1
|
||||
cache.maxItemsCache.eviction-policy=LRU
|
||||
cache.maxItemsCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
|
||||
cache.maxItemsCache.readBackupData=false
|
||||
|
||||
# cache to test time to live setting
|
||||
cache.ttlCache.timeToLiveSeconds=1
|
||||
cache.ttlCache.maxIdleSeconds=0
|
||||
cache.ttlCache.cluster.type=fully-distributed
|
||||
cache.ttlCache.backup-count=1
|
||||
cache.ttlCache.eviction-policy=LRU
|
||||
cache.ttlCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
|
||||
cache.ttlCache.readBackupData=false
|
141
repository/scripts/hazelcast-init/generate-hazelcast-config.py
Executable file
141
repository/scripts/hazelcast-init/generate-hazelcast-config.py
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
"""Python script to convert Hazelcast cache definitions from Java properties to XML"""
|
||||
__author__="Domenico Sibilio"
|
||||
|
||||
import collections
|
||||
import configparser
|
||||
import xml.etree.ElementTree as ET
|
||||
from xml.dom import minidom
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
|
||||
ROOT_PATH = Path(__file__).parent
|
||||
CONFIG_PATH = ROOT_PATH / 'ci-caches.properties'
|
||||
OUTPUT_PATH = ROOT_PATH / 'alfresco-hazelcast-config.xml'
|
||||
TEMPLATE_PATH = ROOT_PATH / 'alfresco-hazelcast-template.xml'
|
||||
TEMPLATE_PLACEHOLDER = '<!-- CACHES DEFINITION (DO NOT REMOVE THIS PLACEHOLDER) -->'
|
||||
PROPS_TO_XML = {
|
||||
# time-to-live-seconds with value = x
|
||||
'timeToLiveSeconds': 'time-to-live-seconds',
|
||||
# max-idle-seconds with value = x
|
||||
'maxIdleSeconds': 'max-idle-seconds',
|
||||
# backup-count with value = x
|
||||
'backup-count': 'backup-count',
|
||||
# read-backup-data with value = x
|
||||
'readBackupData': 'read-backup-data',
|
||||
# merge-policy with value = x
|
||||
'merge-policy': 'merge-policy',
|
||||
# eviction with eviction-policy=x and max-size-policy=PER_NODE and size=${maxItems}
|
||||
'eviction-policy': 'eviction',
|
||||
# near-cache.eviction max-size-policy=ENTRY_COUNT, eviction-policy=LRU and size = x
|
||||
'nearCache.maxSize': 'size',
|
||||
# near-cache.max-idle-seconds with value = x
|
||||
'nearCache.maxIdleSeconds': 'max-idle-seconds',
|
||||
# near-cache.time-to-live-seconds with value = x
|
||||
'nearCache.timeToLiveSeconds': 'time-to-live-seconds',
|
||||
}
|
||||
|
||||
|
||||
def get_prop(prop_key: str):
|
||||
# shortcut to get the property within the default section
|
||||
return config.get('default', prop_key)
|
||||
|
||||
|
||||
def get_cache_name(sections: List[str]):
|
||||
# get the cache name given the full property string split by '.'
|
||||
return '.'.join(sections[0:get_cache_name_index(sections)+1])
|
||||
|
||||
|
||||
def get_cache_name_index(sections: List[str]):
|
||||
# returns the index where the cache name ends
|
||||
# given the full property string split by '.'
|
||||
for i, e in enumerate(sections):
|
||||
if e.endswith('Cache'):
|
||||
return i
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
def get_key(sections: List[str]):
|
||||
# get the property key name given the full property string split by '.'
|
||||
cn_index = get_cache_name_index(sections)
|
||||
key_sections = sections[cn_index+1::]
|
||||
return '.'.join(key_sections)
|
||||
|
||||
|
||||
def prettify(xml_string: str):
|
||||
# format and indent an xml string
|
||||
prettified_xml = minidom.parseString(xml_string).toprettyxml(indent=' ')
|
||||
return '\n'.join([line for line in prettified_xml.splitlines() if line.strip()])
|
||||
|
||||
|
||||
# entrypoint
|
||||
parser = argparse.ArgumentParser(description='A Python script to generate XML Hazelcast cache definitions starting from Java properties.')
|
||||
parser.add_argument('-s', '--source', default=CONFIG_PATH, help='path to the Java properties file to convert')
|
||||
args = parser.parse_args()
|
||||
|
||||
source_path = args.source
|
||||
|
||||
# add dummy section to properties
|
||||
with open(source_path, 'r') as f:
|
||||
cache_props = '[default]\n' + f.read()
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
# preserve property case
|
||||
config.optionxform = str
|
||||
# parse config file
|
||||
config.read_string(cache_props)
|
||||
|
||||
# group properties by cache name
|
||||
props_by_cache = collections.defaultdict(list)
|
||||
for item in config.items('default'):
|
||||
sections = item[0].split('.')
|
||||
if sections[0] == 'cache':
|
||||
cache_name = get_cache_name(sections)
|
||||
key = get_key(sections)
|
||||
value = item[1]
|
||||
props_by_cache[cache_name].append((key, value))
|
||||
|
||||
# read template file
|
||||
with open(TEMPLATE_PATH, 'r') as input:
|
||||
template = input.read()
|
||||
|
||||
# perform template substitutions to apply the caches.properties configuration
|
||||
map_configs = ''
|
||||
for cache, props in props_by_cache.items():
|
||||
props = dict(props)
|
||||
if(props.get('cluster.type') == 'fully-distributed'):
|
||||
map = ET.Element('map', name=cache)
|
||||
near_cache = None
|
||||
for prop, value in props.items():
|
||||
xml_prop = PROPS_TO_XML.get(prop)
|
||||
# handle eviction configuration
|
||||
if prop == 'eviction-policy':
|
||||
ET.SubElement(map, xml_prop,
|
||||
{'eviction-policy': value,
|
||||
'max-size-policy': 'PER_NODE',
|
||||
'size': props.get('maxItems') if props.get('maxItems') else '0'})
|
||||
# handle near-cache configuration
|
||||
elif prop.startswith('nearCache'):
|
||||
if near_cache is None:
|
||||
near_cache = ET.SubElement(map, 'near-cache')
|
||||
if prop.split('.')[1] == 'maxSize':
|
||||
ET.SubElement(near_cache, 'eviction',
|
||||
{'max-size-policy': 'ENTRY_COUNT',
|
||||
'eviction-policy': 'LRU',
|
||||
xml_prop: value})
|
||||
else:
|
||||
ET.SubElement(near_cache, xml_prop).text = value
|
||||
# handle basic map configuration
|
||||
elif xml_prop:
|
||||
ET.SubElement(map, xml_prop).text = value
|
||||
ET.SubElement(map, 'per-entry-stats-enabled').text = 'true'
|
||||
map_configs += minidom.parseString(ET.tostring(map)).childNodes[0].toprettyxml(indent=' ')
|
||||
|
||||
template = template.replace(TEMPLATE_PLACEHOLDER, map_configs)
|
||||
|
||||
# produce actual Hazelcast config file
|
||||
with open(OUTPUT_PATH, 'w') as output:
|
||||
output.write(prettify(template))
|
||||
print(f"Generated XML Hazelcast config: {OUTPUT_PATH}")
|
@@ -1,28 +1,28 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2016 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2016 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.email.server;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -199,26 +199,26 @@ public class AliasableAspect implements NodeServicePolicies.OnAddAspectPolicy,
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
|
||||
* @exception AlfrescoRuntimeException Throws if the <b>alias</b> property is duplicated.
|
||||
*/
|
||||
public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
|
||||
* @exception AlfrescoRuntimeException Throws if the <b>alias</b> property is duplicated.
|
||||
*/
|
||||
public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName)
|
||||
{
|
||||
Object alias = nodeService.getProperty(nodeRef, EmailServerModel.PROP_ALIAS);
|
||||
if (alias != null)
|
||||
{
|
||||
addAlias(nodeRef, alias.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy#onUpdateProperties(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, java.util.Map)
|
||||
* @exception AlfrescoRuntimeException Throws if the <b>alias</b> property is duplicated.
|
||||
*/
|
||||
public void onUpdateProperties(NodeRef nodeRef, Map<QName, Serializable> before, Map<QName, Serializable> after)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy#onUpdateProperties(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, java.util.Map)
|
||||
* @exception AlfrescoRuntimeException Throws if the <b>alias</b> property is duplicated.
|
||||
*/
|
||||
public void onUpdateProperties(NodeRef nodeRef, Map<QName, Serializable> before, Map<QName, Serializable> after)
|
||||
{
|
||||
String oldAlias = (String)before.get(EmailServerModel.PROP_ALIAS);
|
||||
String newAlias = (String)after.get(EmailServerModel.PROP_ALIAS);
|
||||
|
@@ -1614,6 +1614,10 @@ public abstract class AbstractPropertyValueDAOImpl implements PropertyValueDAO
|
||||
{
|
||||
Map<Serializable, Serializable> map = (Map<Serializable, Serializable>) container;
|
||||
Serializable mapKey = getPropertyValueById(keyPropId).getSecond();
|
||||
if(mapKey == null)
|
||||
{
|
||||
logger.error("Found null key for id " + keyPropId + " with value " + value);
|
||||
}
|
||||
map.put(mapKey, value);
|
||||
}
|
||||
else if (container instanceof Collection<?>)
|
||||
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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)
|
||||
{
|
||||
|
@@ -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) &&
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
{
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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))));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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. -->
|
||||
|
@@ -686,7 +686,6 @@ cache.hbClusterUsageCache.backup-count=1
|
||||
cache.hbClusterUsageCache.eviction-policy=NONE
|
||||
cache.hbClusterUsageCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
|
||||
cache.hbClusterUsageCache.readBackupData=false
|
||||
q
|
||||
|
||||
#
|
||||
# Query accelerator cluster cache
|
||||
|
@@ -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" />
|
||||
|
@@ -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" />
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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());
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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("");
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -34,7 +34,7 @@ services:
|
||||
ports:
|
||||
- "3307:3306"
|
||||
activemq:
|
||||
image: alfresco/alfresco-activemq:5.18.2-jre17-rockylinux8
|
||||
image: alfresco/alfresco-activemq:5.18.3-jre17-rockylinux8
|
||||
ports:
|
||||
- "5672:5672" # AMQP
|
||||
- "61616:61616" # OpenWire
|
@@ -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,8 +19,8 @@ services:
|
||||
ports:
|
||||
- "5433:5432"
|
||||
activemq:
|
||||
profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio"]
|
||||
image: alfresco/alfresco-activemq:5.18.2-jre17-rockylinux8
|
||||
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
|
||||
- "61616:61616" # OpenWire
|
||||
@@ -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
|
@@ -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 =========================="
|
Reference in New Issue
Block a user