diff --git a/.github/SECURITY.md b/.github/SECURITY.md index cde7527b6c0..395d132edda 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,15 +6,16 @@ We appreciate your efforts to responsibly disclose your findings, and will make ## Supported versions -Security updates will typically only be applied to the latest release (at least until **Janssen** reaches first stable major version). +Security updates will typically only be applied to the latest release. -| Version | Supported | -| -------- | ------------------ | -| >=0.1 | :white_check_mark: | +| Version | Supported | +|---------|--------------------| +| <1.0.0 | :x: | +| >=1.0.0 | :white_check_mark: | ## Reporting a vulnerability -To report a security issue, send an email to [security@jans.io](mailto:security@jans.io?subject=SECURITY) +To report a security issue email [security@jans.io](mailto:security@jans.io?subject=SECURITY) The **Janssen** team will send a response indicating the next steps in handling your report. After the initial reply to your report, the team will keep you informed of the progress towards a fix and full announcement, diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33fe6fac22..97049410915 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,10 +26,6 @@ updates: schedule: interval: daily - - package-ecosystem: pip - directory: /demos/jans-tent - schedule: - interval: daily - package-ecosystem: docker directory: /docker-jans-all-in-one diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index d398da52eca..6a0dfef7cb1 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -57,7 +57,7 @@ jobs: egress-policy: audit - name: Install Cosign - uses: sigstore/cosign-installer@v3.5.0 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -91,9 +91,9 @@ jobs: if: steps.build_docker_image.outputs.build || github.event_name == 'tags' run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update #- uses: actions/delete-package-versions@v5 @@ -165,19 +165,19 @@ jobs: fi # UPDATE BUILD DATES INSIDE THE DOCKERFILE BEFORE BUILDING THE DEV IMAGES TRIGGERED BY JENKINS - - name: Setup Python 3.7 + - name: Setup Python 3.10 if: github.event_name == 'workflow_dispatch' && ${{ matrix.docker-images }} != 'loadtesting-jmeter' uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Install Python dependencies if: github.event_name == 'workflow_dispatch' && ${{ matrix.docker-images }} != 'loadtesting-jmeter' run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update sudo apt-get install jq diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7f62905eff6..28e9ce016b9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -207,6 +207,13 @@ jobs: git add index.yaml && git update-index --refresh cd .. # END move generated chart from a previous step + + # copy search from nightly to all other versions. This is to ensure that the search index is available for all versions + for folder in v*/; do + cp -r nightly/search "$folder" + git add $folder/search && git update-index --refresh + done + # END copy search from nightly to all other versions echo "Replacing release number markers with actual release number" cd ${LATEST} diff --git a/.github/workflows/build-nightly-build.yml b/.github/workflows/build-nightly-build.yml index 92559bedfbd..83255433c8b 100644 --- a/.github/workflows/build-nightly-build.yml +++ b/.github/workflows/build-nightly-build.yml @@ -2,6 +2,11 @@ name: Activate Nightly Build on: workflow_dispatch: + inputs: + branch: + description: 'The branch to build night release from' + required: false + default: 'main' schedule: - cron: "0 23 * * *" permissions: @@ -37,4 +42,8 @@ jobs: gh release delete ${NIGHTLY_VERSION} --cleanup-tag --yes || echo "v${NIGHTLY_VERSION}" does not exist gh release delete ${NIGHTLY_VERSION} --cleanup-tag --yes || echo "v${NIGHTLY_VERSION}" does not exist git push --delete origin ${NIGHTLY_VERSION} || echo "v${NIGHTLY_VERSION}" does not exist - gh release create ${NIGHTLY_VERSION} --generate-notes --prerelease --title "${NIGHTLY_VERSION}" \ No newline at end of file + TARGET_BRANCH=${{ github.event.inputs.branch }} + if [ -z "$TARGET_BRANCH" ]; then + TARGET_BRANCH="main" + fi + gh release create ${NIGHTLY_VERSION} --generate-notes --prerelease --title "${NIGHTLY_VERSION}" --target "${TARGET_BRANCH}" \ No newline at end of file diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 6d5687ec882..c9b886d4c24 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -5,6 +5,8 @@ on: tags: - 'v**' - 'nightly' +permissions: + contents: read jobs: publish_binary_packages: if: github.repository == 'JanssenProject/jans' @@ -196,7 +198,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y python3 build-essential ca-certificates dbus systemd iproute2 gpg python3-pip python3-dev libpq-dev gcc - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" pip3 install shiv wheel setuptools echo "Building jans-linux-setup package" sudo chown -R runner:docker /home/runner/work/jans/jans @@ -337,7 +339,7 @@ jobs: gh release upload $VER *.zip *.sha256sum --clobber build_cedarling_python: if: github.repository == 'JanssenProject/jans' - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 @@ -356,8 +358,8 @@ jobs: git_user_signingkey: true git_commit_gpgsign: true - - uses: actions/setup-python@v5 - - uses: PyO3/maturin-action@v1 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + - uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab # v1.45.0 with: working-directory: ${{ github.workspace }}/jans-cedarling/bindings/cedarling_python command: build @@ -373,9 +375,9 @@ jobs: TAG="0.0.0" fi cd ${{ github.workspace }}/jans-cedarling/target/wheels - sha256sum cedarling_python-"${TAG}"-cp311-cp311-manylinux_2_34_x86_64.whl > cedarling_python-"${TAG}"-cp311-cp311-manylinux_2_34_x86_64.whl.sha256sum - sha256sum cedarling_python-"${TAG}"-cp310-cp310-manylinux_2_34_x86_64.whl > cedarling_python-"${TAG}"-cp310-cp310-manylinux_2_34_x86_64.whl.sha256sum - gpg --armor --detach-sign cedarling_python-"${TAG}"-cp311-cp311-manylinux_2_34_x86_64.whl || echo "Failed to sign" - gpg --armor --detach-sign cedarling_python-"${TAG}"-cp310-cp310-manylinux_2_34_x86_64.whl || echo "Failed to sign" + sha256sum cedarling_python-"${TAG}"-cp311-cp311-manylinux_2_31_x86_64.whl > cedarling_python-"${TAG}"-cp311-cp311-manylinux_2_31_x86_64.whl.sha256sum + sha256sum cedarling_python-"${TAG}"-cp310-cp310-manylinux_2_31_x86_64.whl > cedarling_python-"${TAG}"-cp310-cp310-manylinux_2_31_x86_64.whl.sha256sum + gpg --armor --detach-sign cedarling_python-"${TAG}"-cp311-cp311-manylinux_2_31_x86_64.whl || echo "Failed to sign" + gpg --armor --detach-sign cedarling_python-"${TAG}"-cp310-cp310-manylinux_2_31_x86_64.whl || echo "Failed to sign" echo "${{ secrets.MOAUTO_WORKFLOW_TOKEN }}" | gh auth login --with-token gh release upload "${VERSION}" *.whl *.sha256sum *.asc \ No newline at end of file diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0142d4bc5b4..59fe986c6c5 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -40,35 +40,50 @@ on: concurrency: group: run-once cancel-in-progress: false +permissions: + contents: read jobs: cleanup: - if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' runs-on: ubuntu-20.04 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + packages: write steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit - name: Get version ID for 0.0.0-nightly - if: github.event_name == 'push' && github.ref == 'refs/heads/main' id: get_version_id run: | - services=$(gh api -H "Accept: application/vnd.github+json" \ - /orgs/JanssenProject/packages?package_type=maven \ - | jq -r '.[].name') - for service in "${services}"; do - version_id=$(gh api -H "Accept: application/vnd.github+json" \ - /orgs/JanssenProject/packages/maven/io.jans.${service}/versions \ - | jq -r '.[] | select(.name == "0.0.0-nightly") | .id') - echo "version_id=$version_id" >> $GITHUB_ENV - gh api --method DELETE \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /orgs/JanssenProject/packages/maven/io.jans."${service}"/versions/"${version_id}" + page=1 + services="" + while true; do + response=$(gh api -H "Accept: application/vnd.github+json" \ + /orgs/JanssenProject/packages?package_type=maven\&per_page=100\&page=$page) + names=$(echo "$response" | jq -r '.[].name') + if [ -z "$names" ]; then + break + fi + services="$services $names" + page=$((page + 1)) done - + + services=$(echo "$services" | tr '\n' ' ' | sed 's/ *$//') + echo "Services: $services" + for service in $services; do + echo "Checking $service" + version_id=$(gh api -H "Accept: application/vnd.github+json" \ + /orgs/JanssenProject/packages/maven/"${service}"/versions \ + | jq -r '.[] | select(.name == "0.0.0-nightly") | .id') + echo "version_id=$version_id" >> $GITHUB_ENV + gh api --method DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /orgs/JanssenProject/packages/maven/"${service}"/versions/"${version_id}" || echo "Failed to delete $service" + done prep-matrix: needs: cleanup @@ -126,18 +141,18 @@ jobs: with: egress-policy: audit - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.inputs.branch }} - name: Set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' - name: Set up Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' @@ -146,7 +161,8 @@ jobs: - name: Build ${{ matrix.service }} run: | if [ "${{ github.event_name }}" == "pull_request" ]; then - mvn -f ${{ matrix.service }}/pom.xml -Dcfg=${{ env.PROFILE_NAME }} -Dmaven.test.skip=${{ matrix.maven_skip_tests }} clean install -Drevision=${{ github.head_ref }}-nightly + revision=${{ github.sha }}-nightly + mvn -f ${{ matrix.service }}/pom.xml -Dcfg=${{ env.PROFILE_NAME }} -Dmaven.test.skip=${{ matrix.maven_skip_tests }} clean install -Drevision=$revision else mvn -f ${{ matrix.service }}/pom.xml -Dcfg=${{ env.PROFILE_NAME }} -Dmaven.test.skip=${{ matrix.maven_skip_tests }} clean install fi @@ -159,7 +175,7 @@ jobs: - name: Archive results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: build-results path: ${{ matrix.service }}/target @@ -170,7 +186,9 @@ jobs: run-tests: if: github.event_name == 'push' || github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.event.inputs.project == 'jans-bom, jans-orm, jans-core, jans-lock/lock-server, agama, jans-auth-server, jans-link, jans-fido2, jans-scim, jans-keycloak-link, jans-config-api, jans-keycloak-integration, jans-casa') - permissions: write-all + permissions: + contents: read + packages: write needs: cleanup runs-on: ubuntu-20.04 env: @@ -198,18 +216,18 @@ jobs: with: egress-policy: audit - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.inputs.branch }} - name: Set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' - name: Set up Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' @@ -276,13 +294,13 @@ jobs: ls /tmp/reports/ - name: Upload Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: ${{ matrix.persistence }}-test-results path: /tmp/reports - name: Publish Test Report ${{ matrix.persistence }} - uses: starburstdata/action-testng-report@v1 + uses: starburstdata/action-testng-report@f245422953fb97ec5075d07782a1b596124b7cc4 # v1.0.5 with: report_paths: /tmp/reports/${{ matrix.persistence }}*.xml github_token: ${{ github.token }} diff --git a/.github/workflows/lint-flak8.yml b/.github/workflows/lint-flak8.yml index 63dadc76b64..e8eff713621 100644 --- a/.github/workflows/lint-flak8.yml +++ b/.github/workflows/lint-flak8.yml @@ -4,14 +4,16 @@ on: branches: - main paths: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - - 'demos/jans-tent/**' + - 'jans-pycloudlib/**' + - 'jans-cli-tui/**' + - 'jans-linux-setup/**' pull_request: branches: - main paths: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - - 'demos/jans-tent/**' + - 'jans-pycloudlib/**' + - 'jans-cli-tui/**' + - 'jans-linux-setup/**' permissions: contents: read @@ -23,8 +25,11 @@ jobs: #max-parallel: 1 fail-fast: false matrix: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - python-projects: ["demos/jans-tent"] + python-projects: [ + "jans-pycloudlib", + "jans-cli-tui", + "jans-linux-setup" + ] steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 diff --git a/.github/workflows/ops-docs.yml b/.github/workflows/ops-docs.yml index 07c61013b3f..34311ad3e04 100644 --- a/.github/workflows/ops-docs.yml +++ b/.github/workflows/ops-docs.yml @@ -71,10 +71,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Python 3.7 + - name: Set up Python 3.10 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Auto-merge inhouse doc prs run: | diff --git a/.github/workflows/ops-label-pr-issues.yml b/.github/workflows/ops-label-pr-issues.yml index 73528021229..bf0b1cd5f83 100644 --- a/.github/workflows/ops-label-pr-issues.yml +++ b/.github/workflows/ops-label-pr-issues.yml @@ -31,17 +31,17 @@ jobs: - name: check out code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Setup Python 3.7 + - name: Set up Python 3.10 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Install dependencies run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update sudo apt-get install jq curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ca0b9fcd917..d7df2a159cb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,12 +10,17 @@ jobs: strategy: fail-fast: false steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f # v4.1.3 id: release-please with: release-type: simple diff --git a/.github/workflows/sanitary-github-cache.yml b/.github/workflows/sanitary-github-cache.yml index b2bfb70f57d..e1dd3fa9676 100644 --- a/.github/workflows/sanitary-github-cache.yml +++ b/.github/workflows/sanitary-github-cache.yml @@ -4,7 +4,8 @@ on: types: - closed workflow_dispatch: - +permissions: + contents: read jobs: cleanup: runs-on: ubuntu-latest diff --git a/.github/workflows/sanitary-workflow-runs.yml b/.github/workflows/sanitary-workflow-runs.yml index fd3137becc7..c8115cc62a8 100644 --- a/.github/workflows/sanitary-workflow-runs.yml +++ b/.github/workflows/sanitary-workflow-runs.yml @@ -3,6 +3,8 @@ on: schedule: - cron: '0 0 */2 * *' workflow_dispatch: +permissions: + contents: read jobs: del_runs: runs-on: ubuntu-latest diff --git a/.github/workflows/scan-sonar.yml b/.github/workflows/scan-sonar.yml index 66284080304..69cac4cfc36 100644 --- a/.github/workflows/scan-sonar.yml +++ b/.github/workflows/scan-sonar.yml @@ -55,7 +55,8 @@ on: - '!**.txt' workflow_dispatch: - +permissions: + contents: read jobs: sonar-scan: name: sonar scan @@ -82,7 +83,9 @@ jobs: jans-linux-setup jans-cli-tui jans-pycloudlib - + permissions: + contents: read + pull-requests: read steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 diff --git a/.github/workflows/test-cedarling.yml b/.github/workflows/test-cedarling.yml index 6647eba00c0..0d01fa97dee 100644 --- a/.github/workflows/test-cedarling.yml +++ b/.github/workflows/test-cedarling.yml @@ -19,15 +19,39 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable - name: Run Tests + working-directory: jans-cedarling run: | - cd ./jans-cedarling cargo test --workspace - - name: Run Clippy + - name: Run Clippy on native target + working-directory: jans-cedarling run: | - cd ./jans-cedarling cargo clippy -- -Dwarnings + wasm_tests: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Install Rust + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable + - name: Install WASM dependencies + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Run WASM tests using chrome + working-directory: jans-cedarling/bindings/cedarling_wasm + run: | + wasm-pack test --headless --chrome + - name: Run WASM tests using firefox + working-directory: jans-cedarling/bindings/cedarling_wasm + run: | + wasm-pack test --headless --firefox + - name: Run Clippy on WASM target + working-directory: jans-cedarling + run: | + cargo clippy --target wasm32-unknown-unknown -- -Dwarnings python_tests: runs-on: ubuntu-latest strategy: @@ -45,9 +69,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" python3 -m pip install tox - name: Test with pytest + working-directory: jans-cedarling/bindings/cedarling_python run: | - cd ./jans-cedarling/bindings/cedarling_python tox diff --git a/.github/workflows/test-jans-pycloudlib.yml b/.github/workflows/test-jans-pycloudlib.yml index 3603b64f320..b673adb249f 100644 --- a/.github/workflows/test-jans-pycloudlib.yml +++ b/.github/workflows/test-jans-pycloudlib.yml @@ -41,7 +41,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" python3 -m pip install tox - name: Test with pytest run: | diff --git a/README.md b/README.md index 44b01972fad..f44da2bbc3b 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,10 @@ commercial distribution of Janssen Project Components called **Social**: [Linkedin](https://www.linkedin.com/company/janssen-project) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/janssen-auth-server)](https://artifacthub.io/packages/search?repo=janssen-auth-server) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4353/badge)](https://bestpractices.coreinfrastructure.org/projects/4353) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/JanssenProject/jans/badge)](https://scorecard.dev/viewer/?uri=github.com/JanssenProject/jans) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/4353/badge)](https://www.bestpractices.dev/projects/4353) [![Hex.pm](https://img.shields.io/hexpm/l/plug)](./LICENSE) -[![GitHub contributors](https://img.shields.io/github/contributors/janssenproject/jans)](#users-and-community) +[![GitHub contributors](https://img.shields.io/github/contributors/janssenproject/jans)](#community) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) ---- @@ -45,26 +46,27 @@ commercial distribution of Janssen Project Components called | Component | Description | Lifecycle Stage | |:---------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------| -| **[Jans Auth Server](jans-auth-server)** | A very complete Java OAuth Authorization Server and a [certified](https://openid.net/certification/) OpenID Connect Provider. It's the upstream open-source core of [Gluu Flex](https://gluu.org/flex). | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | +| **[Jans Auth Server](jans-auth-server)** | A complete OAuth Authorization Server and a [certified](https://openid.net/certification/) OpenID Connect Provider written in Java. It's the upstream open-source core of [Gluu Flex](https://gluu.org/flex). | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | | **[Agama](agama)** | Agama offers an interoperable way to design authentication flows, coded in a DSL purpose-built for writing identity journeys. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | | **[Jans FIDO](jans-fido2)** | Enables end-users to enroll and authenticate with passkeys and other FIDO authenticators. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | | **[Jans SCIM](jans-scim)** | [SCIM](http://www.simplecloud.info/) JSON/REST [API](https://docs.jans.io/head/admin/reference/openapi/) for user management, including associated FIDO devices. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | -| **[Jans Config API](jans-config-api)** | RESTful control plane for all Janssen components. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | -| **[Text UI ("TUI")](jans-cli-tui)** | Command line and interactive configuration tools to help you correctly call the Config API. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | +| **[Jans Config API](jans-config-api)** | RESTful APIs manage configuration for all Janssen components. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | +| **[Text UI ("TUI")](jans-cli-tui)** | User interface accessible from command line. TUI is text-based interactive configuration tool that leverages config-API to configure Janssen Server modules | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | +| **[Jans CLI](jans-cli-tui)** | Command line configuration tools to help you correctly call the Config API. | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | | **[Jans Casa](jans-casa)** | Jans Casa is a self-service web portal for end-users to manage authentication and authorization preferences for their account in the Janssen Server | ![Graduated](https://img.shields.io/badge/Graduated-%2301ba77) | | **[Jans KC](jans-keycloak-integration)** | provides an array of out of the box IAM services in a single lightweight container image. It's handy for many workforce requirements like SAML. The Janssen authenticator module (SPI) simplifies SSO across Janssen and Keycloak websites. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans LDAP Link](jans-link)** | a group of components that provide synchronization services to update the Janssen User Store from an external authoritative LDAP data sources | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Keycloak Link](jans-keycloak-link)** | a group of components that provide synchronization services to update the Janssen User Store from an external authoritative Keycloak data sources | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | -| **[Jans Cedarling](jans-cedaring)** | Cedarling binding for Python and WASM. In simple terms, the Cedarling returns the answer: should the application allow this action on this resource given these JWT tokens.. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | +| **[Jans Cedarling](jans-cedarling)** | Cedarling is an embeddable stateful Policy Decision Point for authorization requests. In simple terms, the Cedarling returns the answer: should the application allow this action on this resource given these JWT tokens. It is written in Rust with bindings to WASM, iOS, Android, and Python. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Lock](jans-lock)** | An enterprise authorization solution featuring the Cedarling, a stateless PDP and the Lock Server which centralizes audit logs and configuration. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Tarp](demos/jans-tarp)** | An OpenID Connect RP test website that runs as a browser plugin in Chrome or Firefox. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Chip](demos/jans-chip)** | Sample iOS and Android mobile applications that implement the full OAuth and FIDO security stack for app integrity, client constrained access tokens, and user presence. | ![Demo](https://img.shields.io/badge/Demo-%23368af7) | -| **[Jans Tent](demos/jans-tent)** | A test Relying Party ("RP") built using Python and Flask. Enables you to send different requests by quickly modifying just one configuration file. | ![Demo](https://img.shields.io/badge/Demo-%23368af7) | ## Installation You can install the Janssen federation stack in a Kubernetes cluster or as a -single VM. Check out the [Janssen Documentation](https://docs.jans.io/head/admin/install/) +single VM. Check out the +[Janssen Documentation](https://docs.jans.io/head/janssen-server/install/) for details. ## Community diff --git a/agama/misc/json_template.ftlh b/agama/misc/json_template.ftl similarity index 100% rename from agama/misc/json_template.ftlh rename to agama/misc/json_template.ftl diff --git a/charts/janssen-all-in-one/README.md b/charts/janssen-all-in-one/README.md index b02f1bcf0ef..e331b0a1b71 100644 --- a/charts/janssen-all-in-one/README.md +++ b/charts/janssen-all-in-one/README.md @@ -134,6 +134,8 @@ Kubernetes: `>=v1.22.0-0` | cnConfiguratorCustomSchema | object | `{"secretName":""}` | Use custom configuration schema in existing secrets. Note, the secrets has to contain the key configuration.json or any basename as specified in cnConfiguratorConfigurationFile. | | cnConfiguratorCustomSchema.secretName | string | `""` | The name of the secrets used for storing custom configuration schema. | | cnConfiguratorDumpFile | string | `"/etc/jans/conf/configuration.out.json"` | Path to dumped configuration schema file | +| cnConfiguratorKey | string | `""` | Key to encrypt/decrypt configuration schema file using AES-256 CBC mode. Set the value to empty string to disable encryption/decryption, or 32 alphanumeric characters to enable it. | +| cnConfiguratorKeyFile | string | `"/etc/jans/conf/configuration.key"` | Path to file contains key to encrypt/decrypt configuration schema file. | | cnDocumentStoreType | string | `"DB"` | Document store type to use for shibboleth files DB. | | cnGoogleApplicationCredentials | string | `"/etc/jans/conf/google-credentials.json"` | Base64 encoded service account. The sa must have roles/secretmanager.admin to use Google secrets. Leave as this is a sensible default. | | cnPersistenceType | string | `"sql"` | Persistence backend to run Janssen with hybrid|sql. | diff --git a/charts/janssen-all-in-one/templates/_helpers.tpl b/charts/janssen-all-in-one/templates/_helpers.tpl index d30466ae7f9..135d37cbb03 100644 --- a/charts/janssen-all-in-one/templates/_helpers.tpl +++ b/charts/janssen-all-in-one/templates/_helpers.tpl @@ -73,10 +73,10 @@ Create optional scopes list {{- define "janssen-all-in-one.optionalScopes"}} {{ $newList := list }} {{- if eq .Values.configmap.cnCacheType "REDIS" }} -{{ $newList = append $newList ("redis" | quote ) }} +{{ $newList = append $newList "redis" }} {{- end}} {{ if eq .Values.cnPersistenceType "sql" }} -{{ $newList = append $newList ("sql" | quote) }} +{{ $newList = append $newList "sql" }} {{- end }} {{ toJson $newList }} {{- end }} @@ -178,12 +178,78 @@ Create configuration schema-related objects. {{- define "janssen-all-in-one.config.schema" -}} {{- $commonName := (printf "%s-configuration-file" .Release.Name) -}} {{- $secretName := .Values.cnConfiguratorCustomSchema.secretName | default $commonName -}} +{{- $keyName := (printf "%s-configuration-key-file" .Release.Name) -}} volumes: - name: {{ $commonName }} secret: secretName: {{ $secretName }} +{{- if .Values.cnConfiguratorKey }} + - name: {{ $keyName }} + secret: + secretName: {{ $keyName }} +{{- end }} volumeMounts: - name: {{ $commonName }} mountPath: {{ .Values.cnConfiguratorConfigurationFile }} subPath: {{ .Values.cnConfiguratorConfigurationFile | base }} -{{- end -}} +{{- if .Values.cnConfiguratorKey }} + - name: {{ $keyName }} + mountPath: {{ .Values.cnConfiguratorKeyFile }} + subPath: {{ .Values.cnConfiguratorKeyFile | base }} +{{- end }} +{{- end }} + +{{/* +Obfuscate configuration schema (only if configuration key is available) +*/}} +{{- define "janssen-all-in-one.config.prepareSchema" }} + +{{- $configmapSchema := dict }} +{{- $_ := set $configmapSchema "hostname" .Values.fqdn }} +{{- $_ := set $configmapSchema "country_code" .Values.countryCode }} +{{- $_ := set $configmapSchema "state" .Values.state }} +{{- $_ := set $configmapSchema "city" .Values.city }} +{{- $_ := set $configmapSchema "admin_email" .Values.email }} +{{- $_ := set $configmapSchema "orgName" .Values.orgName }} +{{- $_ := set $configmapSchema "auth_sig_keys" (index .Values "auth-server" "authSigKeys") }} +{{- $_ := set $configmapSchema "auth_enc_keys" (index .Values "auth-server" "authEncKeys") }} +{{- $_ := set $configmapSchema "optional_scopes" (include "janssen-all-in-one.optionalScopes" . | trim) }} +{{- if .Values.saml.enabled }} +{{- $_ := set $configmapSchema "kc_admin_username" .Values.configmap.kcAdminUsername }} +{{- end }} +{{- $_ := set $configmapSchema "init_keys_exp" (index .Values "auth-server-key-rotation" "initKeysLife") }} + +{{- $secretSchema := dict }} +{{- $_ := set $secretSchema "admin_password" .Values.adminPassword }} +{{- $_ := set $secretSchema "redis_password" .Values.redisPassword }} +{{- if or ( eq .Values.cnPersistenceType "sql" ) ( eq .Values.cnPersistenceType "hybrid" ) }} +{{- $_ := set $secretSchema "sql_password" .Values.configmap.cnSqldbUserPassword }} +{{- end }} +{{- if eq .Values.configSecretAdapter "vault" }} +{{- $_ := set $secretSchema "vault_role_id" .Values.configmap.cnVaultRoleId }} +{{- $_ := set $secretSchema "vault_secret_id" .Values.configmap.cnVaultSecretId }} +{{- end }} +{{- if or (eq .Values.configSecretAdapter "google") (eq .Values.configAdapterName "google") }} +{{- $_ := set $secretSchema "google_credentials" .Values.configmap.cnGoogleSecretManagerServiceAccount }} +{{- end }} +{{- if or (eq .Values.configAdapterName "aws") (eq .Values.configSecretAdapter "aws") }} +{{- $_ := set $secretSchema "aws_credentials" (include "config.aws-shared-credentials" . | b64enc) }} +{{- $_ := set $secretSchema "aws_config" (include "config.aws-config" . | b64enc) }} +{{- $_ := set $secretSchema "aws_replica_regions" (toJson .Values.configmap.cnAwsSecretsReplicaRegions | b64enc) }} +{{- end }} +{{- if .Values.saml.enabled }} +{{- $_ := set $secretSchema "kc_db_password" .Values.configmap.kcDbPassword }} +{{- $_ := set $secretSchema "kc_admin_password" .Values.configmap.kcAdminPassword }} +{{- end }} +{{- $_ := set $secretSchema "encoded_salt" .Values.salt }} + +{{- $schema := dict "_configmap" $configmapSchema "_secret" $secretSchema }} + +{{- if .Values.cnConfiguratorKey }} +{{- printf "%s" (encryptAES .Values.cnConfiguratorKey (toPrettyJson $schema)) }} +{{- else -}} +{{- toPrettyJson $schema }} +{{- end }} + +{{/* end of helpers */}} +{{- end }} diff --git a/charts/janssen-all-in-one/templates/secret.yaml b/charts/janssen-all-in-one/templates/secret.yaml index 9964d04a533..bcc2b62f50f 100644 --- a/charts/janssen-all-in-one/templates/secret.yaml +++ b/charts/janssen-all-in-one/templates/secret.yaml @@ -22,45 +22,33 @@ metadata: type: Opaque stringData: {{ .Values.cnConfiguratorConfigurationFile | base }}: |- - { - "_configmap": { - "hostname": {{ .Values.fqdn | quote }}, - "country_code": {{ .Values.countryCode | quote }}, - "state": {{ .Values.state | quote }}, - "city": {{ .Values.city | quote }}, - "admin_email": {{ .Values.email | quote }}, - "orgName": {{ .Values.orgName | quote }}, - "auth_sig_keys": {{ index .Values "auth-server" "authSigKeys" | quote }}, - "auth_enc_keys": {{ index .Values "auth-server" "authEncKeys" | quote }}, - "optional_scopes": {{ list (include "janssen-all-in-one.optionalScopes" . | fromJsonArray | join ",") | quote }}, - {{- if .Values.saml.enabled }} - "kc_admin_username": {{ .Values.configmap.kcAdminUsername | quote }}, - {{- end }} - "init_keys_exp": {{ index .Values "auth-server-key-rotation" "initKeysLife" }} - }, - "_secret": { - "admin_password": {{ .Values.adminPassword | quote }}, - "redis_password": {{ .Values.redisPassword | quote }}, - {{ if or ( eq .Values.cnPersistenceType "sql" ) ( eq .Values.cnPersistenceType "hybrid" ) }} - "sql_password": {{ .Values.configmap.cnSqldbUserPassword | quote }}, - {{- end }} - {{ if eq .Values.configSecretAdapter "vault" }} - "vault_role_id": {{ .Values.configmap.cnVaultRoleId | quote }}, - "vault_secret_id": {{ .Values.configmap.cnVaultSecretId | quote }}, - {{- end }} - {{ if or (eq .Values.configSecretAdapter "google") (eq .Values.configAdapterName "google") }} - "google_credentials": {{ .Values.configmap.cnGoogleSecretManagerServiceAccount | quote }}, - {{- end }} - {{ if or (eq .Values.configAdapterName "aws") (eq .Values.configSecretAdapter "aws") }} - "aws_credentials": {{ include "janssen-all-in-one.aws-shared-credentials" . | b64enc | quote }}, - "aws_config": {{ include "janssen-all-in-one.aws-config" . | b64enc | quote }}, - "aws_replica_regions": {{ .Values.configmap.cnAwsSecretsReplicaRegions | toJson | b64enc | quote }}, - {{- end }} - {{- if .Values.saml.enabled }} - "kc_db_password": {{ .Values.configmap.kcDbPassword | quote }}, - "kc_admin_password": {{ .Values.configmap.kcAdminPassword | quote }}, - {{- end }} - "encoded_salt": {{ .Values.salt | quote }} - } - } +{{ include "janssen-all-in-one.config.prepareSchema" . | indent 4 }} +{{- end }} + +--- + +{{- if .Values.cnConfiguratorKey -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-configuration-key-file + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Release.Name }}-{{ include "janssen-all-in-one.name" . }}-aio +{{ include "janssen-all-in-one.labels" . | indent 4 }} +{{- if .Values.additionalLabels }} +{{ toYaml .Values.additionalLabels | indent 4 }} +{{- end }} +{{- if or (.Values.additionalAnnotations) (.Values.customAnnotations.secret) }} + annotations: +{{- if .Values.additionalAnnotations }} +{{ toYaml .Values.additionalAnnotations | indent 4 }} +{{- end }} +{{- if .Values.customAnnotations.secret }} +{{ toYaml .Values.customAnnotations.secret | indent 4 }} +{{- end }} +{{- end }} +type: Opaque +data: + {{ .Values.cnConfiguratorKeyFile | base }}: {{ .Values.cnConfiguratorKey | b64enc }} {{- end }} diff --git a/charts/janssen-all-in-one/values.yaml b/charts/janssen-all-in-one/values.yaml index f446a4c50f4..616bd10f675 100644 --- a/charts/janssen-all-in-one/values.yaml +++ b/charts/janssen-all-in-one/values.yaml @@ -573,6 +573,10 @@ cnConfiguratorDumpFile: /etc/jans/conf/configuration.out.json cnConfiguratorCustomSchema: # -- The name of the secrets used for storing custom configuration schema. secretName: "" +# -- Key to encrypt/decrypt configuration schema file using AES-256 CBC mode. Set the value to empty string to disable encryption/decryption, or 32 alphanumeric characters to enable it. +cnConfiguratorKey: "" +# -- Path to file contains key to encrypt/decrypt configuration schema file. +cnConfiguratorKeyFile: /etc/jans/conf/configuration.key # ingress properties istio: diff --git a/charts/janssen/README.md b/charts/janssen/README.md index 56dace93960..4f015ece23e 100644 --- a/charts/janssen/README.md +++ b/charts/janssen/README.md @@ -265,7 +265,7 @@ Kubernetes: `>=v1.22.0-0` | fido2.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | fido2.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | fido2.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| global | object | `{"alb":{"ingress":false},"auth-server":{"appLoggers":{"auditStatsLogLevel":"INFO","auditStatsLogTarget":"FILE","authLogLevel":"INFO","authLogTarget":"STDOUT","enableStdoutLogPrefix":"true","httpLogLevel":"INFO","httpLogTarget":"FILE","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"authEncKeys":"RSA1_5 RSA-OAEP","authServerServiceName":"auth-server","authSigKeys":"RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512","cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"authServerAdditionalAnnotations":{},"authServerEnabled":true,"authServerLabels":{},"authzenAdditionalAnnotations":{},"authzenConfigEnabled":true,"authzenConfigLabels":{},"deviceCodeAdditionalAnnotations":{},"deviceCodeEnabled":true,"deviceCodeLabels":{},"firebaseMessagingAdditionalAnnotations":{},"firebaseMessagingEnabled":true,"firebaseMessagingLabels":{},"lockAdditionalAnnotations":{},"lockConfigAdditionalAnnotations":{},"lockConfigEnabled":false,"lockConfigLabels":{},"lockEnabled":false,"lockLabels":{},"openidAdditionalAnnotations":{},"openidConfigEnabled":true,"openidConfigLabels":{},"u2fAdditionalAnnotations":{},"u2fConfigEnabled":true,"u2fConfigLabels":{},"uma2AdditionalAnnotations":{},"uma2ConfigEnabled":true,"uma2ConfigLabels":{},"webdiscoveryAdditionalAnnotations":{},"webdiscoveryEnabled":true,"webdiscoveryLabels":{},"webfingerAdditionalAnnotations":{},"webfingerEnabled":true,"webfingerLabels":{}},"lockEnabled":false},"auth-server-key-rotation":{"customAnnotations":{"cronjob":{},"secret":{},"service":{}},"enabled":true,"initKeysLife":48},"awsStorageType":"io1","azureStorageAccountType":"Standard_LRS","azureStorageKind":"Managed","casa":{"appLoggers":{"casaLogLevel":"INFO","casaLogTarget":"STDOUT","enableStdoutLogPrefix":"true","timerLogLevel":"INFO","timerLogTarget":"FILE"},"casaServiceName":"casa","cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"casaAdditionalAnnotations":{},"casaEnabled":false,"casaLabels":{}}},"cloud":{"testEnviroment":false},"cnAwsConfigFile":"/etc/jans/conf/aws_config_file","cnAwsSecretsReplicaRegionsFile":"/etc/jans/conf/aws_secrets_replica_regions","cnAwsSharedCredentialsFile":"/etc/jans/conf/aws_shared_credential_file","cnConfiguratorConfigurationFile":"/etc/jans/conf/configuration.json","cnConfiguratorCustomSchema":{"secretName":""},"cnConfiguratorDumpFile":"/etc/jans/conf/configuration.out.json","cnDocumentStoreType":"DB","cnGoogleApplicationCredentials":"/etc/jans/conf/google-credentials.json","cnPersistenceType":"sql","cnPrometheusPort":"","cnSqlPasswordFile":"/etc/jans/conf/sql_password","config":{"customAnnotations":{"clusterRoleBinding":{},"configMap":{},"job":{},"role":{},"roleBinding":{},"secret":{},"service":{},"serviceAccount":{}},"enabled":true},"config-api":{"appLoggers":{"configApiLogLevel":"INFO","configApiLogTarget":"STDOUT","enableStdoutLogPrefix":"true","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","configApiServerServiceName":"config-api","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"configApiAdditionalAnnotations":{},"configApiEnabled":true,"configApiLabels":{}},"plugins":"fido2,scim,user-mgt"},"configAdapterName":"kubernetes","configSecretAdapter":"kubernetes","fido2":{"appLoggers":{"enableStdoutLogPrefix":"true","fido2LogLevel":"INFO","fido2LogTarget":"STDOUT","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"fido2ServiceName":"fido2","ingress":{"fido2AdditionalAnnotations":{},"fido2ConfigAdditionalAnnotations":{},"fido2ConfigEnabled":false,"fido2ConfigLabels":{},"fido2Enabled":false,"fido2Labels":{},"fido2WebauthnAdditionalAnnotations":{},"fido2WebauthnEnabled":false,"fido2WebauthnLabels":{}}},"fqdn":"demoexample.jans.io","gcePdStorageType":"pd-standard","isFqdnRegistered":false,"istio":{"additionalAnnotations":{},"additionalLabels":{},"enabled":false,"gateways":[],"ingress":false,"namespace":"istio-system"},"jobTtlSecondsAfterFinished":300,"kc-scheduler":{"enabled":false},"lbIp":"22.22.22.22","link":{"appLoggers":{"enableStdoutLogPrefix":"true","linkLogLevel":"INFO","linkLogTarget":"STDOUT","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"service":{},"virtualService":{}},"enabled":false,"ingress":{"linkAdditionalAnnotations":{},"linkEnabled":true,"linkLabels":{}},"linkServiceName":"link"},"nginx-ingress":{"enabled":true},"persistence":{"customAnnotations":{"job":{},"secret":{},"service":{}},"enabled":true},"saml":{"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":false,"ingress":{"samlAdditionalAnnotations":{},"samlEnabled":false,"samlLabels":{}},"samlServiceName":"saml"},"scim":{"appLoggers":{"enableStdoutLogPrefix":"true","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scimLogLevel":"INFO","scimLogTarget":"STDOUT","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"scimAdditionalAnnotations":{},"scimConfigAdditionalAnnotations":{},"scimConfigEnabled":false,"scimConfigLabels":{},"scimEnabled":false,"scimLabels":{}},"scimServiceName":"scim"},"serviceAccountName":"default","storageClass":{"allowVolumeExpansion":true,"allowedTopologies":[],"mountOptions":["debug"],"parameters":{},"provisioner":"microk8s.io/hostpath","reclaimPolicy":"Retain","volumeBindingMode":"WaitForFirstConsumer"},"usrEnvs":{"normal":{},"secret":{}}}` | Parameters used globally across all services helm charts. | +| global | object | `{"alb":{"ingress":false},"auth-server":{"appLoggers":{"auditStatsLogLevel":"INFO","auditStatsLogTarget":"FILE","authLogLevel":"INFO","authLogTarget":"STDOUT","enableStdoutLogPrefix":"true","httpLogLevel":"INFO","httpLogTarget":"FILE","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"authEncKeys":"RSA1_5 RSA-OAEP","authServerServiceName":"auth-server","authSigKeys":"RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512","cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"authServerAdditionalAnnotations":{},"authServerEnabled":true,"authServerLabels":{},"authzenAdditionalAnnotations":{},"authzenConfigEnabled":true,"authzenConfigLabels":{},"deviceCodeAdditionalAnnotations":{},"deviceCodeEnabled":true,"deviceCodeLabels":{},"firebaseMessagingAdditionalAnnotations":{},"firebaseMessagingEnabled":true,"firebaseMessagingLabels":{},"lockAdditionalAnnotations":{},"lockConfigAdditionalAnnotations":{},"lockConfigEnabled":false,"lockConfigLabels":{},"lockEnabled":false,"lockLabels":{},"openidAdditionalAnnotations":{},"openidConfigEnabled":true,"openidConfigLabels":{},"u2fAdditionalAnnotations":{},"u2fConfigEnabled":true,"u2fConfigLabels":{},"uma2AdditionalAnnotations":{},"uma2ConfigEnabled":true,"uma2ConfigLabels":{},"webdiscoveryAdditionalAnnotations":{},"webdiscoveryEnabled":true,"webdiscoveryLabels":{},"webfingerAdditionalAnnotations":{},"webfingerEnabled":true,"webfingerLabels":{}},"lockEnabled":false},"auth-server-key-rotation":{"customAnnotations":{"cronjob":{},"secret":{},"service":{}},"enabled":true,"initKeysLife":48},"awsStorageType":"io1","azureStorageAccountType":"Standard_LRS","azureStorageKind":"Managed","casa":{"appLoggers":{"casaLogLevel":"INFO","casaLogTarget":"STDOUT","enableStdoutLogPrefix":"true","timerLogLevel":"INFO","timerLogTarget":"FILE"},"casaServiceName":"casa","cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"casaAdditionalAnnotations":{},"casaEnabled":false,"casaLabels":{}}},"cloud":{"testEnviroment":false},"cnAwsConfigFile":"/etc/jans/conf/aws_config_file","cnAwsSecretsReplicaRegionsFile":"/etc/jans/conf/aws_secrets_replica_regions","cnAwsSharedCredentialsFile":"/etc/jans/conf/aws_shared_credential_file","cnConfiguratorConfigurationFile":"/etc/jans/conf/configuration.json","cnConfiguratorCustomSchema":{"secretName":""},"cnConfiguratorDumpFile":"/etc/jans/conf/configuration.out.json","cnConfiguratorKey":"","cnConfiguratorKeyFile":"/etc/jans/conf/configuration.key","cnDocumentStoreType":"DB","cnGoogleApplicationCredentials":"/etc/jans/conf/google-credentials.json","cnPersistenceType":"sql","cnPrometheusPort":"","cnSqlPasswordFile":"/etc/jans/conf/sql_password","config":{"customAnnotations":{"clusterRoleBinding":{},"configMap":{},"job":{},"role":{},"roleBinding":{},"secret":{},"service":{},"serviceAccount":{}},"enabled":true},"config-api":{"appLoggers":{"configApiLogLevel":"INFO","configApiLogTarget":"STDOUT","enableStdoutLogPrefix":"true","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","configApiServerServiceName":"config-api","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"configApiAdditionalAnnotations":{},"configApiEnabled":true,"configApiLabels":{}},"plugins":"fido2,scim,user-mgt"},"configAdapterName":"kubernetes","configSecretAdapter":"kubernetes","fido2":{"appLoggers":{"enableStdoutLogPrefix":"true","fido2LogLevel":"INFO","fido2LogTarget":"STDOUT","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"fido2ServiceName":"fido2","ingress":{"fido2AdditionalAnnotations":{},"fido2ConfigAdditionalAnnotations":{},"fido2ConfigEnabled":false,"fido2ConfigLabels":{},"fido2Enabled":false,"fido2Labels":{},"fido2WebauthnAdditionalAnnotations":{},"fido2WebauthnEnabled":false,"fido2WebauthnLabels":{}}},"fqdn":"demoexample.jans.io","gcePdStorageType":"pd-standard","isFqdnRegistered":false,"istio":{"additionalAnnotations":{},"additionalLabels":{},"enabled":false,"gateways":[],"ingress":false,"namespace":"istio-system"},"jobTtlSecondsAfterFinished":300,"kc-scheduler":{"enabled":false},"lbIp":"22.22.22.22","link":{"appLoggers":{"enableStdoutLogPrefix":"true","linkLogLevel":"INFO","linkLogTarget":"STDOUT","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"service":{},"virtualService":{}},"enabled":false,"ingress":{"linkAdditionalAnnotations":{},"linkEnabled":true,"linkLabels":{}},"linkServiceName":"link"},"nginx-ingress":{"enabled":true},"persistence":{"customAnnotations":{"job":{},"secret":{},"service":{}},"enabled":true},"saml":{"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":false,"ingress":{"samlAdditionalAnnotations":{},"samlEnabled":false,"samlLabels":{}},"samlServiceName":"saml"},"scim":{"appLoggers":{"enableStdoutLogPrefix":"true","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scimLogLevel":"INFO","scimLogTarget":"STDOUT","scriptLogLevel":"INFO","scriptLogTarget":"FILE"},"cnCustomJavaOptions":"","customAnnotations":{"deployment":{},"destinationRule":{},"horizontalPodAutoscaler":{},"pod":{},"podDisruptionBudget":{},"secret":{},"service":{},"virtualService":{}},"enabled":true,"ingress":{"scimAdditionalAnnotations":{},"scimConfigAdditionalAnnotations":{},"scimConfigEnabled":false,"scimConfigLabels":{},"scimEnabled":false,"scimLabels":{}},"scimServiceName":"scim"},"serviceAccountName":"default","storageClass":{"allowVolumeExpansion":true,"allowedTopologies":[],"mountOptions":["debug"],"parameters":{},"provisioner":"microk8s.io/hostpath","reclaimPolicy":"Retain","volumeBindingMode":"WaitForFirstConsumer"},"usrEnvs":{"normal":{},"secret":{}}}` | Parameters used globally across all services helm charts. | | global.alb.ingress | bool | `false` | Activates ALB ingress | | global.auth-server-key-rotation.enabled | bool | `true` | Boolean flag to enable/disable the auth-server-key rotation cronjob chart. | | global.auth-server-key-rotation.initKeysLife | int | `48` | The initial auth server key rotation keys life in hours | @@ -344,6 +344,8 @@ Kubernetes: `>=v1.22.0-0` | global.cnConfiguratorCustomSchema | object | `{"secretName":""}` | Use custom configuration schema in existing secrets. Note, the secrets has to contain the key configuration.json or any basename as specified in cnConfiguratorConfigurationFile. | | global.cnConfiguratorCustomSchema.secretName | string | `""` | The name of the secrets used for storing custom configuration schema. | | global.cnConfiguratorDumpFile | string | `"/etc/jans/conf/configuration.out.json"` | Path to dumped configuration schema file | +| global.cnConfiguratorKey | string | `""` | Key to encrypt/decrypt configuration schema file using AES-256 CBC mode. Set the value to empty string to disable encryption/decryption, or 32 alphanumeric characters to enable it. | +| global.cnConfiguratorKeyFile | string | `"/etc/jans/conf/configuration.key"` | Path to file contains key to encrypt/decrypt configuration schema file. | | global.cnDocumentStoreType | string | `"DB"` | Document store type to use for shibboleth files DB. | | global.cnGoogleApplicationCredentials | string | `"/etc/jans/conf/google-credentials.json"` | Base64 encoded service account. The sa must have roles/secretmanager.admin to use Google secrets. Leave as this is a sensible default. | | global.cnPersistenceType | string | `"sql"` | Persistence backend to run Janssen with hybrid|sql | diff --git a/charts/janssen/charts/config/templates/_helpers.tpl b/charts/janssen/charts/config/templates/_helpers.tpl index c10074117bf..e03aae53613 100644 --- a/charts/janssen/charts/config/templates/_helpers.tpl +++ b/charts/janssen/charts/config/templates/_helpers.tpl @@ -73,10 +73,10 @@ Create optional scopes list {{- define "config.optionalScopes"}} {{ $newList := list }} {{- if eq .Values.configmap.cnCacheType "REDIS" }} -{{ $newList = append $newList ("redis" | quote ) }} +{{ $newList = append $newList "redis" }} {{- end}} {{ if eq .Values.global.cnPersistenceType "sql" }} -{{ $newList = append $newList ("sql" | quote) }} +{{ $newList = append $newList "sql" }} {{- end }} {{ toJson $newList }} {{- end }} @@ -105,3 +105,58 @@ Create AWS config. {{- end }} {{- printf "[%s]\nregion = %s\n" $profile .Values.configmap.cnAwsDefaultRegion }} {{- end }} + +{{/* +Obfuscate configuration schema (only if configuration key is available) +*/}} +{{- define "config.prepareSchema" }} + +{{- $configmapSchema := dict }} +{{- $_ := set $configmapSchema "hostname" .Values.global.fqdn }} +{{- $_ := set $configmapSchema "country_code" .Values.countryCode }} +{{- $_ := set $configmapSchema "state" .Values.state }} +{{- $_ := set $configmapSchema "city" .Values.city }} +{{- $_ := set $configmapSchema "admin_email" .Values.email }} +{{- $_ := set $configmapSchema "orgName" .Values.orgName }} +{{- $_ := set $configmapSchema "auth_sig_keys" (index .Values "global" "auth-server" "authSigKeys") }} +{{- $_ := set $configmapSchema "auth_enc_keys" (index .Values "global" "auth-server" "authEncKeys") }} +{{- $_ := set $configmapSchema "optional_scopes" (include "config.optionalScopes" . | trim) }} +{{- if .Values.global.saml.enabled }} +{{- $_ := set $configmapSchema "kc_admin_username" .Values.configmap.kcAdminUsername }} +{{- end }} +{{- $_ := set $configmapSchema "init_keys_exp" (index .Values "global" "auth-server-key-rotation" "initKeysLife") }} + +{{- $secretSchema := dict }} +{{- $_ := set $secretSchema "admin_password" .Values.adminPassword }} +{{- $_ := set $secretSchema "redis_password" .Values.redisPassword }} +{{- if or ( eq .Values.global.cnPersistenceType "sql" ) ( eq .Values.global.cnPersistenceType "hybrid" ) }} +{{- $_ := set $secretSchema "sql_password" .Values.configmap.cnSqldbUserPassword }} +{{- end }} +{{- if eq .Values.global.configSecretAdapter "vault" }} +{{- $_ := set $secretSchema "vault_role_id" .Values.configmap.cnVaultRoleId }} +{{- $_ := set $secretSchema "vault_secret_id" .Values.configmap.cnVaultSecretId }} +{{- end }} +{{- if or (eq .Values.global.configSecretAdapter "google") (eq .Values.global.configAdapterName "google") }} +{{- $_ := set $secretSchema "google_credentials" .Values.configmap.cnGoogleSecretManagerServiceAccount }} +{{- end }} +{{- if or (eq .Values.global.configAdapterName "aws") (eq .Values.global.configSecretAdapter "aws") }} +{{- $_ := set $secretSchema "aws_credentials" (include "config.aws-shared-credentials" . | b64enc) }} +{{- $_ := set $secretSchema "aws_config" (include "config.aws-config" . | b64enc) }} +{{- $_ := set $secretSchema "aws_replica_regions" (toJson .Values.configmap.cnAwsSecretsReplicaRegions | b64enc) }} +{{- end }} +{{- if .Values.global.saml.enabled }} +{{- $_ := set $secretSchema "kc_db_password" .Values.configmap.kcDbPassword }} +{{- $_ := set $secretSchema "kc_admin_password" .Values.configmap.kcAdminPassword }} +{{- end }} +{{- $_ := set $secretSchema "encoded_salt" .Values.salt }} + +{{- $schema := dict "_configmap" $configmapSchema "_secret" $secretSchema }} + +{{- if .Values.global.cnConfiguratorKey }} +{{- printf "%s" (encryptAES .Values.global.cnConfiguratorKey (toPrettyJson $schema)) }} +{{- else -}} +{{- toPrettyJson $schema }} +{{- end }} + +{{/* end of helpers */}} +{{- end }} diff --git a/charts/janssen/charts/config/templates/secrets.yaml b/charts/janssen/charts/config/templates/secrets.yaml index 3bc6dfee66d..dfca599dae0 100644 --- a/charts/janssen/charts/config/templates/secrets.yaml +++ b/charts/janssen/charts/config/templates/secrets.yaml @@ -22,45 +22,33 @@ metadata: type: Opaque stringData: {{ .Values.global.cnConfiguratorConfigurationFile | base }}: |- - { - "_configmap": { - "hostname": {{ .Values.global.fqdn | quote }}, - "country_code": {{ .Values.countryCode | quote }}, - "state": {{ .Values.state | quote }}, - "city": {{ .Values.city | quote }}, - "admin_email": {{ .Values.email | quote }}, - "orgName": {{ .Values.orgName | quote }}, - "auth_sig_keys": {{ index .Values "global" "auth-server" "authSigKeys" | quote }}, - "auth_enc_keys": {{ index .Values "global" "auth-server" "authEncKeys" | quote }}, - "optional_scopes": {{ list (include "config.optionalScopes" . | fromJsonArray | join ",") | quote }}, - {{- if .Values.global.saml.enabled }} - "kc_admin_username": {{ .Values.configmap.kcAdminUsername | quote }}, - {{- end }} - "init_keys_exp": {{ index .Values "global" "auth-server-key-rotation" "initKeysLife" }} - }, - "_secret": { - "admin_password": {{ .Values.adminPassword | quote }}, - "redis_password": {{ .Values.redisPassword | quote }}, - {{ if or ( eq .Values.global.cnPersistenceType "sql" ) ( eq .Values.global.cnPersistenceType "hybrid" ) }} - "sql_password": {{ .Values.configmap.cnSqldbUserPassword | quote }}, - {{- end }} - {{ if eq .Values.global.configSecretAdapter "vault" }} - "vault_role_id": {{ .Values.configmap.cnVaultRoleId | quote }}, - "vault_secret_id": {{ .Values.configmap.cnVaultSecretId | quote }}, - {{- end }} - {{ if or (eq .Values.global.configSecretAdapter "google") (eq .Values.global.configAdapterName "google") }} - "google_credentials": {{ .Values.configmap.cnGoogleSecretManagerServiceAccount | quote }}, - {{- end }} - {{ if or (eq .Values.global.configAdapterName "aws") (eq .Values.global.configSecretAdapter "aws") }} - "aws_credentials": {{ include "config.aws-shared-credentials" . | b64enc | quote }}, - "aws_config": {{ include "config.aws-config" . | b64enc | quote }}, - "aws_replica_regions": {{ .Values.configmap.cnAwsSecretsReplicaRegions | toJson | b64enc | quote }}, - {{- end }} - {{- if .Values.global.saml.enabled }} - "kc_db_password": {{ .Values.configmap.kcDbPassword | quote }}, - "kc_admin_password": {{ .Values.configmap.kcAdminPassword | quote }}, - {{- end }} - "encoded_salt": {{ .Values.salt | quote }} - } - } -{{- end -}} +{{ include "config.prepareSchema" . | indent 4 }} +{{- end }} + +--- + +{{- if .Values.global.cnConfiguratorKey -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-configuration-key-file + namespace: {{ .Release.Namespace }} + labels: + APP_NAME: configurator +{{ include "config.labels" . | indent 4 }} +{{- if .Values.additionalLabels }} +{{ toYaml .Values.additionalLabels | indent 4 }} +{{- end }} +{{- if or (.Values.additionalAnnotations) (.Values.global.config.customAnnotations.secret) }} + annotations: +{{- if .Values.additionalAnnotations }} +{{ toYaml .Values.additionalAnnotations | indent 4 }} +{{- end }} +{{- if .Values.global.config.customAnnotations.secret }} +{{ toYaml .Values.global.config.customAnnotations.secret | indent 4 }} +{{- end }} +{{- end }} +type: Opaque +data: + {{ .Values.global.cnConfiguratorKeyFile | base }}: {{ .Values.global.cnConfiguratorKey | b64enc }} +{{- end }} diff --git a/charts/janssen/templates/_helpers.tpl b/charts/janssen/templates/_helpers.tpl index c284b5db6b0..8e2cc761e0c 100644 --- a/charts/janssen/templates/_helpers.tpl +++ b/charts/janssen/templates/_helpers.tpl @@ -37,12 +37,23 @@ Create configuration schema-related objects. {{- define "cn.config.schema" -}} {{- $commonName := (printf "%s-configuration-file" .Release.Name) -}} {{- $secretName := .Values.global.cnConfiguratorCustomSchema.secretName | default $commonName -}} +{{- $keyName := (printf "%s-configuration-key-file" .Release.Name) -}} volumes: - name: {{ $commonName }} secret: secretName: {{ $secretName }} +{{- if .Values.global.cnConfiguratorKey }} + - name: {{ $keyName }} + secret: + secretName: {{ $keyName }} +{{- end }} volumeMounts: - name: {{ $commonName }} mountPath: {{ .Values.global.cnConfiguratorConfigurationFile }} subPath: {{ .Values.global.cnConfiguratorConfigurationFile | base }} -{{- end -}} +{{- if .Values.global.cnConfiguratorKey }} + - name: {{ $keyName }} + mountPath: {{ .Values.global.cnConfiguratorKeyFile }} + subPath: {{ .Values.global.cnConfiguratorKeyFile | base }} +{{- end }} +{{- end }} diff --git a/charts/janssen/values.schema.json b/charts/janssen/values.schema.json index ea4fc5db7a8..2b571dd738f 100644 --- a/charts/janssen/values.schema.json +++ b/charts/janssen/values.schema.json @@ -1254,6 +1254,11 @@ "description": "The location of file contains password for the SQL user config.configmap.cnSqlDbUser. The file path must end with sql_password.", "type": "string", "pattern": ".*sql_password\\b.*" + }, + "cnConfiguratorKey": { + "description": "Key to encrypt/decrypt configuration schema file using AES-256 CBC mode. Set the value to empty string to disable encryption/decryption, or 32 alphanumeric characters to enable it.", + "type": "string", + "pattern": "^(?:[a-zA-Z0-9]{32})?$" } } }, @@ -2643,4 +2648,4 @@ "else": true } } -} \ No newline at end of file +} diff --git a/charts/janssen/values.yaml b/charts/janssen/values.yaml index 6f6ee66f33c..edecfe88d4f 100644 --- a/charts/janssen/values.yaml +++ b/charts/janssen/values.yaml @@ -249,9 +249,9 @@ config: cnVaultAddr: http://localhost:8200 # -- Verify connection to Vault. cnVaultVerify: false - # -- Path to file contains Vault AppRole role ID. + # -- Path to the file that contains Vault AppRole role ID. cnVaultRoleIdFile: /etc/certs/vault_role_id - # -- Path to file contains Vault AppRole secret ID. + # -- Path to the file that contains Vault AppRole secret ID. cnVaultSecretIdFile: /etc/certs/vault_secret_id # -- Vault namespace used to access the secrets. cnVaultNamespace: "" @@ -259,7 +259,7 @@ config: cnVaultKvPath: secret # -- Base prefix name used to access secrets. cnVaultPrefix: jans - # -- Path to Vault AppRole. + # -- Path to the Vault AppRole. cnVaultAppRolePath: approle # [vault_envs] END # -- Value passed to Java option -XX:MaxRAMPercentage @@ -1190,19 +1190,23 @@ global: samlAdditionalAnnotations: { } # -- passing custom java options to saml. DO NOT PASS JAVA_OPTIONS in envs. cnCustomJavaOptions: "" - # -- Path to SQL password file + # -- Path to the SQL password file cnSqlPasswordFile: /etc/jans/conf/sql_password kc-scheduler: # -- Boolean flag to enable/disable the kc-scheduler cronjob chart. enabled: false - # -- Path to configuration schema file + # -- Path to the configuration schema file cnConfiguratorConfigurationFile: /etc/jans/conf/configuration.json - # -- Path to dumped configuration schema file + # -- Path to the dumped configuration schema file cnConfiguratorDumpFile: /etc/jans/conf/configuration.out.json - # -- Use custom configuration schema in existing secrets. Note, the secrets has to contain the key configuration.json or any basename as specified in cnConfiguratorConfigurationFile. + # -- Use custom configuration schema in existing Kubernetes secret. Note that the secret has to contain the configuration.json key or any basename as specified in cnConfiguratorConfigurationFile. cnConfiguratorCustomSchema: - # -- The name of the secrets used for storing custom configuration schema. + # -- The name of the Kubernetes secret used for storing custom configuration schema. secretName: "" + # -- Key to encrypt/decrypt configuration schema file using AES-256 CBC mode. Set the value to empty string to disable encryption/decryption, or 32 alphanumeric characters to enable it. + cnConfiguratorKey: "" + # -- Path to the file that contains the key to encrypt/decrypt the configuration schema file. + cnConfiguratorKeyFile: /etc/jans/conf/configuration.key # -- Nginx ingress definitions chart nginx-ingress: diff --git a/demos/README.md b/demos/README.md index d16501ccf25..66b93674826 100644 --- a/demos/README.md +++ b/demos/README.md @@ -4,6 +4,10 @@ This folder holds different demos for different applications with janssen author ## [Benchmarking](benchmarking) Holds a docker load test image packaging for Janssen. This image can load test users to a janssen environment and can execute jmeter tests. -## [Jans-tent](jans-tent) -Reliable OpenID client to be used in auth testing. +## [Janssen Chip](jans-chip) +- A first party android mobile application that leverages dynamic client registration (DCR), DPoP access tokens. +- Passkey authentication + +## [Janssen Tarp](jans-tarp) +A Relying Party tool in form of a Browser Extension for convenient testing of authentication flows on a browser. diff --git a/demos/benchmarking/docker-jans-loadtesting-jmeter/Dockerfile b/demos/benchmarking/docker-jans-loadtesting-jmeter/Dockerfile index 00dbd0b23bd..ac11e837321 100644 --- a/demos/benchmarking/docker-jans-loadtesting-jmeter/Dockerfile +++ b/demos/benchmarking/docker-jans-loadtesting-jmeter/Dockerfile @@ -1,5 +1,5 @@ # FROM blazemeter/taurus:master-f96971fa-2022-12-12 -FROM blazemeter/taurus:1.16.35@sha256:e4232d0b0fcf16f22cce5420d8286d89b96f8ff1490ed4d141c435d12db0a731 +FROM blazemeter/taurus:1.16.38@sha256:5bb39436180f7c769e00140b781bb1054a1eb4592dd9b82f76dcde470811bf39 # =============== # Ubuntu packages diff --git a/demos/jans-tent/.flaskenv b/demos/jans-tent/.flaskenv deleted file mode 100644 index bc1b2cf6e71..00000000000 --- a/demos/jans-tent/.flaskenv +++ /dev/null @@ -1,2 +0,0 @@ -#.flaskenv -FLASK_APP=clientapp diff --git a/demos/jans-tent/.gitignore b/demos/jans-tent/.gitignore deleted file mode 100644 index 6b3dc1fcd19..00000000000 --- a/demos/jans-tent/.gitignore +++ /dev/null @@ -1,146 +0,0 @@ -#jans-tent-specific -client_info.json -*.log.* - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -.vscode/ -.scannerwork - diff --git a/demos/jans-tent/LICENSE b/demos/jans-tent/LICENSE deleted file mode 100644 index 6912a5f93c9..00000000000 --- a/demos/jans-tent/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 Christian Eland - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/demos/jans-tent/README.md b/demos/jans-tent/README.md deleted file mode 100644 index 3f6c6c1ff87..00000000000 --- a/demos/jans-tent/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# Jans Tent - -To test an OpenID Provider ("OP"), you need a test Relying Party ("RP"). Jans -Tent is easy to configure RP which enables you to send different requests by -quickly modifying one file (`config.py`). It's a Python Flask application, -so it's easy to hack for other testing requirements. - -By default, it uses `localhost` as the `redirect_uri`, so if you run it on your -laptop, all you need to do is specify the OP hostname to run it. Tent uses -dynamic client registration to obtain client credentials. But you can also use -an existing client_id if you like. - -## Installation - -**Important**: Ensure you have `Python >= 3.11` - -**Mac Users**: We recommend using [pyenv - simple python version management](https://github.com/pyenv/pyenv) instead of Os x native python. - -1. Navigate to the project root folder `jans/demos/jans-tent` -2. Create virtual environment -```bash -python3 -m venv venv -```` -3. Activate the virtual virtual environment -```bash -source venv/bin/activate -``` -4. Install dependencies -```bash -pip install -r requirements.txt -``` - -## Setup - -### 1. Edit configuration file `clientapp/config.py` according to your needs: - * Set `ISSUER`, replace `op_hostname` (required) - * Set any other desired configuration - -### 2. Generate test RP server self signed certs - -Generate `key.pem` and `cert.pem` at `jans-tent` project root folder (`jans/demos/jans-tent`). i.e: -```bash -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -``` - -### 3. Import your OP TLS certificate - -(remember to be inside your virtual environment) - -Supply the hostname of the ISSUER after the `=` - -```bash -export OP_HOSTNAME= -``` - -```bash -echo | openssl s_client -servername $OP_HOSTNAME -connect $OP_HOSTNAME:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > op_web_cert.cer -``` - -```bash -export CERT_PATH=$(python3 -m certifi) -``` - -```bash -export SSL_CERT_FILE=${CERT_PATH} -``` - -```bash -export REQUESTS_CA_BUNDLE=${CERT_PATH} && mv op_web_cert.cer $CERT_PATH -``` - -## Using the server - -### Start the server - -Please notice that your client will be automatically registered once the server -starts. If your client was already registered, when you start the server again, -it won't register. Remember to be inside your virtual environment! - -```bash -python main.py -``` - -### Login! - -Navigate your browser to `https://localhost:9090` and click the link to start. - -## Manual client configuration - -In case your OP doesn't support dynamic registration, manually configure your -client by creating a file caled `client_info.json` in the `jans-tent` folder -with the following claims: - -```json -{ - "op_metadata_url": "https://op_hostname/.well-known/openid-configuration", - "client_id": "e4f2c3a9-0797-4c6c-9268-35c5546fb3e9", - "client_secret": "a3e71cf1-b9b4-44c5-a9e6-4c7b5c660a5d" -} -``` - -## Updating Tent to use a different OP - -If you want to test a different OP, do the following: - -1. Remove `op_web_cert` from the tent folder, and follow the procedure above -to download and install a new OP TLS certificate -2. Remove `client_info.json` from the tent folder -3. Update the value of `ISSUER` in `./clientapp/config.py` -4. Run `./register_new_client.py` - -## Other Tent endpoints - -### Auto-register endpoint - -Sending a `POST` request to Jans Tent `/register` endpoint containing a `JSON` -with the OP/AS url and client url, like this: - -```json -{ - "op_url": "https://OP_HOSTNAME", - "client_url": "https://localhost:9090", - "additional_params": { - "scope": "openid mail profile" - } -} -``` -Please notice that `additional_params` is not required by endpoint. - -The response will return the registered client id and client secret - -### Auto-config endpoint - -Sending a `POST` request to the Tent `/configuration` endpoint, containing the -client id, client secret, and metadata endpoint will fetch data from OP metadata -url and override the `config.py` settings during runtime. - -```json -{ - "client_id": "e4f2c3a9-0797-4c6c-9268-35c5546fb3e9", - "client_secret": "5c9e4775-0f1d-4a56-87c9-a629e1f88b9b", - "op_metadata_url": "https://OP_HOSTNAME/.well-known/openid-configuration" -} -``` diff --git a/demos/jans-tent/behave.ini b/demos/jans-tent/behave.ini deleted file mode 100644 index cbb1bc67a71..00000000000 --- a/demos/jans-tent/behave.ini +++ /dev/null @@ -1,3 +0,0 @@ -[behave] -stderr_capture=False -stdout_capture=False diff --git a/demos/jans-tent/clientapp/__init__.py b/demos/jans-tent/clientapp/__init__.py deleted file mode 100644 index a7429e815a5..00000000000 --- a/demos/jans-tent/clientapp/__init__.py +++ /dev/null @@ -1,251 +0,0 @@ -''' -Project: Test Auth Client -Author: Christian Hawk - - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import base64 -import urllib -import json -import os -from urllib.parse import urlparse -from authlib.integrations.flask_client import OAuth -from flask import (Flask, jsonify, redirect, render_template, request, session, - url_for) -from . import config as cfg -from .helpers.client_handler import ClientHandler -from .helpers.cgf_checker import register_client_if_no_client_info -from .utils.logger import setup_logger - -setup_logger() - -oauth = OAuth() - - -def add_config_from_json(): - with open('client_info.json', 'r') as openfile: - client_info = json.load(openfile) - cfg.SERVER_META_URL = client_info['op_metadata_url'] - cfg.CLIENT_ID = client_info['client_id'] - cfg.CLIENT_SECRET = client_info['client_secret'] - cfg.END_SESSION_ENDPOINT = client_info['end_session_endpoint'] # separate later - - -def get_preselected_provider(): - provider_id_string = cfg.PRE_SELECTED_PROVIDER_ID - provider_object = '{ "provider" : "%s" }' % provider_id_string - provider_object_bytes = provider_object.encode() - base64url_bytes = base64.urlsafe_b64encode(provider_object_bytes) - base64url_value = base64url_bytes.decode() - # if base64url_value.endswith('='): - # base64url_value_unpad = base64url_value.replace('=', '') - # return base64url_value_unpad - return base64url_value - - -def get_provider_host(): - provider_host_string = cfg.PROVIDER_HOST_STRING - provider_object = '{ "providerHost" : "%s" }' % provider_host_string - provider_object_bytes = provider_object.encode() - base64url_bytes = base64.urlsafe_b64encode(provider_object_bytes) - base64url_value = base64url_bytes.decode() - # if base64url_value.endswith('='): - # base64url_value_unpad = base64url_value.replace('=', '') - # return base64url_value_unpad - return base64url_value - - -def ssl_verify(ssl_verify=cfg.SSL_VERIFY): - if ssl_verify is False: - os.environ['CURL_CA_BUNDLE'] = "" - - -class BaseClientErrors(Exception): - status_code = 500 - - -def create_app(): - register_client_if_no_client_info() - add_config_from_json() - ssl_verify() - - app = Flask(__name__) - - app.secret_key = b'fasfafpj3rasdaasfglaksdgags331s' - app.config['OP_CLIENT_ID'] = cfg.CLIENT_ID - app.config['OP_CLIENT_SECRET'] = cfg.CLIENT_SECRET - oauth.init_app(app) - oauth.register( - 'op', - server_metadata_url=cfg.SERVER_META_URL, - client_kwargs={ - 'scope': cfg.SCOPE - }, - token_endpoint_auth_method=cfg.SERVER_TOKEN_AUTH_METHOD - ) - - @app.route('/') - def index(): - user = session.get('user') - id_token = session.get('id_token') - return render_template("home.html", user=user, id_token=id_token) - - @app.route('/logout') - def logout(): - app.logger.info('Called /logout') - if 'id_token' in session.keys(): - app.logger.info('Cleaning session credentials') - token_hint = session.get('id_token') - session.pop('id_token') - session.pop('user') - parsed_redirect_uri = urllib.parse.urlparse(cfg.REDIRECT_URIS[0]) - post_logout_redirect_uri = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - return redirect( - '%s?post_logout_redirect_uri=%s&token_hint=%s' % ( - cfg.END_SESSION_ENDPOINT, post_logout_redirect_uri, token_hint - ) - ) - - app.logger.info('Not authorized to logout, redirecting to index') - return redirect(url_for('index')) - - @app.route('/register', methods=['POST']) - def register(): - app.logger.info('/register called') - content = request.json - app.logger.debug('data = %s' % content) - status = 0 - data = '' - if content is None: - status = 400 - # message = 'No json data posted' - elif 'op_url' and 'redirect_uris' not in content: - status = 400 - # message = 'Not needed keys found in json' - else: - app.logger.info('Trying to register client %s on %s' % - (content['redirect_uris'], content['op_url'])) - op_url = content['op_url'] - redirect_uris = content['redirect_uris'] - - op_parsed_url = urlparse(op_url) - client_parsed_redirect_uri = urlparse(redirect_uris[0]) - - if op_parsed_url.scheme != 'https' or client_parsed_redirect_uri.scheme != 'https': - status = 400 - - elif ((( - op_parsed_url.path != '' or op_parsed_url.query != '') or client_parsed_redirect_uri.path == '') or client_parsed_redirect_uri.query != ''): - status = 400 - - else: - additional_metadata = {} - if 'additional_params' in content.keys(): - additional_metadata = content['additional_params'] - client_handler = ClientHandler( - content['op_url'], content['redirect_uris'], additional_metadata - ) - data = client_handler.get_client_dict() - status = 200 - return jsonify(data), status - - @app.route('/protected-content', methods=['GET']) - def protected_content(): - app.logger.debug('/protected-content - cookies = %s' % request.cookies) - app.logger.debug('/protected-content - session = %s' % session) - if 'user' in session: - return session['user'] - - return redirect(url_for('login')) - - @app.route('/login') - def login(): - app.logger.info('/login requested') - redirect_uri = cfg.REDIRECT_URIS[0] - app.logger.debug('/login redirect_uri = %s' % redirect_uri) - # response = oauth.op.authorize_redirect() - query_args = { - 'redirect_uri': redirect_uri, - } - - if cfg.ACR_VALUES is not None: - query_args['acr_values'] = cfg.ACR_VALUES - - # used for inbound-saml, uncomment and set config.py to use it - # if cfg.PRE_SELECTED_PROVIDER is True: - # query_args[ - # 'preselectedExternalProvider'] = get_preselected_provider() - - # used for gluu-passport, , uncomment and set config.py to use it - # if cfg.PROVIDER_HOST_STRING is not None: - # query_args["providerHost"] = get_provider_host() - - if cfg.ADDITIONAL_PARAMS is not None: - query_args |= cfg.ADDITIONAL_PARAMS - - response = oauth.op.authorize_redirect(**query_args) - - app.logger.debug('/login authorize_redirect(redirect_uri) url = %s' % - (response.location)) - - return response - - @app.route('/oidc_callback') - @app.route('/callback') - def callback(): - try: - if not request.args['code']: - return {}, 400 - - app.logger.info('/callback - received %s - %s' % - (request.method, request.query_string)) - token = oauth.op.authorize_access_token() - app.logger.debug('/callback - token = %s' % token) - user = oauth.op.userinfo() - app.logger.debug('/callback - user = %s' % user) - session['user'] = user - session['id_token'] = token['userinfo'] - app.logger.debug('/callback - cookies = %s' % request.cookies) - app.logger.debug('/callback - session = %s' % session) - - return redirect('/') - - except Exception as error: - app.logger.error(str(error)) - return {'error': str(error)}, 400 - - @app.route("/configuration", methods=["POST"]) - def configuration(): - # Receives client configuration via API - app.logger.info('/configuration called') - content = request.json - app.logger.debug("content = %s" % content) - if content is not None: - if 'provider_id' in content: - cfg.PRE_SELECTED_PROVIDER_ID = content['provider_id'] - cfg.PRE_SELECTED_PROVIDER = True - app.logger.debug('/configuration: provider_id = %s' % - content['provider_id']) - - return jsonify({"provider_id": content['provider_id']}), 200 - - if "client_id" in content and "client_secret" in content: - # Setup client_id and client_secret - oauth.op.client_id = content['client_id'] - oauth.op.client_secret = content['client_secret'] - return {}, 200 - else: - return {}, 400 - - return app diff --git a/demos/jans-tent/clientapp/config.py b/demos/jans-tent/clientapp/config.py deleted file mode 100644 index 04bcb8df3b9..00000000000 --- a/demos/jans-tent/clientapp/config.py +++ /dev/null @@ -1,36 +0,0 @@ -# REQUIRED -# Replace op_hostname -ISSUER = 'https://op_hostname' - -# Tent redirect uri -REDIRECT_URIS = [ - 'https://localhost:9090/oidc_callback' -] - -# OPTIONAL: Use at your own risk - -# Token authentication method can be -# client_secret_basic -# client_secret_post -# none -SERVER_TOKEN_AUTH_METHOD = 'client_secret_post' - -# ACR VALUES -# Examples: -# ACR_VALUES = "agama" -# ACR_VALUES = 'simple_password_auth' -ACR_VALUES = None - -# ADDITIONAL PARAMS TO CALL AUTHORIZE ENDPOINT, WITHOUT BASE64 ENCODING. USE DICT {'param': 'value'} -# ADDITIONAL_PARAMS = {'paramOne': 'valueOne', 'paramTwo': 'valueTwo'} -ADDITIONAL_PARAMS = None - -# SYSTEM SETTINGS -# use with caution, unsecure requests, for development environments -SSL_VERIFY = False - -# SCOPES -# Only scope "openid" is required for a pairwise identifier from the OP. -# OP can provision additional optional scopes as needed. -# SCOPE = 'openid email profile' -SCOPE = 'openid' diff --git a/demos/jans-tent/clientapp/helpers/__init__.py b/demos/jans-tent/clientapp/helpers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/clientapp/helpers/cgf_checker.py b/demos/jans-tent/clientapp/helpers/cgf_checker.py deleted file mode 100644 index e5ade597adf..00000000000 --- a/demos/jans-tent/clientapp/helpers/cgf_checker.py +++ /dev/null @@ -1,17 +0,0 @@ -from os.path import exists -import logging -from clientapp.utils.dcr_from_config import register - -logger = logging.getLogger(__name__) - - -def configuration_exists() -> bool: - return exists('client_info.json') - - -def register_client_if_no_client_info() -> None: - if configuration_exists() : - logger.info('Found configuration file client_info.json, skipping auto-register') - else: - logger.info('Client configuration not found, trying to auto-register through DCR') - register() diff --git a/demos/jans-tent/clientapp/helpers/client_handler.py b/demos/jans-tent/clientapp/helpers/client_handler.py deleted file mode 100644 index 7e5f8f12e2a..00000000000 --- a/demos/jans-tent/clientapp/helpers/client_handler.py +++ /dev/null @@ -1,117 +0,0 @@ -''' -Project: Test Auth Client -Author: Christian Hawk - - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import logging -import json -from httplib2 import RelativeURIError -from typing import Optional, Dict, Any - -from oic.oauth2 import ASConfigurationResponse -from oic.oic import Client -from oic.utils.authn.client import CLIENT_AUTHN_METHOD -from .custom_msg_factory import CustomMessageFactory - - -logger = logging.getLogger(__name__) - - -class ClientHandler: - __redirect_uris = None - __client_id = None - __client_secret = None - __metadata_url = None - __op_url = None - __additional_metadata = None - __end_session_endpoint = None - op_data = None - - def __init__(self, op_url: str, redirect_uris: list[str], additional_metadata: dict): - """[initializes] - - :param op_url: [url from oidc provider starting with https] - :type op_url: str - :param redirect_uris: [url from client starting with https] - :type redirect_uris: list - :param additional_metadata: additional client metadata - :type additional_metadata: dict - """ - self.__additional_metadata = additional_metadata - self.clientAdapter = Client(client_authn_method=CLIENT_AUTHN_METHOD, message_factory=CustomMessageFactory) - self.__op_url = op_url - self.__redirect_uris = redirect_uris - self.__metadata_url = '%s/.well-known/openid-configuration' % op_url - self.op_data = self.discover(op_url) - self.reg_info = self.register_client(op_data=self.op_data, redirect_uris=redirect_uris) - self.__end_session_endpoint = self.op_data['end_session_endpoint'] - self.__client_id = self.reg_info['client_id'] - self.__client_secret = self.reg_info['client_secret'] - - def get_client_dict(self) -> dict: - r = { - 'op_metadata_url': self.__metadata_url, - 'client_id': self.__client_id, - 'client_secret': self.__client_secret, - 'end_session_endpoint': self.__end_session_endpoint - } - - return r - - def register_client(self, op_data: ASConfigurationResponse = op_data, redirect_uris: Optional[list[str]] = __redirect_uris) -> dict: - """[register client and returns client information] - - :param op_data: [description] - :type op_data: dict - :param redirect_uris: [description] - :type redirect_uris: list[str] - :return: [client information including client-id and secret] - :rtype: dict - """ - logger.debug('called ClientHandler´s register_client method') - registration_args = {'redirect_uris': redirect_uris, - 'response_types': ['code'], - 'grant_types': ['authorization_code'], - 'application_type': 'web', - 'client_name': 'Jans Tent', - 'token_endpoint_auth_method': 'client_secret_post', - **self.__additional_metadata - } - logger.info('calling register with registration_args: %s', json.dumps(registration_args, indent=2)) - reg_info = self.clientAdapter.register(op_data['registration_endpoint'], **registration_args) - logger.info('register_client - reg_info = %s', json.dumps(reg_info.to_dict(), indent=2)) - return reg_info - - def discover(self, op_url: Optional[str] = __op_url) -> ASConfigurationResponse: - """Discover op information on .well-known/open-id-configuration - :param op_url: [description], defaults to __op_url - :type op_url: str, optional - :return: [data retrieved from OP url] - :rtype: ASConfigurationResponse - """ - logger.debug('called discover') - try: - op_data = self.clientAdapter.provider_config(op_url) - return op_data - - except json.JSONDecodeError as err: - logger.error('Error trying to decode JSON: %s' % err) - - except RelativeURIError as err: - logger.error(err) - - except Exception as e: - logging.error('An unexpected ocurred: %s' % e) - diff --git a/demos/jans-tent/clientapp/helpers/custom_msg_factory.py b/demos/jans-tent/clientapp/helpers/custom_msg_factory.py deleted file mode 100644 index 11f0ae09a60..00000000000 --- a/demos/jans-tent/clientapp/helpers/custom_msg_factory.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Custom message factory required by pyoic to add scope param -Overrides RegistrationRequest, RegistrationResponse -and use them to create CustomMessageFactory -""" - -from oic.oic.message import OIDCMessageFactory, RegistrationRequest, RegistrationResponse, MessageTuple, OPTIONAL_LOGICAL -from oic.oauth2.message import OPTIONAL_LIST_OF_STRINGS, REQUIRED_LIST_OF_STRINGS, SINGLE_OPTIONAL_STRING, SINGLE_OPTIONAL_INT - - -class MyRegistrationRequest(RegistrationRequest): - c_param = { - "redirect_uris": REQUIRED_LIST_OF_STRINGS, - "response_types": OPTIONAL_LIST_OF_STRINGS, - "grant_types": OPTIONAL_LIST_OF_STRINGS, - "application_type": SINGLE_OPTIONAL_STRING, - "contacts": OPTIONAL_LIST_OF_STRINGS, - "client_name": SINGLE_OPTIONAL_STRING, - "logo_uri": SINGLE_OPTIONAL_STRING, - "client_uri": SINGLE_OPTIONAL_STRING, - "policy_uri": SINGLE_OPTIONAL_STRING, - "tos_uri": SINGLE_OPTIONAL_STRING, - "jwks": SINGLE_OPTIONAL_STRING, - "jwks_uri": SINGLE_OPTIONAL_STRING, - "sector_identifier_uri": SINGLE_OPTIONAL_STRING, - "subject_type": SINGLE_OPTIONAL_STRING, - "id_token_signed_response_alg": SINGLE_OPTIONAL_STRING, - "id_token_encrypted_response_alg": SINGLE_OPTIONAL_STRING, - "id_token_encrypted_response_enc": SINGLE_OPTIONAL_STRING, - "userinfo_signed_response_alg": SINGLE_OPTIONAL_STRING, - "userinfo_encrypted_response_alg": SINGLE_OPTIONAL_STRING, - "userinfo_encrypted_response_enc": SINGLE_OPTIONAL_STRING, - "request_object_signing_alg": SINGLE_OPTIONAL_STRING, - "request_object_encryption_alg": SINGLE_OPTIONAL_STRING, - "request_object_encryption_enc": SINGLE_OPTIONAL_STRING, - "token_endpoint_auth_method": SINGLE_OPTIONAL_STRING, - "token_endpoint_auth_signing_alg": SINGLE_OPTIONAL_STRING, - "default_max_age": SINGLE_OPTIONAL_INT, - "require_auth_time": OPTIONAL_LOGICAL, - "default_acr_values": OPTIONAL_LIST_OF_STRINGS, - "initiate_login_uri": SINGLE_OPTIONAL_STRING, - "request_uris": OPTIONAL_LIST_OF_STRINGS, - "post_logout_redirect_uris": OPTIONAL_LIST_OF_STRINGS, - "frontchannel_logout_uri": SINGLE_OPTIONAL_STRING, - "frontchannel_logout_session_required": OPTIONAL_LOGICAL, - "backchannel_logout_uri": SINGLE_OPTIONAL_STRING, - "backchannel_logout_session_required": OPTIONAL_LOGICAL, - "scope": OPTIONAL_LIST_OF_STRINGS, # added - } - c_default = {"application_type": "web", "response_types": ["code"]} - c_allowed_values = { - "application_type": ["native", "web"], - "subject_type": ["public", "pairwise"], - } - - -class CustomMessageFactory(OIDCMessageFactory): - registration_endpoint = MessageTuple(MyRegistrationRequest, RegistrationResponse) - diff --git a/demos/jans-tent/clientapp/templates/home.html b/demos/jans-tent/clientapp/templates/home.html deleted file mode 100644 index 021c6fcfaac..00000000000 --- a/demos/jans-tent/clientapp/templates/home.html +++ /dev/null @@ -1,21 +0,0 @@ - - Index Test - -

Welcome to the test of your life

-

- {% if user %} -

Userinfo JSON payload

-
-        {{ user|tojson }}
-        
-

-

id_token JSON payload

-
-        {{ id_token|tojson }}
-        
- logout - {% else %} -

Click here to start!

- {% endif %} - - \ No newline at end of file diff --git a/demos/jans-tent/clientapp/utils/__init__.py b/demos/jans-tent/clientapp/utils/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/clientapp/utils/dcr_from_config.py b/demos/jans-tent/clientapp/utils/dcr_from_config.py deleted file mode 100644 index 7ab19246abf..00000000000 --- a/demos/jans-tent/clientapp/utils/dcr_from_config.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import urllib.parse - -from clientapp import config as cfg -from clientapp.helpers.client_handler import ClientHandler -import json -from urllib import parse - -OP_URL = cfg.ISSUER -REDIRECT_URIS = cfg.REDIRECT_URIS -SCOPE = cfg.SCOPE -parsed_redirect_uri = urllib.parse.urlparse(cfg.REDIRECT_URIS[0]) -POST_LOGOUT_REDIRECT_URI = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - - -def setup_logging() -> None: - logging.getLogger('oic') - logging.getLogger('urllib3') - logging.basicConfig( - level=logging.DEBUG, - handlers=[logging.StreamHandler(), logging.FileHandler('register_new_client.log')], - format='[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s') - - -def register() -> None: - """ - Register client with information from config and write info to client_info.json - :return: None - """ - logger = logging.getLogger(__name__) - scope_as_list = SCOPE.split(" ") - additional_params = { - 'scope': scope_as_list, - 'post_logout_redirect_uris': [POST_LOGOUT_REDIRECT_URI] - } - client_handler = ClientHandler(OP_URL, REDIRECT_URIS, additional_params) - json_client_info = json.dumps(client_handler.get_client_dict(), indent=4) - with open('client_info.json', 'w') as outfile: - logger.info('Writing registered client information to client_info.json') - outfile.write(json_client_info) - diff --git a/demos/jans-tent/clientapp/utils/logger.py b/demos/jans-tent/clientapp/utils/logger.py deleted file mode 100644 index acbcc2ca7bf..00000000000 --- a/demos/jans-tent/clientapp/utils/logger.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -from logging.handlers import TimedRotatingFileHandler - - -def setup_logger() -> None: - formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s") - log_file = "test-client.log" - file_handler = TimedRotatingFileHandler(log_file, when='midnight') - console_handler = logging.StreamHandler() - console_handler.setFormatter(formatter) - file_handler.setFormatter(formatter) - logging.getLogger("oic") - logging.getLogger("oauth") - logging.getLogger("flask-oidc") - logging.getLogger("urllib3") - logging.basicConfig(level=logging.DEBUG, handlers=[file_handler, console_handler]) diff --git a/demos/jans-tent/docs/images/authorize_code_flow.png b/demos/jans-tent/docs/images/authorize_code_flow.png deleted file mode 100644 index a0c26264989..00000000000 Binary files a/demos/jans-tent/docs/images/authorize_code_flow.png and /dev/null differ diff --git a/demos/jans-tent/main.py b/demos/jans-tent/main.py deleted file mode 100644 index ebd89f31fd6..00000000000 --- a/demos/jans-tent/main.py +++ /dev/null @@ -1,6 +0,0 @@ -from clientapp import create_app - -if __name__ == '__main__': - app = create_app() - app.debug = True - app.run(host='0.0.0.0', ssl_context=('cert.pem', 'key.pem'), port=9090, use_reloader=False) diff --git a/demos/jans-tent/register_new_client.py b/demos/jans-tent/register_new_client.py deleted file mode 100644 index 89061c42275..00000000000 --- a/demos/jans-tent/register_new_client.py +++ /dev/null @@ -1,12 +0,0 @@ -# executes a new client auto-register from config.py -import logging -from clientapp.utils.dcr_from_config import register - -# add independent logging for CLI script -logging.getLogger('oic') -logging.getLogger('urllib3') -logging.basicConfig( - level=logging.DEBUG, - handlers=[logging.StreamHandler(), logging.FileHandler('register_new_client.log')], - format='[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s') -register() diff --git a/demos/jans-tent/requirements.txt b/demos/jans-tent/requirements.txt deleted file mode 100644 index f6438fdbaae..00000000000 --- a/demos/jans-tent/requirements.txt +++ /dev/null @@ -1,119 +0,0 @@ -appnope==0.1.3 -astroid==2.12.5 -asttokens==2.0.8 -async-generator==1.10 -attrs==22.1.0 -Authlib==1.2.0 -autopep8==1.7.0 -backcall==0.2.0 -bandit==1.7.4 -behave==1.2.6 -certifi==2022.12.7 -cffi==1.15.1 -chardet==5.0.0 -charset-normalizer==2.1.1 -click==8.1.3 -coverage==6.4.4 -cryptography==42.0.0 -decorator==5.1.1 -defusedxml==0.7.1 -dill==0.3.5.1 -dodgy==0.2.1 -EasyProcess==1.1 -executing==1.0.0 -flake8==5.0.4 -Flask==2.2.2 -flask-oidc==1.4.0 -future==0.18.3 -gitdb==4.0.9 -GitPython==3.1.37 -h11==0.13.0 -httplib2==0.21.0 -idna==3.3 -importlib-metadata==4.12.0 -iniconfig==1.1.1 -install==1.3.5 -ipdb==0.13.9 -ipython==8.10.0 -ipython-genutils==0.2.0 -isort==5.10.1 -itsdangerous==2.0.0 -jedi==0.18.1 -Jinja2==3.1.2 -lazy-object-proxy==1.7.1 -Mako==1.2.4 -MarkupSafe==2.1.1 -matplotlib-inline==0.1.6 -mccabe==0.7.0 -more-itertools==8.14.0 -mypy==0.971 -mypy-extensions==0.4.3 -oauth2client==4.1.3 -oic==1.5.0 -outcome==1.2.0 -packaging==21.3 -parse==1.19.0 -parse-type==0.6.0 -parso==0.8.3 -pbr==5.10.0 -pep8==1.7.1 -pep8-naming==0.13.2 -pexpect==4.8.0 -pickleshare==0.7.5 -platformdirs==2.5.2 -pluggy==1.0.0 -poetry-semver==0.1.0 -prompt-toolkit==3.0.31 -prospector==0.12.2 -ptyprocess==0.7.0 -pure-eval==0.2.2 -py==1.11.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycodestyle==2.9.1 -pycparser==2.21 -pycryptodomex==3.17 -pydocstyle==6.1.1 -pyflakes==2.5.0 -Pygments==2.13.0 -pyjwkest==1.4.2 -pylama==8.4.1 -pylint==2.15.0 -pylint-celery==0.3 -pylint-common==0.2.5 -pylint-django==2.5.3 -pylint-flask==0.6 -pylint-plugin-utils==0.7 -pyOpenSSL==22.0.0 -pyparsing==3.0.9 -PySocks==1.7.1 -pytest==7.1.3 -python-dotenv==0.21.0 -PyVirtualDisplay==3.0 -PyYAML==6.0 -requests==2.28.1 -requirements-detector==1.0.3 -rsa==4.9 -selenium==4.4.3 -setoptconf==0.3.0 -six==1.16.0 -smmap==5.0.0 -sniffio==1.3.0 -snowballstemmer==2.2.0 -sortedcontainers==2.4.0 -stack-data==0.5.0 -stevedore==4.0.0 -toml==0.10.2 -tomli==2.0.1 -tomlkit==0.11.4 -traitlets==5.3.0 -trio==0.21.0 -trio-websocket==0.9.2 -typed-ast==1.5.4 -typing_extensions==4.3.0 -urllib3==1.26.12 -wcwidth==0.2.5 -Werkzeug==2.2.2 -wrapt==1.14.1 -wsproto==1.2.0 -zipp==3.8.1 diff --git a/demos/jans-tent/tests/behaver/features/environment.py b/demos/jans-tent/tests/behaver/features/environment.py deleted file mode 100644 index 037be43286f..00000000000 --- a/demos/jans-tent/tests/behaver/features/environment.py +++ /dev/null @@ -1,31 +0,0 @@ -from selenium import webdriver -import os -from pyvirtualdisplay import Display - -display = Display(visible=0, size=(1024, 768)) - - -def before_all(context): - os.environ['CURL_CA_BUNDLE'] = "" - display.start() - - -def before_scenario(context, scenario): - options = webdriver.FirefoxOptions() - options.headless = True - context.web = webdriver.Firefox() - - # context.web = webdriver.Firefox() - - -def after_scenario(context, scenario): - context.web.delete_all_cookies() - context.web.close() - - -def after_step(context, step): - print() - - -def after_all(context): - pass diff --git a/demos/jans-tent/tests/behaver/features/oidc_auth.feature b/demos/jans-tent/tests/behaver/features/oidc_auth.feature deleted file mode 100644 index b95f5df3e61..00000000000 --- a/demos/jans-tent/tests/behaver/features/oidc_auth.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: Allow authenticated users to access protected pages - - @authenticated - Scenario: User is authenticated - Given username is "johndoe" - And user is authenticated - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user access the protected content link - - Scenario: User does not exist - Given user does not exist - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user goes to external login page - - Scenario: User is not authenticated - Given username is "johndoe" - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user goes to external login page - - # Scenario: Normal user try to access admin content - # Given username is "johndoe" - # And user role is "user" - # And protected content link is https://chris.testingenv.org/admin/admin-protected-content - # When user clicks the protected content link - # Then user gets a 403 error - - # Scenario: Admin can access admin contents - # Given username is "johndoe" - # And user role is "admin" - # And protected content link is https://chris.testingenv.org/admin/admin-protected-content - # When user clicks the protected content link - # Then user access the protected content link - - - - - diff --git a/demos/jans-tent/tests/behaver/features/passport_social_auth.feature b/demos/jans-tent/tests/behaver/features/passport_social_auth.feature deleted file mode 100644 index 685576147f2..00000000000 --- a/demos/jans-tent/tests/behaver/features/passport_social_auth.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: use passport social github to login - """ - As an user, - I want to use passport-social flow to authenticate - So I can access protected-content - """ - - Background: - Given auth method is passport-social - And user is visiting "/" - - Scenario: User is authenticated - Given username is "johndoe" - And protected content link is https://localhost:5000/content/protected-user-content - When user clicks the protected content link - Then user access the protected content link - - Scenario: User is not authenticated - Given user is not authenticated - When user clicks the protected content link - Then user goes to external login page - - - - - \ No newline at end of file diff --git a/demos/jans-tent/tests/behaver/features/steps/allow.py b/demos/jans-tent/tests/behaver/features/steps/allow.py deleted file mode 100644 index 97a6ddfdb4a..00000000000 --- a/demos/jans-tent/tests/behaver/features/steps/allow.py +++ /dev/null @@ -1,116 +0,0 @@ -from behave import when, then, given -import requests -import time -from selenium.webdriver.common.by import By - -base_url = "https://chris.testingenv.org" - - -def cookiesTransformer(sel_session_id, sel_other_cookies): - ''' This transform cookies from selenium to requests ''' - s = requests.Session() - s.cookies.set('session_id', sel_session_id) - i = 0 - while i < len(sel_other_cookies): - s.cookies.set(sel_other_cookies[i]['name'], - sel_other_cookies[i]['value'], - path=sel_other_cookies[i]['path'], - domain=sel_other_cookies[i]['domain'], - secure=sel_other_cookies[i]['secure'], - rest={'httpOnly': sel_other_cookies[i]['httpOnly']}) - i = i + 1 - - return s - - -@given(u'username is "{username}"') -def define_username(context, username): - context.username = username - context.password = "test123" - - -@given(u'user is authenticated') -def user_authenticates(context): - context.web.get("https://chris.testingenv.org/login") - time.sleep(3) - context.web.set_window_size(625, 638) - context.web.find_element(By.ID, "username").click() - context.web.find_element(By.ID, "username").send_keys("johndoo") - time.sleep(3) - context.web.find_element(By.ID, "password").send_keys("test123") - context.web.find_element(By.ID, "loginButton").click() - time.sleep(3) - - -@given(u'protected content link is {protected_content}') -def define_protected_content_link(context, protected_content): - context.protected_content = protected_content - - -@when(u'user clicks the protected content link') -def user_clicks_protected_content_link(context): - - context.web.get(base_url) - time.sleep(2) - context.web.find_element_by_xpath( - '//a[@href="' + "https://chris.testingenv.org/protected-content" + - '"]').click() - context.has_clicked = True - context.response = requests.get(context.protected_content) - - -@then(u'user access the protected content link') -def user_access_protected_content_link(context): - # WE FETCH THE COOKIES FROM SELENIUM AND PASS THEM TO REQUESTS TO VALIDATE - #sel_cookies = context.web.get_cookies() - #sel_cookie = sel_cookies[0] - # set cookie in requests - - # get session id from selenium - #sel_session_id = context.web.session_id - ''' - sess = requests.Session() - - sess.cookies.set('session_id',sel_session_id) - sess.cookies.set( - sel_cookie['name'], - sel_cookie['value'], - path = sel_cookie['path'], - domain = sel_cookie['domain'], - secure = sel_cookie['secure'], - rest= {'httpOnly' : sel_cookie['httpOnly']} - ) - - new_sess = cookiesTransformer(sel_session_id,sel_cookies) - ''' - new_sess = cookiesTransformer(context.web.session_id, - context.web.get_cookies()) - res = new_sess.get(context.protected_content, verify=False) - - assert res.url == context.protected_content - - -@given(u'user does not exist') -def user_does_not_exist(context): - pass - - -@then(u'user goes to external login page') -def user_directed_to_external_login_page(context): - #context.web.get("https://chris.testingenv.org/login") - - time.sleep(1) - external_login_url = 'https://chris.gluutwo.org/oxauth/login.htm' - #import ipdb; ipdb.set_trace() - assert (context.web.current_url == external_login_url) - #new_sess = cookiesTransformer(context.web.session_id,context.web.get_cookies()) - - -@given(u'user role is "{role}"') -def define_user_role(context, role): - context.role = role - - -@then(u'user gets a 403 error') -def step_impl(context): - raise NotImplementedError(u'STEP: Then user gets a 403 error') diff --git a/demos/jans-tent/tests/unit_integration/helper.py b/demos/jans-tent/tests/unit_integration/helper.py deleted file mode 100644 index b282f4b64ab..00000000000 --- a/demos/jans-tent/tests/unit_integration/helper.py +++ /dev/null @@ -1,189 +0,0 @@ - -from unittest import TestCase -from unittest.mock import MagicMock -import clientapp -from clientapp import create_app -from clientapp.helpers.client_handler import ClientHandler -from flask import Flask -from typing import List -import helper -import os -import builtins - - -class FlaskBaseTestCase(TestCase): - def setUp(self): - self.stashed_add_config_from_json = clientapp.add_config_from_json - clientapp.cfg.CLIENT_ID = 'any-client-id-stub' - clientapp.cfg.CLIENT_SECRET = 'any-client-secret-stub' - clientapp.cfg.SERVER_META_URL = 'https://ophostname.com/server/meta/url' - clientapp.cfg.END_SESSION_ENDPOINT = 'https://ophostname.com/end_session_endpoint' - clientapp.add_config_from_json = MagicMock(name='add_config_from_json') - clientapp.add_config_from_json.return_value(None) - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - builtins.open = MagicMock(name='open') - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - self.app = create_app() - self.app.testing = True - self.app_context = self.app.test_request_context( - base_url="https://chris.testingenv.org") - self.app_context.push() - self.client = self.app.test_client() - - #self.oauth = OAuth(self.app) - os.environ['AUTHLIB_INSECURE_TRANSPORT'] = "1" - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - clientapp.add_config_from_json = self.stashed_add_config_from_json - - -# Helper functions -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -# Mocks -OP_DATA_DICT_RESPONSE = { - 'request_parameter_supported': True, - 'token_revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke', - 'introspection_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/introspection', - 'claims_parameter_supported': False, - 'issuer': 'https://t1.techno24x7.com', - 'userinfo_encryption_enc_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'id_token_encryption_enc_values_supported': ['A128CBC+HS256', 'A256CBC+HS512', 'A128GCM', 'A256GCM'], - 'authorization_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/authorize', - 'service_documentation': 'http://gluu.org/docs', - 'id_generation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/id', - 'claims_supported': ['street_address', 'country', 'zoneinfo', 'birthdate', 'role', 'gender', 'formatted', - 'user_name', 'phone_mobile_number', 'preferred_username', 'locale', 'inum', 'updated_at', - 'nickname', 'email', 'website', 'email_verified', 'profile', 'locality', - 'phone_number_verified', 'given_name', 'middle_name', 'picture', 'name', 'phone_number', - 'postal_code', 'region', 'family_name'], - 'scope_to_claims_mapping': [{ - 'profile': ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', - 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'] - }, { - 'openid': [] - }, { - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/scim_access': [] - }, { - 'permission': ['role'] - }, { - 'super_gluu_ro_session': [] - }, { - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/passport_access': [] - }, { - 'phone': ['phone_number_verified', 'phone_number'] - }, { - 'revoke_session': [] - }, { - 'address': ['formatted', 'postal_code', 'street_address', 'locality', 'country', 'region'] - }, { - 'clientinfo': ['name', 'inum'] - }, { - 'mobile_phone': ['phone_mobile_number'] - }, { - 'email': ['email_verified', 'email'] - }, { - 'user_name': ['user_name'] - }, { - 'oxtrust-api-write': [] - }, { - 'oxd': [] - }, { - 'uma_protection': [] - }, { - 'oxtrust-api-read': [] - }], - 'op_policy_uri': 'http://ox.gluu.org/doku.php?id=oxauth:policy', - 'token_endpoint_auth_methods_supported': ['client_secret_basic', 'client_secret_post', 'client_secret_jwt', - 'private_key_jwt', 'tls_client_auth', 'self_signed_tls_client_auth'], - 'tls_client_certificate_bound_access_tokens': True, - 'response_modes_supported': ['query', 'form_post', 'fragment'], - 'backchannel_logout_session_supported': True, - 'token_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/token', - 'response_types_supported': ['code id_token', 'code', 'id_token', 'token', 'code token', 'code id_token token', - 'id_token token'], - 'request_uri_parameter_supported': True, - 'backchannel_user_code_parameter_supported': False, - 'grant_types_supported': ['implicit', 'refresh_token', 'client_credentials', 'authorization_code', 'password', - 'urn:ietf:params:oauth:grant-type:uma-ticket'], - 'ui_locales_supported': ['en', 'bg', 'de', 'es', 'fr', 'it', 'ru', 'tr'], - 'userinfo_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/userinfo', - 'op_tos_uri': 'http://ox.gluu.org/doku.php?id=oxauth:tos', - 'auth_level_mapping': { - '-1': ['simple_password_auth'], - '60': ['passport_saml'], - '40': ['passport_social'] - }, - 'require_request_uri_registration': False, - 'id_token_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'frontchannel_logout_session_supported': True, - 'claims_locales_supported': ['en'], - 'clientinfo_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/clientinfo', - 'request_object_signing_alg_values_supported': ['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', - 'ES256', 'ES384', 'ES512'], - 'request_object_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'session_revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke_session', - 'check_session_iframe': 'https://t1.techno24x7.com/oxauth/opiframe.htm', - 'scopes_supported': ['address', 'openid', 'clientinfo', 'user_name', 'profile', - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/scim_access', 'uma_protection', - 'permission', 'revoke_session', 'oxtrust-api-write', 'oxtrust-api-read', 'phone', - 'mobile_phone', 'oxd', 'super_gluu_ro_session', 'email', - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/passport_access'], - 'backchannel_logout_supported': True, - 'acr_values_supported': ['simple_password_auth', 'passport_saml', 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', 'passport_social'], - 'request_object_encryption_enc_values_supported': ['A128CBC+HS256', 'A256CBC+HS512', 'A128GCM', 'A256GCM'], - 'display_values_supported': ['page', 'popup'], - 'userinfo_signing_alg_values_supported': ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', - 'ES512'], - 'claim_types_supported': ['normal'], - 'userinfo_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'end_session_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/end_session', - 'revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke', - 'backchannel_authentication_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/bc-authorize', - 'token_endpoint_auth_signing_alg_values_supported': ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', - 'ES384', 'ES512'], - 'frontchannel_logout_supported': True, - 'jwks_uri': 'https://t1.techno24x7.com/oxauth/restv1/jwks', - 'subject_types_supported': ['public', 'pairwise'], - 'id_token_signing_alg_values_supported': ['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', - 'ES384', 'ES512'], - 'registration_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/register', - 'id_token_token_binding_cnf_values_supported': ['tbh'] -} - -REGISTER_CLIENT_RESPONSE = {'allow_spontaneous_scopes': False, 'application_type': 'web', 'rpt_as_jwt': False, - 'registration_client_uri': 'https://t1.techno24x7.com/jans-auth/restv1/register?client_id' - '=079f3682-3d60-4bca-8ff7-bbc7dbc18cd7', - 'run_introspection_script_before_jwt_creation': False, - 'registration_access_token': '89c51fd6-34ec-497e-a4ae-85e21b7e725b', - 'client_id': '079f3682-3d60-4bca-8ff7-bbc7dbc18cd7', - 'token_endpoint_auth_method': 'client_secret_post', - 'scope': 'online_access device_sso openid permission uma_protection offline_access', - 'client_secret': '8f53c454-f6ee-4181-8581-9f1ee120b878', 'client_id_issued_at': 1680038429, - 'backchannel_logout_session_required': False, 'client_name': 'Jans Tent', - 'par_lifetime': 600, 'spontaneous_scopes': [], 'id_token_signed_response_alg': 'RS256', - 'access_token_as_jwt': False, 'grant_types': ['authorization_code'], - 'subject_type': 'pairwise', 'additional_token_endpoint_auth_methods': [], - 'keep_client_authorization_after_expiration': False, 'require_par': False, - 'redirect_uris': ['https://localhost:9090/oidc_callback'], 'additional_audience': [], - 'frontchannel_logout_session_required': False, 'client_secret_expires_at': 0, - 'access_token_signing_alg': 'RS256', 'response_types': ['code']} - - diff --git a/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py b/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py deleted file mode 100644 index 91ebb43edca..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py +++ /dev/null @@ -1,62 +0,0 @@ -import clientapp -from flask import Flask, url_for -from typing import List -from helper import FlaskBaseTestCase - - -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -# class FlaskBaseTestCase(TestCase): -# def setUp(self): -# self.app = clientapp.create_app() -# self.app.testing = True -# self.app_context = self.app.test_request_context( -# base_url="https://chris.testingenv.org") -# self.app_context.push() -# self.client = self.app.test_client() -# #self.oauth = OAuth(self.app) -# os.environ['AUTHLIB_INSECURE_TRANSPORT'] = "1" - - -class TestCallbackEndpoint(FlaskBaseTestCase): - def test_oidc_callback_endpoint_exist(self): - endpoints = [] - for item in clientapp.create_app().url_map.iter_rules(): - endpoint = item.rule - endpoints.append(endpoint) - - self.assertTrue('/oidc_callback' in endpoints, - "enpoint /oidc_callback knão existe no app") - - def test_callback_endpoint_should_exist(self): - - self.assertTrue('callback' in app_endpoints(clientapp.create_app()), - 'endpoint /callback does not exist in app') - - def test_endpoint_args_without_code_should_return_400(self): - resp = self.client.get(url_for('callback')) - - self.assertEqual(resp.status_code, 400) - - -''' - def test_endpoint_should_return_status_code_302(self): - # if there is - - self.assertEqual( - self.client.get(url_for('callback')).status_code, - 302, - 'Callback endpoint is not returning 302 status_code' - ) - - - #def test_endpoint_should_return_ -''' diff --git a/demos/jans-tent/tests/unit_integration/test_cfg_checker.py b/demos/jans-tent/tests/unit_integration/test_cfg_checker.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py b/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py deleted file mode 100644 index 83aef096738..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py +++ /dev/null @@ -1,145 +0,0 @@ -from helper import FlaskBaseTestCase -import clientapp -import helper -from flask import url_for -from clientapp.helpers.client_handler import ClientHandler -from unittest.mock import MagicMock, patch - - -class TestRegisterEndpoint(FlaskBaseTestCase): - - def test_if_app_has_register_endpoint(self): - self.assertIn( - 'register', - helper.app_endpoints(clientapp.create_app()) - ) - - def test_if_endpoint_accepts_post(self): - methods = None - for rule in self.app.url_map.iter_rules('register'): - methods = rule.methods - self.assertIn( - 'POST', - methods - ) - - # def test_init_should_call_discover_once(self): - # ClientHandler.discover = MagicMock(name='discover') - # ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - # ClientHandler.discover.assert_called_once() - - def test_endpoint_should_return_valid_req(self): - self.assertIn( - self.client.post(url_for('register')).status_code, - range(100, 511), - '/register returned invalid requisition' - ) - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_init_client_handler(self): - self.client.post(url_for('register'), json={ - 'op_url': 'https://test.com', - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - ClientHandler.__init__.assert_called_once() - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_accept_2_params(self): - first_value = 'https://op' - second_value = ['https://client.com.br/oidc_callback'] - - self.client.post(url_for('register'), json={ - 'op_url': first_value, - 'redirect_uris': second_value - }) - ClientHandler.__init__.assert_called_once_with(first_value, second_value, {}) - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_accept_3_params(self): - first_value = 'https://op' - second_value = ['https://client.com.br/oidc_callback'] - third_value = {'scope': 'openid email profile'} - - self.client.post(url_for('register'), json={ - 'op_url': first_value, - 'redirect_uris': second_value, - 'additional_params': third_value - }) - - ClientHandler.__init__.assert_called_once_with(first_value, second_value, third_value) - - def test_endpoint_should_return_error_code_400_if_no_data_sent(self): - self.assertEqual( - self.client.post(url_for('register')).status_code, - 400, - 'status_code for empty request is NOT 400' - ) - - def test_should_return_400_error_if_no_needed_keys_provided(self): - self.assertEqual( - self.client.post(url_for('register'), json={ - 'other_key': 'othervalue', - 'another_key': 'another_value' - }).status_code, - 400, - 'not returning 400 code if no needed keys provided' - ) - - def test_should_return_400_if_values_are_not_valid_urls(self): - self.assertEqual( - self.client.post(url_for('register'), json={ - 'op_url': 'not_valid_url', - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }).status_code, - 400, - 'not returning status 400 if values are not valid urls' - ) - - @patch('clientapp.helpers.client_handler.ClientHandler.get_client_dict', MagicMock(return_value=None)) - def test_valid_post_should_should_call_get_client_dict_once(self): - op_url = 'https://op.com.br' - self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - ClientHandler.get_client_dict.assert_called_once() - - def test_should_should_return_200_if_registered(self): - op_url = 'https://op.com.br' - test_client_id = '1234-5678-9ten11' - test_client_secret = 'mysuperprotectedsecret' - with patch.object(ClientHandler, 'get_client_dict', return_value={ - 'op_metadata_url': '%s/.well-known/open-id-configuration' % op_url, - 'client_id': test_client_id, - 'client_secret': test_client_secret - }) as get_client_dict: - response = self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - self.assertEqual(response.status_code, 200) - get_client_dict.reset() - - def test_should_return_expected_keys(self): - op_url = 'https://op.com.br' - redirect_uris = ['https://client.com.br/oidc_calback'] - test_client_id = '1234-5678-9ten11' - test_client_secret = 'mysuperprotectedsecret' - additional_params = {'param1': 'value1'} - - expected_keys = {'op_metadata_url', 'client_id', 'client_secret'} - - with patch.object(ClientHandler, 'get_client_dict', return_value={ - 'op_metadata_url': '%s/.well-known/open-id-configuration' % op_url, - 'client_id': test_client_id, - 'client_secret': test_client_secret - }) as get_client_dict: - response = self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': redirect_uris, - 'additional_params': additional_params - }) - print(response) - assert expected_keys <= response.json.keys(), response.json - - get_client_dict.reset() diff --git a/demos/jans-tent/tests/unit_integration/test_config.py b/demos/jans-tent/tests/unit_integration/test_config.py deleted file mode 100644 index 419e3bd56a9..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_config.py +++ /dev/null @@ -1,19 +0,0 @@ -import clientapp.config as cfg -from unittest import TestCase - - -class TestConfig(TestCase): - def test_has_attribute_SSL_VERIFY(self): - self.assertTrue(hasattr(cfg, 'SSL_VERIFY'), - 'SSL_VERIFY attribute is missing in config.') - - def test_SSL_VERIFY_has_boolean_value(self): - self.assertTrue('__bool__' in cfg.SSL_VERIFY.__dir__(), - 'SSL_VERIFY is not boolean.') - - def test_has_attribute_SCOPE(self): - self.assertTrue(hasattr(cfg, 'SCOPE'), - 'SCOPE attribute is missing in config.') - - def test_SCOPE_default_should_be_openid(self): - self.assertTrue(cfg.SCOPE == 'openid') diff --git a/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py b/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py deleted file mode 100644 index 1890b1d4cee..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py +++ /dev/null @@ -1,107 +0,0 @@ -import clientapp -from flask import Flask, url_for -from typing import List -import json -from helper import FlaskBaseTestCase - - -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -def valid_client_configuration(): - return { - "client_id": "my-client-id", - "client_secret": "my-client-secret", - "op_metadata_url": "https://op.com/.well-known/openidconfiguration" - } - - -class TestConfigurationEndpoint(FlaskBaseTestCase): - def test_create_app_has_configuration(self): - self.assertTrue( - 'configuration' in app_endpoints(clientapp.create_app()), - 'endpoint /configuration does not exist in app') - - def test_configuration_endpoint_should_return_valid_req(self): - self.assertIn( - self.client.post(url_for('configuration')).status_code, - range(100, 511), '/configuration returned invalid requisition') - - def test_endpoint_should_return_200_if_valid_json(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - response = self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - self.assertEqual(response.status_code, 200) - - def test_endpoint_should_return_posted_data_if_valid_json(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - response = self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertEqual(json_data, json.dumps(response.json)) - - def test_endpoint_should_setup_cfg_with_provider_id(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertEqual(clientapp.cfg.PRE_SELECTED_PROVIDER_ID, 'whatever') - - def test_endpoint_should_setup_cfg_with_pre_selected_provider_true(self): - clientapp.cfg.PRE_SELECTED_PROVIDER = False - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertTrue(clientapp.cfg.PRE_SELECTED_PROVIDER, ) - - def test_endpoint_should_return_200_if_valid_client_config(self): - headers = {'Content-type': 'application/json'} - json_data = json.dumps(valid_client_configuration()) - response = self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertEqual(response.status_code, 200, - 'endpoint is NOT returning 200 for valid client configuration') - - def test_endpoint_should_register_new_oauth_client_id(self): - headers = {'Content-type': 'application/json'} - client_id = "my-client-id" - client_secret = "my-client-secret" - op_metadata_url = "https://op.com/.well-known/openidconfiguration" - json_data = json.dumps({ - "client_id": client_id, - "client_secret": client_secret, - "op_metadata_url": op_metadata_url - }) - self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertTrue(clientapp.oauth.op.client_id == client_id, - 'endpoint is NOT changing op.client_id') - - def test_endpoint_should_register_new_oauth_client_secret(self): - headers = {'Content-type': 'application/json'} - json_data = json.dumps(valid_client_configuration()) - client_secret = valid_client_configuration()['client_secret'] - self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertTrue(clientapp.oauth.op.client_secret == client_secret, - '%s is is not %s' % (clientapp.oauth.op.client_secret, client_secret)) diff --git a/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py b/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py deleted file mode 100644 index e02f909ea28..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py +++ /dev/null @@ -1,76 +0,0 @@ -from clientapp.utils import dcr_from_config -from clientapp import config as cfg -from unittest.mock import MagicMock, patch, mock_open -from unittest import TestCase -from clientapp.helpers.client_handler import ClientHandler -import helper -import json -import builtins - -class TestDrcFromConfig(TestCase): - - def setUp(self) -> None: - # stashing to restore on teardown - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - builtins.open = MagicMock(name='open') - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - - def test_if_setup_logging_exists(self): - assert hasattr(dcr_from_config, 'setup_logging') - - def test_if_static_variables_exists(self): - assert hasattr(dcr_from_config, 'OP_URL') - assert hasattr(dcr_from_config, 'REDIRECT_URIS') - - def test_if_static_variables_from_config(self): - assert dcr_from_config.OP_URL == cfg.ISSUER - assert dcr_from_config.REDIRECT_URIS == cfg.REDIRECT_URIS - - def test_register_should_be_calable(self): - assert callable(dcr_from_config.register), 'not callable' - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_register_should_call_ClientHandler(self): - dcr_from_config.register() - ClientHandler.__init__.assert_called_once() - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_register_should_call_ClientHandler_with_params(self): - dcr_from_config.register() - ClientHandler.__init__.assert_called_once_with( - cfg.ISSUER, cfg.REDIRECT_URIS, { - 'scope': cfg.SCOPE.split(" "), - "post_logout_redirect_uris": ['https://localhost:9090'] - } - ) - - def test_register_should_call_open(self): - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - - open_mock.assert_called_once() - - def test_register_should_call_open_with_correct_params(self): - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - open_mock.assert_called_once_with('client_info.json', 'w') - - def test_register_should_call_write_with_client_info(self): - client = ClientHandler(cfg.ISSUER, cfg.REDIRECT_URIS, {}) - expected_json_client_info = json.dumps(client.get_client_dict(), indent=4) - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - open_mock_handler = open_mock() - open_mock_handler.write.assert_called_once_with(expected_json_client_info) - - diff --git a/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py b/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py deleted file mode 100644 index 0f51b7cb596..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py +++ /dev/null @@ -1,277 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock -import inspect - -import clientapp.helpers.client_handler as client_handler -from typing import Optional -import helper -from oic.oauth2 import ASConfigurationResponse - - -ClientHandler = client_handler.ClientHandler - -# helper -def get_class_instance(op_url='https://t1.techno24x7.com', - client_url='https://mock.test.com', - additional_metadata={}): - client_handler_obj = ClientHandler(op_url, client_url, additional_metadata) - return client_handler_obj - - -class TestDynamicClientRegistration(TestCase): - - def setUp(self) -> None: - self.register_client_stash = ClientHandler.register_client - self.discover_stash = ClientHandler.discover - - @staticmethod - def mock_methods(): - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - - def restore_stashed_mocks(self): - ClientHandler.discover = self.discover_stash - ClientHandler.register_client = self.register_client_stash - - def test_if_json_exists(self): - self.assertTrue(hasattr(client_handler, 'json'), - 'json does not exists in client_handler') - - def test_if_json_is_from_json_package(self): - self.assertTrue(client_handler.json.__package__ == 'json', - 'json is not from json') - - # testing ClientHandler class - def test_if_ClientHandler_is_class(self): - self.assertTrue(inspect.isclass(ClientHandler)) - - def test_if_register_client_exists(self): - self.assertTrue(hasattr(ClientHandler, 'register_client'), - 'register_client does not exists in ClientHandler') - - def test_if_register_client_is_callable(self): - self.assertTrue(callable(ClientHandler.register_client), - 'register_client is not callable') - - def test_if_register_client_receives_params(self): - expected_args = ['self', 'op_data', 'redirect_uris'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.register_client).args == expected_args, - 'register_client does not receive expected args') - - def test_if_register_client_params_are_expected_type(self): - insp = inspect.getfullargspec(ClientHandler.register_client) - self.assertTrue( - insp.annotations['op_data'] == ASConfigurationResponse - and insp.annotations['redirect_uris'] == Optional[list[str]], - 'register_client is not receiving the right params') - - def test_if_class_has_initial_expected_attrs(self): - initial_expected_attrs = [ - '_ClientHandler__client_id', - '_ClientHandler__client_secret', - '_ClientHandler__redirect_uris', - '_ClientHandler__metadata_url', - '_ClientHandler__additional_metadata', - 'discover', # method - 'register_client' # method - ] - - self.assertTrue( - all(attr in ClientHandler.__dict__.keys() - for attr in initial_expected_attrs), - 'ClientHandler does not have initial attrs') - - def test_if_discover_exists(self): - self.assertTrue(hasattr(ClientHandler, 'discover'), - 'discover does not exists in ClientHandler') - - def test_if_discover_is_callable(self): - self.assertTrue(callable(ClientHandler.discover), - 'discover is not callable') - - def test_if_discover_receives_params(self): - expected_args = ['self', 'op_url'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.discover).args == expected_args, - 'discover does not receive expected args') - - def test_if_discover_params_are_expected_type(self): - insp = inspect.getfullargspec(ClientHandler.discover) - self.assertTrue( - insp.annotations['op_url'] == Optional[str], - 'discover is not receiving the right params') - - def test_discover_should_return_valid_dict(self): - """[Checks if returns main keys] - """ - - main_keys = { - 'issuer', 'authorization_endpoint', 'token_endpoint', - 'userinfo_endpoint', 'clientinfo_endpoint', - 'session_revocation_endpoint', 'end_session_endpoint', - 'revocation_endpoint', 'registration_endpoint' - } - - self.mock_methods() - op_data = ClientHandler.discover(ClientHandler, - 'https://t1.techno24x7.com') - self.assertTrue(main_keys <= set(op_data), - 'discovery return data does not have main keys') - self.restore_stashed_mocks() - - def test_if_get_client_dict_exists(self): - self.assertTrue(hasattr(ClientHandler, 'get_client_dict'), - 'get_client_dict does not exists in ClientHandler') - - def test_if_get_client_dict_is_callable(self): - self.assertTrue(callable(ClientHandler.get_client_dict), - 'get_client_dict is not callable') - - def test_if_get_client_dict_receives_params(self): - expected_args = ['self'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.get_client_dict).args == expected_args, - 'get_client_dict does not receive expected args') - - def test_client_id_should_return_something(self): - self.assertIsNotNone( - ClientHandler.get_client_dict(ClientHandler), - 'get_client_dict returning NoneType. It has to return something!') - - def test_get_client_dict_should_return_a_dict(self): - self.assertIsInstance(ClientHandler.get_client_dict(ClientHandler), - dict, 'get_client_dict is not returning a dict') - - def test_class_init_should_set_op_url(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - - client_handler_obj = get_class_instance(op_url) - - self.restore_stashed_mocks() - - self.assertEqual(client_handler_obj.__dict__['_ClientHandler__op_url'], - op_url) - - def test_class_init_should_set_redirect_uris(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - redirect_uris = 'https://mock.test.com/oidc_callback' - client_handler_obj = ClientHandler(op_url, redirect_uris, {}) - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__redirect_uris'], - redirect_uris) - - def test_class_init_should_set_metadata_url(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - - client_handler_obj = get_class_instance(op_url) - - self.restore_stashed_mocks() - - expected_metadata_url = op_url + '/.well-known/openid-configuration' - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__metadata_url'], - expected_metadata_url) - - def test_class_init_should_set_additional_params(self): - self.mock_methods() - expected_metadata = {'metakey1': 'meta value 1'} - client_handler_obj = get_class_instance(additional_metadata=expected_metadata) - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__additional_metadata'], - expected_metadata - ) - - def test_class_init_should_have_docstring(self): - self.assertTrue(ClientHandler.__init__.__doc__, - 'ClientHandler.__init__ has doc') - - def test_if_get_client_dict_return_expected_keys(self): - expected_keys = [ - 'op_metadata_url', - 'client_id', - 'client_secret', - ] - - self.mock_methods() - - client_handler_obj = get_class_instance() - client_dict = client_handler_obj.get_client_dict() - - self.restore_stashed_mocks() - - self.assertTrue( - all(key in client_dict.keys() for key in expected_keys), - 'there is no %s IN %s: get_client_dict is NOT returning expected keys' - % (str(expected_keys), str(client_dict.keys()))) - - def test_get_client_dict_values_cannot_be_none(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - client_handler_obj = get_class_instance(op_url) - client_dict = client_handler_obj.get_client_dict() - - self.restore_stashed_mocks() - - for key in client_dict.keys(): - self.assertIsNotNone(client_dict[key], - 'get_client_dict[%s] cannot be None!' % key) - - def test_get_client_dict_should_return_url_metadata_value(self): - self.mock_methods() - - client_handler_obj = get_class_instance() - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.get_client_dict()['op_metadata_url'], - client_handler_obj._ClientHandler__metadata_url) - - def test_get_client_dict_should_return_client_id_value(self): - self.mock_methods() - - client_handler_obj = get_class_instance() - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.get_client_dict()['client_id'], - client_handler_obj._ClientHandler__client_id - ) - - def test_init_should_call_discover_once(self): - self.mock_methods() - - get_class_instance() - - ClientHandler.discover.assert_called_once() - - self.restore_stashed_mocks() - - def test_init_should_call_register_client_once(self): - self.mock_methods() - - get_class_instance() - ClientHandler.register_client.assert_called_once() - - self.restore_stashed_mocks() - diff --git a/demos/jans-tent/tests/unit_integration/test_flask_factory.py b/demos/jans-tent/tests/unit_integration/test_flask_factory.py deleted file mode 100644 index 0813c3b1ad1..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_flask_factory.py +++ /dev/null @@ -1,93 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock -import clientapp -from flask import Flask -import os -import builtins -from clientapp.helpers.client_handler import ClientHandler -import helper - - -class TestFlaskApp(TestCase): - - def setUp(self) -> None: - self.stashed_add_config_from_json = clientapp.add_config_from_json - clientapp.cfg.CLIENT_ID = 'any-client-id-stub' - clientapp.cfg.CLIENT_SECRET = 'any-client-secret-stub' - clientapp.cfg.SERVER_META_URL = 'https://ophostname.com/server/meta/url' - clientapp.add_config_from_json = MagicMock(name='add_config_from_json') - clientapp.add_config_from_json.return_value(None) - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - builtins.open = MagicMock(name='open') - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - clientapp.add_config_from_json = self.stashed_add_config_from_json - - def test_create_app_should_exist(self): - self.assertEqual(hasattr(clientapp, 'create_app'), True, - 'app factory does not exists') - - def test_create_app_should_be_invokable(self): - self.assertEqual(callable(clientapp.create_app), True, - 'cannot invoke create_app from clientapp') - - def test_create_app_should_return_a_flask_app(self): - - self.assertIsInstance(clientapp.create_app(), Flask, - 'create_app is not returning a Flask instance') - - def test_if_app_has_secret_key(self): - self.assertTrue(hasattr(clientapp.create_app(), 'secret_key'), ) - - def test_if_secret_key_not_none(self): - self.assertIsNotNone(clientapp.create_app().secret_key, - 'app secret key is unexpectedly None') - - def test_if_oauth_is_app_extension(self): - self.assertTrue('authlib.integrations.flask_client' in - clientapp.create_app().extensions) - - def test_if_settings_py_exists(self): - self.assertTrue(os.path.exists('clientapp/config.py'), - 'File clientapp/config.py does not exist') - - def test_if_op_client_id_exists_in_app_configuration(self): - self.assertTrue('OP_CLIENT_ID' in clientapp.create_app().config, - 'No OP_CLIENT_ID in app.config') - - def test_if_clientapp_has_cfg(self): - self.assertTrue(hasattr(clientapp, 'cfg')) - - def test_if_cfg_is_module_from_configpy(self): - self.assertTrue( - os.path.relpath(clientapp.cfg.__file__) == 'clientapp/config.py') - - ... - - def test_if_OP_CLIENT_ID_is_equal_cfg_CLIENT_ID(self): - self.assertEqual(clientapp.create_app().config['OP_CLIENT_ID'], - clientapp.cfg.CLIENT_ID) - - def test_if_OP_CLIENT_SECRET_exists_in_app_configuration(self): - self.assertTrue('OP_CLIENT_SECRET' in clientapp.create_app().config, - 'No OP_CLIENT_SECRET in app.config') - - def test_if_OP_CLIENT_SECRET_is_equal_cfg_CLIENT_ID(self): - self.assertEqual(clientapp.create_app().config['OP_CLIENT_SECRET'], - clientapp.cfg.CLIENT_SECRET) - - def test_if_has_attr_ssl_verify(self): - self.assertTrue(hasattr(clientapp, 'ssl_verify'), - 'There is no ssl_verify in clientapp') - - def test_should_have_method_to_set_CA_CURL_CERT(self): - self.assertTrue(clientapp.ssl_verify.__call__) diff --git a/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py b/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py deleted file mode 100644 index 4f7fc9dc13f..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py +++ /dev/null @@ -1,46 +0,0 @@ -import clientapp -from helper import FlaskBaseTestCase - - -class TestPreselectedProvider(FlaskBaseTestCase): - - # """ - # We should be able to send Preselected passport provider to gluu OIDC as a authorization param - # like this: preselectedExternalProvider= - # Where is the Base64-encoded representation of a small JSON - # content that looking like this: - # { "provider" : } - # """ - def setUp(self): - clientapp.cfg.PRE_SELECTED_PROVIDER = True - FlaskBaseTestCase.setUp(FlaskBaseTestCase) - - def test_config_should_have_preselected_provider_option(self): - self.assertTrue(hasattr(clientapp.cfg, 'PRE_SELECTED_PROVIDER'), - 'cfg doesnt have PRE_SELECTED_PROVIDER attribute') - - def test_config_pre_selected_provider_should_be_boolean(self): - self.assertTrue( - type(clientapp.cfg.PRE_SELECTED_PROVIDER) == bool, - 'cfg.PRE_SELECTED_PROVIDER is not bool') - - def test_preselected_provider_id_should_exist_in_cfg(self): - self.assertTrue(hasattr(clientapp.cfg, 'PRE_SELECTED_PROVIDER_ID')) - - def test_clientapp_should_have_get_preselected_provider(self): - self.assertTrue( - hasattr(clientapp, 'get_preselected_provider'), - 'client app does not have get_preselected_provider attr') - - def test_get_preselected_provider_should_be_callable(self): - self.assertTrue(callable(clientapp.get_preselected_provider), - 'get_preselected_provider is not callable') - - def test_get_selected_provider_should_return_base64(self): - - clientapp.cfg.PRE_SELECTED_PROVIDER_ID = 'saml-emaillink' - expected_response = "eyAicHJvdmlkZXIiIDogInNhbWwtZW1haWxsaW5rIiB9" - self.assertEqual(clientapp.get_preselected_provider(), - expected_response) - - diff --git a/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py b/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py deleted file mode 100644 index c0bb85bfc87..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py +++ /dev/null @@ -1,65 +0,0 @@ -import clientapp -from helper import FlaskBaseTestCase, app_endpoints -from flask import url_for, session -from urllib import parse -from clientapp import config as cfg - -class TestLogoutEndpoint(FlaskBaseTestCase): - def authenticated_session_mock(self): - with self.client.session_transaction() as session: - session['id_token'] = 'id_token_stub' - - def test_endpoint_exists(self): - self.assertIn( - 'logout', - app_endpoints(clientapp.create_app()) - ) - - def test_endpoint_should_require_authentication(self): - ... - def test_logout_endpoint_should_redirect_to_home_if_unauthenticated(self): - # print(self.client.get(url_for('logout')).response) - response = self.client.get(url_for('logout')) - assert(response.status_code == 302) - assert(response.location == url_for('index')) - - - def test_logout_endpoint_should_clear_session(self): - with self.client.session_transaction() as sess: - sess['id_token'] = 'id_token_stub' - sess['user'] = 'userinfo stub' - - with self.client: - self.client.get(url_for('logout')) - assert 'id_token' not in session - assert 'user' not in session - - def test_endpoint_should_redirect_to_end_session_endpoint(self): - with self.client.session_transaction() as session: - session['id_token'] = 'id_token_stub' - session['user'] = 'userinfo stub' - - response = self.client.get(url_for('logout')) - - parsed_location = parse.urlparse(response.location) - assert parsed_location.scheme == 'https' - assert parsed_location.netloc == 'ophostname.com' - assert parsed_location.path == '/end_session_endpoint' - - - - def test_endpoint_should_redirect_to_end_session_endpoint_with_params(self): - token_stub = 'id_token_stub' - with self.client.session_transaction() as session: - session['id_token'] = token_stub - session['user'] = 'userinfo stub' - - parsed_redirect_uri = parse.urlparse(cfg.REDIRECT_URIS[0]) - post_logout_uri = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - - expected_query = 'post_logout_redirect_uri=%s&token_hint=%s' % (post_logout_uri, token_stub) - response = self.client.get(url_for('logout')) - - parsed_location = parse.urlparse(response.location) - assert parsed_location.query == expected_query - diff --git a/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py b/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py deleted file mode 100644 index 68f7012c88e..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py +++ /dev/null @@ -1,68 +0,0 @@ -from clientapp import create_app, session -from flask import Flask, url_for -from typing import List -from werkzeug import local -from helper import FlaskBaseTestCase - - -def app_endpoint(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -class TestProtectedContentEndpoint(FlaskBaseTestCase): - def test_app_should_contain_protected_content_route(self): - - endpoints = app_endpoint(create_app()) - self.assertIn('protected-content', endpoints, - 'protected-content route not found in app endpoints') - - def test_app_protected_content_route_should_return_valid_requisition(self): - - self.client.get(url_for('protected_content')) - - self.assertIn( - self.client.get(url_for('protected_content')).status_code, - range(100, 511), - 'protected content route returned invalid requisition') - - def test_should_return_if_session_exists_in_clientapp(self): - import clientapp - self.assertTrue(hasattr(clientapp, 'session'), - "session is not an attribute of clientapp") - del clientapp - - def test_should_check_if_session_is_LocalProxy_instance(self): - self.assertIsInstance(session, local.LocalProxy) - - def test_protected_content_return_status_200_ir_session_profile_exists( - self): - - with self.client.session_transaction() as sess: - sess['user'] = 'foo' - - self.assertEqual( - self.client.get(url_for('protected_content')).status_code, 200) - - def test_should_return_302_if_no_session_profile(self): - self.assertEqual( - self.client.get(url_for('protected_content')).status_code, 302) - - def test_protected_content_should_redirect_to_login_if_session_profile_doesnt_exist( - self): - - response = self.client.get(url_for('protected_content')) - self.assertTrue(response.location.endswith(url_for('login')), - 'Protected page is not redirecting to login page') - - ''' TODO - def test_should_return_if_user_logged_in_exists(self): - self.assertTrue( - hasattr(app,'user_logged_in') - ) - ''' diff --git a/docker-jans-all-in-one/Dockerfile b/docker-jans-all-in-one/Dockerfile index 6591e7a7424..86dbca99f41 100644 --- a/docker-jans-all-in-one/Dockerfile +++ b/docker-jans-all-in-one/Dockerfile @@ -58,7 +58,7 @@ RUN apk update \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-all-in-one/app/templates/nginx/jans-auth-location.conf b/docker-jans-all-in-one/app/templates/nginx/jans-auth-location.conf index 6b2fd017c11..04e8cd67693 100644 --- a/docker-jans-all-in-one/app/templates/nginx/jans-auth-location.conf +++ b/docker-jans-all-in-one/app/templates/nginx/jans-auth-location.conf @@ -1,3 +1,28 @@ +location /.well-known/authzen-configuration { + proxy_pass http://jans_auth_backend/jans-auth/restv1/authzen-configuration; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Port ""; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; + proxy_set_header Proxy ""; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_connect_timeout 300s; + proxy_send_timeout 300; + proxy_read_timeout 300; + send_timeout 300; + + proxy_redirect off; + port_in_redirect off; + proxy_http_version 1.1; +} + location /.well-known/openid-configuration { proxy_pass http://jans_auth_backend/jans-auth/.well-known/openid-configuration; proxy_set_header Host $http_host; diff --git a/docker-jans-all-in-one/app/templates/nginx/jans-fido2-location.conf b/docker-jans-all-in-one/app/templates/nginx/jans-fido2-location.conf index 2abf9b8295f..67edc8d3cf2 100644 --- a/docker-jans-all-in-one/app/templates/nginx/jans-fido2-location.conf +++ b/docker-jans-all-in-one/app/templates/nginx/jans-fido2-location.conf @@ -1,3 +1,28 @@ +location /.well-known/webauthn { + proxy_pass http://jans_fido2_backend/jans-fido2/restv1/webauthn/configuration; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Port ""; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; + proxy_set_header Proxy ""; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; + proxy_connect_timeout 300s; + proxy_send_timeout 300; + proxy_read_timeout 300; + send_timeout 300; + + proxy_redirect off; + port_in_redirect off; + proxy_http_version 1.1; +} + location /.well-known/fido2-configuration { proxy_pass http://jans_fido2_backend/jans-fido2/restv1/configuration; proxy_set_header Host $http_host; diff --git a/docker-jans-auth-server/Dockerfile b/docker-jans-auth-server/Dockerfile index 0e35b582bef..bacef4e943a 100644 --- a/docker-jans-auth-server/Dockerfile +++ b/docker-jans-auth-server/Dockerfile @@ -51,7 +51,7 @@ RUN /opt/jython/bin/pip uninstall -y pip setuptools # =========== ENV CN_VERSION=0.0.0-nightly -ENV CN_BUILD_DATE='2024-12-20 08:35' +ENV CN_BUILD_DATE='2025-01-13 16:11' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-server/${CN_VERSION}/jans-auth-server-${CN_VERSION}.war @@ -94,7 +94,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-auth/agama/fl \ /app/static/rdbm \ /app/schema -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -290,7 +290,8 @@ RUN chmod -R g=u ${JETTY_BASE}/jans-auth/custom \ && chown -R 1000:0 /opt/prometheus \ && chown 1000:0 ${JETTY_BASE}/jans-auth/webapps/jans-auth.xml \ && chown -R 1000:0 ${JETTY_HOME}/temp \ - && chown -R 1000:0 ${JETTY_BASE}/jans-auth/_libs + && chown -R 1000:0 ${JETTY_BASE}/jans-auth/_libs \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-auth-server/scripts/lock.py b/docker-jans-auth-server/scripts/lock.py index 637b17cb696..3a7a6f5b384 100644 --- a/docker-jans-auth-server/scripts/lock.py +++ b/docker-jans-auth-server/scripts/lock.py @@ -6,6 +6,8 @@ from string import Template from uuid import uuid4 +from ldif import LDIFWriter + from jans.pycloudlib import get_manager from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.utils import PersistenceMapper @@ -155,12 +157,42 @@ def ctx(self) -> dict[str, _t.Any]: @cached_property def ldif_files(self) -> list[str]: - return [ - "/app/templates/jans-lock/config.ldif", - "/app/templates/jans-lock/clients.ldif", - ] + filenames = ["config.ldif", "clients.ldif"] + + # generate extra scopes + self.generate_scopes_ldif() + filenames.append("scopes.ldif") + + return [f"/app/templates/jans-lock/{filename}" for filename in filenames] def import_ldif_files(self) -> None: for file_ in self.ldif_files: logger.info(f"Importing {file_}") self.client.create_from_ldif(file_, self.ctx) + + def generate_scopes_ldif(self): + # prepare required scopes (if any) + with open("/app/templates/jans-lock/scopes.json") as f: + scopes = json.loads(f.read()) + + with open("/app/templates/jans-lock/scopes.ldif", "wb") as fd: + writer = LDIFWriter(fd, cols=1000) + + for scope in scopes: + writer.unparse( + f"inum={scope['inum']},ou=scopes,o=jans", + { + "objectClass": ["top", "jansScope"], + "description": [scope["description"]], + "displayName": [scope["displayName"]], + "inum": [scope["inum"]], + "jansDefScope": [str(scope["jansDefScope"]).lower()], + "jansId": [scope["jansId"]], + "jansScopeTyp": [scope["jansScopeTyp"]], + "jansAttrs": [json.dumps({ + "spontaneousClientId": None, + "spontaneousClientScopes": [], + "showInConfigurationEndpoint": False, + })], + }, + ) diff --git a/docker-jans-auth-server/scripts/upgrade.py b/docker-jans-auth-server/scripts/upgrade.py index 70f9b9d889d..bf8080f62e3 100644 --- a/docker-jans-auth-server/scripts/upgrade.py +++ b/docker-jans-auth-server/scripts/upgrade.py @@ -62,6 +62,7 @@ def _transform_lock_dynamic_config(conf, manager): ], }), ("groupScopeEnabled", True), + ("statEnabled", True), ]: if missing_key not in conf: conf[missing_key] = value @@ -165,6 +166,8 @@ def invoke(self): if as_boolean(os.environ.get("CN_LOCK_ENABLED", "false")): self.update_lock_dynamic_config() self.update_lock_client_scopes() + self.update_lock_error_config() + self.update_lock_static_config() def update_lock_dynamic_config(self): kwargs = {"table_name": "jansAppConf"} @@ -235,6 +238,46 @@ def update_lock_client_scopes(self): entry.attrs["jansScope"] = client_scopes + diff self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + def update_lock_error_config(self): + kwargs = {"table_name": "jansAppConf"} + id_ = doc_id_from_dn("ou=jans-lock,ou=configuration,o=jans") + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + with contextlib.suppress(json.decoder.JSONDecodeError): + entry.attrs["jansConfErrors"] = json.loads(entry.attrs["jansConfErrors"]) + + with open("/app/templates/jans-lock/errors.json") as f: + conf = json.loads(f.read()) + + if conf != entry.attrs["jansConfErrors"]: + entry.attrs["jansConfErrors"] = json.dumps(conf) + entry.attrs["jansRevision"] += 1 + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + def update_lock_static_config(self): + kwargs = {"table_name": "jansAppConf"} + id_ = doc_id_from_dn("ou=jans-lock,ou=configuration,o=jans") + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + with contextlib.suppress(json.decoder.JSONDecodeError): + entry.attrs["jansConfStatic"] = json.loads(entry.attrs["jansConfStatic"]) + + with open("/app/templates/jans-lock/static-conf.json") as f: + conf = json.loads(f.read()) + + if conf != entry.attrs["jansConfStatic"]: + entry.attrs["jansConfStatic"] = json.dumps(conf) + entry.attrs["jansRevision"] += 1 + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + def main(): # noqa: D103 manager = get_manager() diff --git a/docker-jans-casa/Dockerfile b/docker-jans-casa/Dockerfile index 9f4beb98f2d..a143751e3f1 100644 --- a/docker-jans-casa/Dockerfile +++ b/docker-jans-casa/Dockerfile @@ -60,7 +60,7 @@ RUN mkdir -p /usr/share/java \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-certmanager/Dockerfile b/docker-jans-certmanager/Dockerfile index d14f8d2f8b8..aef95351f42 100644 --- a/docker-jans-certmanager/Dockerfile +++ b/docker-jans-certmanager/Dockerfile @@ -25,7 +25,7 @@ RUN wget -q ${CN_SOURCE_URL} -P /app/javalibs/ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-config-api/Dockerfile b/docker-jans-config-api/Dockerfile index ece0aef0c42..e69aac884ef 100644 --- a/docker-jans-config-api/Dockerfile +++ b/docker-jans-config-api/Dockerfile @@ -70,7 +70,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-config-api/_plugins \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources diff --git a/docker-jans-configurator/Dockerfile b/docker-jans-configurator/Dockerfile index 3b9acd83810..e59453273f0 100644 --- a/docker-jans-configurator/Dockerfile +++ b/docker-jans-configurator/Dockerfile @@ -27,7 +27,7 @@ RUN mkdir -p /opt/jans/configurator/javalibs \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG GIT_CLONE_DEPTH=100 RUN git clone --depth ${GIT_CLONE_DEPTH} --filter blob:none --no-checkout https://github.com/janssenproject/jans /tmp/jans \ diff --git a/docker-jans-configurator/scripts/bootstrap.py b/docker-jans-configurator/scripts/bootstrap.py index 95b8637276b..5c1f9a7c25c 100644 --- a/docker-jans-configurator/scripts/bootstrap.py +++ b/docker-jans-configurator/scripts/bootstrap.py @@ -476,6 +476,16 @@ def get_dump_file(): return f"{DB_DIR}/configuration.out.json" +def get_configuration_key_file(): + path = os.environ.get("CN_CONFIGURATOR_CONFIGURATION_KEY_FILE", "/etc/jans/conf/configuration.key") + + if os.path.isfile(path): + return path + + # backward-compat + return f"{DB_DIR}/configuration.key" + + # ============ # CLI commands # ============ @@ -501,7 +511,14 @@ def cli(): default=get_dump_file(), show_default=True, ) -def load(configuration_file, dump_file): +@click.option( + "--key-file", + type=click.Path(exists=False), + help="Absolute path to file contains key to decrypt configmaps and secrets (if applicable)", + default=get_configuration_key_file(), + show_default=True, +) +def load(configuration_file, dump_file, key_file): """Loads configmaps and secrets from JSON file (generate if not exist). """ deps = ["config_conn", "secret_conn"] @@ -517,7 +534,7 @@ def load(configuration_file, dump_file): with manager.create_lock("configurator-load"): logger.info(f"Loading configmaps and secrets from {configuration_file}") - params, err, code = load_schema_from_file(configuration_file) + params, err, code = load_schema_from_file(configuration_file, key_file=key_file) if code != 0: logger.error(f"Unable to load configmaps and secrets; reason={err}") raise click.Abort() diff --git a/docker-jans-fido2/Dockerfile b/docker-jans-fido2/Dockerfile index c5aad76a3b6..39e22e00a86 100644 --- a/docker-jans-fido2/Dockerfile +++ b/docker-jans-fido2/Dockerfile @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-fido2/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -243,7 +243,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-fido2/resources/log4j2.xml \ && chown -R 1000:0 /usr/share/java \ && chown -R 1000:0 /opt/prometheus \ && chown 1000:0 ${JETTY_BASE}/jans-fido2/webapps/jans-fido2.xml \ - && chown -R 1000:0 ${JETTY_HOME}/temp + && chown -R 1000:0 ${JETTY_HOME}/temp \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-kc-scheduler/Dockerfile b/docker-jans-kc-scheduler/Dockerfile index 686d061ed6b..6af6de15d85 100644 --- a/docker-jans-kc-scheduler/Dockerfile +++ b/docker-jans-kc-scheduler/Dockerfile @@ -38,7 +38,7 @@ RUN wget -q https://repo1.maven.org/maven2/org/codehaus/janino/janino/3.1.9/jani # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets @@ -160,7 +160,8 @@ RUN adduser -s /bin/sh -h /home/1000 -D -G root -u 1000 jans RUN chmod -R g=u /etc/certs \ && chmod -R g=u /etc/jans \ && chmod 664 /opt/java/lib/security/cacerts \ - && chown -R 1000:0 /opt/kc-scheduler + && chown -R 1000:0 /opt/kc-scheduler \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-keycloak-link/Dockerfile b/docker-jans-keycloak-link/Dockerfile index c8663bbedb4..c934ad88811 100644 --- a/docker-jans-keycloak-link/Dockerfile +++ b/docker-jans-keycloak-link/Dockerfile @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-keycloak-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -237,7 +237,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-keycloak-link/resources/log4j2.xml \ && chown -R 1000:0 /opt/prometheus \ && chown 1000:0 ${JETTY_BASE}/jans-keycloak-link/webapps/jans-keycloak-link.xml \ && chown -R 1000:0 /var/jans/cr-snapshots \ - && chown -R 1000:0 ${JETTY_HOME}/temp + && chown -R 1000:0 ${JETTY_HOME}/temp \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-link/Dockerfile b/docker-jans-link/Dockerfile index fb7381450bc..6b62e6cd1a3 100644 --- a/docker-jans-link/Dockerfile +++ b/docker-jans-link/Dockerfile @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -237,7 +237,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-link/resources/log4j2.xml \ && chown -R 1000:0 /opt/prometheus \ && chown 1000:0 ${JETTY_BASE}/jans-link/webapps/jans-link.xml \ && chown -R 1000:0 /var/jans/link-snapshots \ - && chown -R 1000:0 ${JETTY_HOME}/temp + && chown -R 1000:0 ${JETTY_HOME}/temp \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-monolith/Dockerfile b/docker-jans-monolith/Dockerfile index 069e406c4fc..4e0f74da233 100644 --- a/docker-jans-monolith/Dockerfile +++ b/docker-jans-monolith/Dockerfile @@ -42,7 +42,7 @@ EXPOSE 443 8080 1636 # jans-linux-setup # ===================== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 # cleanup RUN rm -rf /tmp/jans diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index bb662da7577..e62f60b0e32 100644 --- a/docker-jans-persistence-loader/Dockerfile +++ b/docker-jans-persistence-loader/Dockerfile @@ -16,7 +16,7 @@ RUN apk update \ # =========== # janssenproject/jans SHA commit -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCRIPT_CATALOG_DIR=docs/script-catalog ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources @@ -180,7 +180,8 @@ RUN adduser -s /bin/sh -h /home/1000 -D -G root -u 1000 1000 # adjust ownership and permission RUN chmod -R g=u /app/custom_ldif \ && chmod -R g=u /etc/certs \ - && chmod -R g=u /etc/jans + && chmod -R g=u /etc/jans \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-saml/Dockerfile b/docker-jans-saml/Dockerfile index 222834ca2bc..4f9d6805c8a 100644 --- a/docker-jans-saml/Dockerfile +++ b/docker-jans-saml/Dockerfile @@ -35,7 +35,7 @@ RUN wget -q https://jenkins.jans.io/maven/io/jans/kc-jans-spi/${CN_VERSION}/kc-j # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -203,7 +203,8 @@ RUN chmod -R g=u /etc/certs \ && chown -R 1000:0 /opt/idp \ && chown -R 1000:0 /usr/share/java \ && chown -R 1000:0 /opt/keycloak/logs \ - && chown -R 1000:0 /opt/keycloak/conf + && chown -R 1000:0 /opt/keycloak/conf \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docker-jans-scim/Dockerfile b/docker-jans-scim/Dockerfile index 5a3d48c1857..d84e7164169 100644 --- a/docker-jans-scim/Dockerfile +++ b/docker-jans-scim/Dockerfile @@ -60,7 +60,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-scim/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=856f9fed1d58a6d41503a0459bbe04f52b0bb8e7 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCIM_RESOURCE_DIR=jans-scim/server/src/main/resources @@ -237,7 +237,8 @@ RUN chmod 664 ${JETTY_BASE}/jans-scim/resources/log4j2.xml \ && chown -R 1000:0 /usr/share/java \ && chown -R 1000:0 /opt/prometheus \ && chown 1000:0 ${JETTY_BASE}/jans-scim/webapps/jans-scim.xml \ - && chown -R 1000:0 ${JETTY_HOME}/temp + && chown -R 1000:0 ${JETTY_HOME}/temp \ + && chown -R 1000:0 /app/templates USER 1000 diff --git a/docs/assets/agama/challenge-flow.png b/docs/assets/agama/challenge-flow.png new file mode 100644 index 00000000000..9cb8292887b Binary files /dev/null and b/docs/assets/agama/challenge-flow.png differ diff --git a/docs/casa/developer/overview.md b/docs/casa/developer/overview.md index 25827284d02..7515dbfb138 100644 --- a/docs/casa/developer/overview.md +++ b/docs/casa/developer/overview.md @@ -109,7 +109,7 @@ This is probably the most common requirement. Visit this [page](./add-authn-meth ### Other forms of customization -Most forms of customization can be tackled using flow cancellation. Through cancellation, a flow can be aborted while running and the control returned to one of its callers. Learn more about this topic [here](../../janssen-server/developer/agama/advanced-usages#cancellation). +Most forms of customization can be tackled using flow cancellation. Through cancellation, a flow can be aborted while running and the control returned to one of its callers. Learn more about this topic [here](../../janssen-server/developer/agama/advanced-usages.md#cancellation). As an example, let's assume you want to add a _"don't have an account? register here"_ button in the initial screen of Casa flow. Here's what you can do: diff --git a/docs/cedarling/cedarling-authz.md b/docs/cedarling/cedarling-authz.md index 03fec2f3145..89f3a5c7619 100644 --- a/docs/cedarling/cedarling-authz.md +++ b/docs/cedarling/cedarling-authz.md @@ -55,27 +55,30 @@ Action, Resource and Context is sent by the application in the authorization req this is a sample request from a hypothetical application: ```js -input = { - "access_token": "eyJhbGc....", - "id_token": "eyJjbGc...", - "userinfo_token": "eyJjbGc...", - "tx_token": "eyJjbGc...", - "action": "View", - "resource": { - "id": "ticket-10101", - "type" : "Ticket", - "owner": "bob@acme.com", - "org_id": "Acme" - }, - "context": { - "ip_address": "54.9.21.201", - "network_type": "VPN", - "user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)", - "time": "1719266610.98636", - } - } - -decision_result = authz(input) +const bootstrap_config = {...}; +const cedarling = await init(bootstrap_config); +let input = { + "tokens": { + "access_token": "eyJhbGc....", + "id_token": "eyJjbGc...", + "userinfo_token": "eyJjbGc...", + }, + "action": "View", + "resource": { + "id": "ticket-10101", + "type" : "Ticket", + "owner": "bob@acme.com", + "org_id": "Acme" + }, + "context": { + "ip_address": "54.9.21.201", + "network_type": "VPN", + "user_agent": "Chrome 125.0.6422.77 (Official Build) (arm64)", + "time": "1719266610.98636", + } + } + +decision_result = await cedarling(input) ``` ## Automatically Adding Entity References to the Context diff --git a/docs/cedarling/cedarling-logs.md b/docs/cedarling/cedarling-logs.md index 05cf27bf645..47c254073dc 100644 --- a/docs/cedarling/cedarling-logs.md +++ b/docs/cedarling/cedarling-logs.md @@ -95,6 +95,15 @@ Example of decision log. "Workload": { "org_id": "some_long_id" }, + "diagnostics": { + "reason": [ + { + "id": "840da5d85403f35ea76519ed1a18a33989f855bf1cf8", + "description": "policy for user" + } + ], + "errors": [] + }, "lock_client_id": null, "action": "Jans::Action::\"Update\"", "resource": "Jans::Issue::\"random_id\"", diff --git a/docs/cedarling/cedarling-wasm.md b/docs/cedarling/cedarling-wasm.md new file mode 100644 index 00000000000..b58712cf220 --- /dev/null +++ b/docs/cedarling/cedarling-wasm.md @@ -0,0 +1,228 @@ +--- +tags: + - cedarling + - wasm +--- + +# WASM for Cedarling + +Cedarling provides a binding for JavaScript programs via the `wasm-pack` tool. This allows browser developers to use the cedarling crate in their code directly. + +## Requirements + +- Rust 1.63 or greater +- Installed `wasm-pack` via `cargo` +- clang with `wasm` target support + +## Building + +- Install `wasm-pack` by: + + ```sh + cargo install wasm-pack + ``` + +- Build cedarling `wasm` in release: + + ```bash + wasm-pack build --release --target web + ``` + + `wasm-pack` automatically make optimization of `wasm` binary file, using `wasm-opt`. +- Get result in the `pkg` folder. + +## Including in projects + +For using result files in browser project you need make result `pkg` folder accessible for loading in the browser so that you can later import the corresponding file from the browser. + +Here is example of code snippet: + +```html + +``` + +## Usage + +Before usage make sure that you have completed `Building` steps. +You can find usage examples in the following locations: + +- `jans-cedarling/bindings/cedarling_wasm/index.html`: A simple example demonstrating basic usage. +- `jans-cedarling/bindings/cedarling_wasm/cedarling_app.html`: A fully featured `Cedarling` browser app where you can test and validate your configuration. + +### Defined API + +```ts +/** + * Create a new instance of the Cedarling application. + * This function can take as config parameter the eather `Map` other `Object` + */ +export function init(config: any): Promise; + +/** + * The instance of the Cedarling application. + */ +export class Cedarling { + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Object` + */ + static new(config: object): Promise; + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Map` + */ + static new_from_map(config: Map): Promise; + /** + * Authorize request + * makes authorization decision based on the [`Request`] + */ + authorize(request: any): Promise; + /** + * Get logs and remove them from the storage. + * Returns `Array` of `Map` + */ + pop_logs(): Array; + /** + * Get specific log entry. + * Returns `Map` with values or `null`. + */ + get_log_by_id(id: string): any; + /** + * Returns a list of all log ids. + * Returns `Array` of `String` + */ + get_log_ids(): Array; +} + +/** + * A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResult { + /** + * Convert `AuthorizeResult` to json string value + */ + json_string(): string; + /** + * Result of authorization where principal is `Jans::Workload` + */ + workload?: AuthorizeResultResponse; + /** + * Result of authorization where principal is `Jans::User` + */ + person?: AuthorizeResultResponse; + /** + * Result of authorization + * true means `ALLOW` + * false means `Deny` + * + * this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + */ + decision: boolean; +} + +/** + * A WASM wrapper for the Rust `cedar_policy::Response` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResultResponse { + /** + * Authorization decision + */ + readonly decision: boolean; + /** + * Diagnostics providing more information on how this decision was reached + */ + readonly diagnostics: Diagnostics; +} + +/** + * Diagnostics + * =========== + * + * Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation. + */ +export class Diagnostics { + /** + * `PolicyId`s of the policies that contributed to the decision. + * If no policies applied to the request, this set will be empty. + * + * The ids should be treated as unordered, + */ + readonly reason: (string)[]; + /** + * Errors that occurred during authorization. The errors should be + * treated as unordered, since policies may be evaluated in any order. + */ + readonly errors: (PolicyEvaluationError)[]; +} + +/** + * PolicyEvaluationError + * ===================== + * + * Represents an error that occurred when evaluating a Cedar policy. + */ +export class PolicyEvaluationError { + /** + * Id of the policy with an error + */ + readonly id: string; + /** + * Underlying evaluation error string representation + */ + readonly error: string; +} +``` diff --git a/docs/janssen-server/auth-server/oauth-features/dpop.md b/docs/janssen-server/auth-server/oauth-features/dpop.md index 9b8133278b3..3b5739455e6 100644 --- a/docs/janssen-server/auth-server/oauth-features/dpop.md +++ b/docs/janssen-server/auth-server/oauth-features/dpop.md @@ -102,12 +102,14 @@ recommended in the Following properties of the Janssen Server can be used to tailor the behavior concerning DPoP. -- [dpopJtiCacheTime](https://docs.jans.io/head/admin/reference/json/properties/janssenauthserver-properties/#dpopjticachetime) -- [dpopSigningAlgValuesSupported](https://docs.jans.io/head/admin/reference/json/properties/janssenauthserver-properties/#dpopsigningalgvaluessupported) -- [dpopTimeframe](https://docs.jans.io/head/admin/reference/json/properties/janssenauthserver-properties/#dpoptimeframe) -- [dpopUseNonce](https://docs.jans.io/head/admin/reference/json/properties/janssenauthserver-properties/#dpopusenonce) -- [dpopNonceCacheTime](https://docs.jans.io/head/admin/reference/json/properties/janssenauthserver-properties/#dpopnoncecachetime) -- [dpopJktForceForAuthorizationCode]((https://docs.jans.io/head/admin/reference/json/properties/janssenauthserver-properties/#dpopjktforceforauthorizationcode)) +- [dpopJtiCacheTime](../../../janssen-server/reference/json/properties/janssenauthserver-properties.md#dpopjticachetime) +- [dpopSigningAlgValuesSupported](../../../janssen-server/reference/json/properties/janssenauthserver-properties.md#dpopsigningalgvaluessupported) +- [dpopTimeframe](../../../janssen-server/reference/json/properties/janssenauthserver-properties.md#dpoptimeframe) +- [dpopUseNonce](../../../janssen-server/reference/json/properties/janssenauthserver-properties.md#dpopusenonce) +- [dpopNonceCacheTime](../../../janssen-server/reference/json/properties/janssenauthserver-properties.md#dpopnoncecachetime) +- [dpopJktForceForAuthorizationCode](../../../janssen-server/reference/json/properties/janssenauthserver-properties.md#dpopjktforceforauthorizationcode) + + ## Have questions in the meantime? diff --git a/docs/janssen-server/developer/agama/faq.md b/docs/janssen-server/developer/agama/faq.md index f5b763206c1..c7f85d29cf5 100644 --- a/docs/janssen-server/developer/agama/faq.md +++ b/docs/janssen-server/developer/agama/faq.md @@ -129,12 +129,6 @@ We plan to offer a debugger in the future. In the meantime, you can do `printf`- ## Miscellaneous -### Does the engine support AJAX? - -If you require a flow with no page refreshes, it could be implemented using AJAX calls as long as they align to the [POST-REDIRECT-GET](./advanced-usages.md#flow-advance-and-navigation) pattern, where a form is submitted, and as response a 302/303 HTTP redirection is obtained. Your Javascript code must also render UI elements in accordance with the data obtained by following the redirect (GET). Also, care must be taken in order to process server errors, timeouts, etc. In general, this requires a considerable amount of effort. - -If you require AJAX to consume a resource (service) residing in the same domain of your server, there is no restriction - the engine is not involved. Interaction with external domains may require to setup CORS configuration appropriately in the authentication server. - ### How to launch a flow? A flow is launched by issuing an authentication request in a browser as explained [here](./jans-agama-engine.md#launching-flows). @@ -195,3 +189,7 @@ Note the localization context (language, country, etc.) used in such a call is b ### Can Agama code be called from Java? No. These two languages are supposed to play roles that should not be mixed, check [here](./agama-best-practices.md#about-flow-design). + +### How to run flows from native applications instead of web browsers? + +There is a separate doc page covering this aspect [here](./native-applications.md). diff --git a/docs/janssen-server/developer/agama/native-applications.md b/docs/janssen-server/developer/agama/native-applications.md new file mode 100644 index 00000000000..e0688606cb7 --- /dev/null +++ b/docs/janssen-server/developer/agama/native-applications.md @@ -0,0 +1,494 @@ +--- +tags: + - developer + - agama + - native apps + - challenge endpoint +--- + +# Agama flows in native applications + +Agama is a framework primarily focused on web flows, however, with the [Authorization Challenge](../../../script-catalog/authorization_challenge/authorization-challenge.md) endpoint of Jans Server, developers can now run their flows outside the browser. This makes possible to offer secure, multi-step authentication flows from desktop and mobile applications without resorting to mechanisms like Web Views that substantially degrade the user experience. + +Additionally, the same already-familiar tools for authoring and deploying Agama projects can be used for the job. Moreover, the flows built for the web can be run in the native world without modification, requiring only to code the respective native UI and the logic that interacts with the Authorization Challenge endpoint, called "the endpoint" hereafter. + +In this document, we present an overview of how the endpoint works to make your Agama flows run without a web browser. Preliminar acquaintance with the following topics is recommended: + +- Agama [DSL](../../../agama/introduction.md#dsl) and `.gama` [format](../../../agama/gama-format.md) +- Agama projects [deployment](../../config-guide/auth-server-config/agama-project-configuration.md) in the Janssen Server +- [Execution rules](../../../agama/execution-rules.md) in the Jans Agama [engine](./jans-agama-engine.md) +- A basic understanding of [OAuth 2.0 for First-Party Applications](https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html) + +## How do flows actually run? + +Before getting into the technicalities, let's cover some key preliminar concepts. + +The engine - the piece of software that actually runs flows - is eminently driven by HTTP requests. This is unsurprising because the main "consumers" of the engine are web browsers. When targetting native apps, the engine remains the same, and flows still run at the server side. This means native apps won't hold any business logic, or make computations of significance. + +The [RRF](../../../agama/language-reference.md#rrf) (render-reply-fetch) Agama instruction is of paramount importance in flows. In a regular web setting, it involves three steps: + +- Injecting some data to a UI template in order to generate HTML markup. This is known as _rendering_ +- Reply the markup to the web browser - this will display a web page +- At the server side, retrieve data the user may have provided in his interaction with the page. This is, _fetch_ + +In a native setting no HTML markup is suppossed to be generated and replied - it's the app that is in charge of displaying the UI now. For this purpose, it will receive (from the endpoint) the data that would be originally injected into the template. Most of times, this will carry information gathered at earlier stages of the flow and that is relevant to properly show or update the UI. + +Likewise, the "data submission" for the _fetch_ phase of RRF is performed by the app too. In this case, the relevant data grabbed from the user interaction is sent to the server side (via challenge endpoint) and becomes the result of the RRF (the value for the variable on the left-hand side of the instruction). Note both the input ("injected" data) and the output (result) is specified in JSON format. + +Once the _fetch_ occurs, the flow proceeds its execution until another RRF instruction is hit, where the procedure described above takes place again. + +Note this approach has two big benefits: + +1. Regular web flows can be reused in the native world without modifications +1. The mindset for flows design remain the same + +There is a subtle exception regarding the first statement and has to do with flows containing RFAC instructions. [RFAC](../../../agama/language-reference.md#rfac) is used to redirect to external sites, and as such, it requires a web browser. In the case of native apps, flows will crash once an RFAC instruction is hit. + +### Inversion of control in apps + +The above concepts bring an important constraint to app design that should be accounted before undertaking any project: control is inverted. + +Normally, an app "knows" exactly what to do at each step of its workflow, and eventually delegates data retrieval tasks to the server side. When using the endpoint, the server side drives the logic: the app does not "take decisions" and instead "reacts" to the received data. This will be demostrated later through a practical example. + +## About the example: OTP via e-mail + +To avoid a rather abstract explanation, we'll use an example to illustrate the steps required to run a flow from a native app. Suppose an authentication flow operating in the following manner: + +- A username is prompted +- If the corresponding user has no e-mail associated to his account, the flow ends with an error message +- If the user has exactly one e-mail in his profile, a random one-time passcode (OTP) is sent to his registered address +- If the user has more than one e-mail, a screen is shown to pick the address where he would like the OTP be sent to +- The user is prompted to enter the passcode sent. If supplied correctly, the flow ends and the user is authenticated, otherwise the flow ends with an error + +This hypothetical flow is simple but will give you a good idea on how to interact with the endpoint. + +### The flow code + +The below depicts the implementation: + +![co.acme.flows.emailOtp](../../../assets/agama/challenge-flow.png) + + + +Flow `co.acme.flows.emailOtp` is self-explanatory and does not require further insight. Note the templates referenced in RRF directives don't necessarily have to exist, however, the template names will be included in the output of the endpoint as the flow executes. This serves as a hint or reference for the app to know the current point of execution and determine what should be shown in the UI. It will be more clearly seen in the next section. + +## Running the flow + +### Requisites + +To be able to run an Agama flow from a native app using the endpoint, it is required to register an OAuth Client in the Jans server with at least the `authorization_challenge` scope. The process of client registration is beyond the scope of this document. + +All HTTP requests exemplified here make use of `curl`. Ensure this tool is familiar to you. + +### Workflow + +Requests to the endpoint are all issued to the URL `https:///jans-auth/restv1/authorize-challenge` using the POST verb. Responses will contain JSON content whose structure will vary depending on the result of the operation as we will see. + +Once the first request is sent, the flow will start and all instructions will be executed until an RRF is found. Here the flow will be paused, and the endpoint will respond with the data that was passed to RRF: the template path and the "injected" data. Let's start issuing real requests now. + +### Initial request + +In the first request, at least the following parameters must be passed: + +|Name|Value| +|-|-| +|`acr_values`|agama_challenge| +|`use_auth_session`|true| +|`client_id`|The client identifier of a previously registered client| +|`flow_name`|The qualified name of the flow to launch| + +So in our example, it may look like: + +``` +curl -i -d acr_values=agama_challenge -d use_auth_session=true + -d flow_name=co.acme.flows.emailOtp -d client_id= + https:///jans-auth/restv1/authorize-challenge +``` + +!!! Note + This command, as all others following has been split into several lines for better readability. + +The response will look like: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_paused" + "flow_paused": { + "_template": "username-prompt.ftl" + }, + "auth_session": "BmAiCeArLdAa0", +} +``` + +While this may look like something wrong happened, it is not really the case. This is derived from the spec the endpoint adheres to, where the authorization server must report every intermediate response as an error with a 401 status code. + +The value of the `error` property references a section that contains the template path. Here it corresponds to the first RRF instruction reached in the execution (line 4 in the flow's code). Particularly this RRF was not invoked passing two parameters, so there is only one property inside the `flow_paused` JSON object. + +Note the presence of `auth_session`. This value allows the authorization server to associate subsequent requests issued by the app with this specific flow execution. + +Based on this response, the app should render UI elements in order to capture the username. Here, `username-prompt.ftl` serves as a hint for the app to know the point of execution the flow is at currently. + +### Subsequent requests + +From here onwards, requests must contain the following parameters: + +|Name|Value| +|-|-| +|`use_auth_session`|true| +|`auth_session`|The value obtained in the previous request| +|`data`|A JSON object value which will become the result of the RRF instruction the flow is paused at| + +!!! Note + Whenever a request is missing the `auth_session` param, it is assumed the [inital request](#initial-request) is being attempted. + +Let's assume the user entered `Joan` as username in the app. A request like the below can then be issued so the variable `obj` at line 4 is assigned a value: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + --data-urlencode data='{ "username": "Joan" }' + https:///jans-auth/restv1/authorize-challenge +``` + +This will make the flow advance until the next RRF is reached. Suppose the user Joan was found to have two e-mail addresses: `joan@doe.com` and `joan@deere.com`. This will make the flow hit line 23. The response will look as follows: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_paused" + "flow_paused": { + "_template": "email-prompt.ftl", + "addresses": [ "joan@doe.com", "joan@deere.com" ] + }, + "auth_session": "BmAiCeArLdAa0", +} +``` + +Note the `flow_paused` section has the contents of the object prepared in line 22. + +Based on this response, now the app should show a selection list for the user to pick one of these addresses. Once the selection is made, a new request can be issued: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + --data-urlencode data='{ "email": "joan@doe.com" }' + https:///jans-auth/restv1/authorize-challenge +``` + +The flow will continue and the hypothetical message will be sent to `joan@doe.com` (line 28). Then the next RRF is reached (line 32) and we get as response: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_paused" + "flow_paused": { + "_template": "passcode-prompt.ftl" + }, + "auth_session": "BmAiCeArLdAa0", +} +``` + +The app must now update the UI so the passcode is prompted. When ready, a new request comes: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + --data-urlencode data='{ "otp": "123456" }' + https:///jans-auth/restv1/authorize-challenge +``` + +Assuming the entered code (123456) was correct, the response would look like: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_finished", + "flow_finished": { + "data": { "userId": "Joan" }, + "success": true + }, + "auth_session": "efb10525-6c43-4e50-88ab-92461c258526" +} +``` + +This means we have hit line 37. + +When a `Finish` instruction is reached it is fully executed and the error reported in the response changes to `flow_finished`. What is left now is binding the user identified by `userId` (Joan) to the authorization request we have been handling (`BmAiCeArLdAa0`). This is how the user actually gets authenticated. + +### Final request + +To authenticate the user, we issue one last request: + +``` +curl -i -d auth_session=BmAiCeArLdAa0 -d use_auth_session=true + https:///jans-auth/restv1/authorize-challenge +``` + +Note parameter `data` is not needed. As response we obtain: + +``` +HTTP/1.1 200 OK +Content-Type: application/json +... + +{ "authorization_code" : "SplxlOBeZQQYbYS6WxSbIA" } + +``` + +Once an authorization code has been obtained, the app can request an access token. This topic is beyond the scope of this document. + +At this point, the app can update the UI giving the user access to the actual app contents. No more requests are expected to be received by the endpoint with the given `auth_session` value. + +## Understanding errors + +So far we have been following the "happy" path in the example flow where all assumptions are met. This is unrealistic so here we offer an overview of how the endpoint behaves when abnormal conditions come up. + +!!! Note + In this section, we stick to the terminology found [here](../../../agama/execution-rules.md#flows-lifecycle). + +### Missing parameters + +Assume the following request is issued: + +``` +curl -i -d use_auth_session=true -d acr_values=agama_challenge -d client_id= + https:///jans-auth/restv1/authorize-challenge +``` + +This lacks the name of the flow to launch. The response is: + +``` +HTTP/1.1 400 Bad Request +Content-Type: application/json +... + +{ + "error": "missing_param", + "missing_param": { "description": "Parameter 'flow_name' missing in request" } +} +``` + +### Failed flows + +Many times, flows simply fail as a way to reject access. This is achived in Agama by using code like: + +``` +obj = { success: false, error: "You are too suspicious" } +Finish obj +``` + +In this case, the response looks like: + +``` +HTTP/1.1 401 Unauthorized +Content-Type: application/json +... + +{ + "error": "flow_finished", + "flow_finished": { + "success": false, + "error": "You are too suspicious" + } +} +``` + +Note `auth_session` is not replied. As such, no more requests to the endpoint should be made passing the `auth_session` value obtained earlier. + +### Engine errors + +There are several conditions under which the engine produces errors. In these cases, the HTTP error emitted by the engine is included in the endpoint response. As in previous error scenarios, no `auth_session` is replied. + +#### Flow timeout + +With native apps, [timeout](./jans-agama-engine.md#how-timeouts-work) of flows obeys the same rules of the web scenario. The only difference is the server property employed for the timeout calculation, namely, `authorizationChallengeSessionLifetimeInSeconds`. If absent, it defaults to one day. + +Here is how a flow timeout is reported: + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "message": "You have exceeded the amount of time required to complete your authentication", + "timeout": true + }, + "contentType": "application/json", + "status": 410 + } +} +``` + +#### Crashed flow + +When a flow crashes, the error is reported in similar way the timeout is reported. Here are some examples: + +1. An attempt to access a property or index of a `null` variable in Agama code + + ``` + HTTP/1.1 500 Server Error + Content-Type: application/json + ... + + { + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "title": "An unexpected error ocurred", + "message": "TypeError: Cannot read property \"x\" from null" + }, + "contentType": "application/json", + "status": 500 + } + } + ``` + +1. A variable does not meet the expected shape for a given Agama directive + + ``` + HTTP/1.1 500 Server Error + Content-Type: application/json + ... + + { + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "title": "An unexpected error ocurred", + "message": "TypeError: Data passed to RRF was not a map or Java equivalent" + }, + "contentType": "application/json", + "status": 500 + } + } + ``` + +1. Indexing a string in Java beyond length + + ``` + HTTP/1.1 500 Server Error + Content-Type: application/json + ... + + { + + "error": "engine_error", + "engine_error": { + "description": "Unexpected response to https:///jans-auth/fl/...", + "body": { + "title": "An unexpected error ocurred", + "message": "String index out of range: 100" + }, + "contentType": "application/json", + "status": 500 + } + } + ``` + +### Other errors + +There are a variety of miscelaneous errors. Here we describe the most common. + +#### Finished flows with problems of user identification + +When a `Finish` instruction does not include a reference to a user identifier, or if the referenced user does not exist, the endpoint responds like: + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "unexpected_error", + "unexpected_error": { "description": "Unable to determine identity of user" } +} +``` + +#### Attempt to launch an unknown flow + +If the initial request references an inexisting flow or one that has been flagged as [not launchable directly](../../../agama/gama-format.md#metadata) by clients. + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "unexpected_error": {"description": "Flow ... does not exist or cannot be launched an application"}, + "error": "unexpected_error" +} +``` + +#### Agama is disabled + +If the Agama engine is disabled, the following is generated upon the first request: + +``` +HTTP/1.1 500 Server Error +Content-Type: application/json +... + +{ + "error": "unexpected_error", + "unexpected_error": { "description": "Agama engine is disabled" } +} +``` diff --git a/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md b/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md index 57d0a9597f6..df56dce9e18 100644 --- a/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md +++ b/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md @@ -387,7 +387,7 @@ Server deployment ## Test -1. [Setup](https://github.com/JanssenProject/jans/tree/main/demos/jans-tent) Janssen Tent +1. [Setup](https://github.com/JanssenProject/jans/tree/v1.2.0/demos/jans-tent) Janssen Tent 2. Change the configuration as given below in `config.py` ``` diff --git a/docs/janssen-server/reference/kubernetes/config-secret-keys.md b/docs/janssen-server/reference/kubernetes/config-secret-keys.md index d6a91c77504..e24d4e504b5 100644 --- a/docs/janssen-server/reference/kubernetes/config-secret-keys.md +++ b/docs/janssen-server/reference/kubernetes/config-secret-keys.md @@ -9,7 +9,10 @@ tags: ## Overview -The `config` job creates a set of configuration (contains `secrets` and `configmaps`) used by all Janssen services. +The `config` job creates a set of configurations (contains `secrets` and `configmaps`) used by all Janssen services. + +!!! Note + We assume Janssen is installed in a namespace called `jans` ## Configmaps @@ -27,7 +30,7 @@ Note that each key in configmaps is based on the schema below: { "city": { "type": "string", - "description": "Locality name (.e.g city)", + "description": "Locality name (e.g. city)", "example": "Austin" }, "country_code": { @@ -502,10 +505,8 @@ Note that each key in secrets is based on the schema below: ## Example decoding secrets ### Opening `base64-decoded` secrets -!!! Note - We assume Jans is installed in a namespace called `jans` -1. Get the `tls-certificate` from backend secret +1. Get the `tls-certificate` from the backend secret ```bash kubectl get secret tls-certificate -n jans -o yaml @@ -525,17 +526,22 @@ Note that each key in secrets is based on the schema below: ## Using Configuration Schema -As mentioned earlier, the `config` job creates configuration. Behind the scene, a Kubernetes' Secret object is created during the deployment to pre-populate `secrets` and `configmaps`. +As mentioned earlier, the `config` job creates a set of configurations. -### Default configuration +This happens by using a Kubernetes secret named `-configuration-file` that gets created during the helm chart installation. + +It contains a JSON schema with the necessary `secrets` and `configmaps` to install Janssen services. + +This secret is then mounted by the `config` job. -By default, the configuration only contains necessary `secrets` and `configmaps` to install Jans services. + +### Default configuration ```yaml apiVersion: v1 kind: Secret metadata: - name: jans-configuration-file + name: janssen-configuration-file namespace: jans labels: APP_NAME: configurator @@ -563,7 +569,7 @@ stringData: } ``` -Note that `_secret` may contain other keys depending on persistence, secrets/configmaps backend, etc. See examples below: +Note that `_secret` may contain other keys depending on the persistence used, the backend of the secrets/configmaps, etc. For example: 1. Secrets/configmaps backend is set to `google`: @@ -594,23 +600,22 @@ Note that `_secret` may contain other keys depending on persistence, secrets/con ### Custom configuration -The default configuration is sufficient for most of the time. If there's a requirement to use custom or reusing existing configuration, user may create a custom Kubernetes object. +The default configuration schema is sufficient for most of the time. However, if there's a requirement to use a custom configuration or reusing an existing configuration, you can create a Kubernetes secret with the custom configuration schema. !!! Warning The custom configuration schema is a BETA feature. -1. Prepare YAML file: +1. Prepare the YAML file containing the custom configuration schema. We will name it `custom-configuration-schema.yaml`: ```yaml - # custom-configuration-schema.yaml apiVersion: v1 kind: Secret metadata: - name: custom-configuration-file + name: custom-configuration-schema namespace: jans type: Opaque stringData: - custom-configuration.json: |- + configuration.json: |- { "_configmap": { "hostname": "demoexample.jans.io", @@ -628,19 +633,69 @@ The default configuration is sufficient for most of the time. If there's a requi } ``` -1. Create Kubernetes secrets: +1. Create the Kubernetes secret: ```bash - kubernetes -n jans create secret generic custom-configuration-schema --from-file=custom-configuration.json + kubectl -n jans apply -f custom-configuration-schema.yaml ``` 1. Specify the secret in `values.yaml`: ```yaml global: - cnConfiguratorConfigurationFile: /etc/jans/conf/custom-configuration.json cnConfiguratorCustomSchema: secretName: custom-configuration-schema ``` -1. Install the Jans charts. +1. Install the Janssen helm chart. + +## Encrypting Configuration Schema + +The encryption uses [Helm-specific](https://helm.sh/docs/chart_template_guide/function_list/#encryptaes) implementation of AES-256 CBC mode. + +### Default configuration + +The [default configuration](#default-configuration) schema can be encrypted by specifying 32 alphanumeric characters to `cnConfiguratorKey` attribute (the default value is an empty string). + +```yaml +global: + cnConfiguratorKey: "VMtVyFha8CfppdDGQSw8zEnfKXRvksAD" +``` + +The following example is what an encrypted default configuration looks like: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: janssen-configuration-file + namespace: jans +stringData: + configuration.json: |- + sxySo+redacted+generated+by+helm/TNpE5PoUR2+JxXiHiLq8X5ibexJcfjAN0fKlqRvU= +``` + +### Custom configuration + +If you are using a [custom configuration](#custom-configuration) schema, you will need to generate the string using [sprig-aes](https://pypi.org/project/sprig-aes/) CLI and paste it into a YAML manifest. + +```yaml +# custom-configuration-schema.yaml +apiVersion: v1 +kind: Secret +metadata: + name: custom-configuration-schema + namespace: jans +type: Opaque +stringData: + configuration.json: |- + sxySo+redacted+generated+by+sprigaes+JxXiHiLq8X5ibexJcfjAN0fKlqRvU= +``` + +Add the `key` used when encrypting using sprig-aes. + +```yaml +global: + cnConfiguratorKey: "VMtVyFha8CfppdDGQSw8zEnfKXRvksAD" +``` + diff --git a/docs/script-catalog/authorization_challenge/AgamaChallenge.java b/docs/script-catalog/authorization_challenge/AgamaChallenge.java new file mode 100644 index 00000000000..89fdade3277 --- /dev/null +++ b/docs/script-catalog/authorization_challenge/AgamaChallenge.java @@ -0,0 +1,317 @@ +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.session.AuthorizationChallengeSession; +import io.jans.as.server.authorize.ws.rs.AuthorizationChallengeSessionService; +import io.jans.as.server.service.UserService; +import io.jans.as.server.service.external.context.ExternalScriptContext; +import io.jans.model.SimpleCustomProperty; +import io.jans.model.custom.script.model.CustomScript; +import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType; +import io.jans.orm.PersistenceEntryManager; +import io.jans.service.cdi.util.CdiUtil; +import io.jans.service.custom.script.CustomScriptManager; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.jans.agama.engine.model.*; +import io.jans.agama.engine.misc.FlowUtils; +import io.jans.agama.engine.service.AgamaPersistenceService; +import io.jans.agama.NativeJansFlowBridge; +import io.jans.agama.engine.client.MiniBrowser; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.util.Base64Util; +import io.jans.util.*; + +import jakarta.servlet.ServletRequest; +import java.io.IOException; +import java.util.*; + +import org.json.*; + +import static io.jans.agama.engine.client.MiniBrowser.Outcome.*; + +public class AuthorizationChallenge implements AuthorizationChallengeType { + + //private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class); + private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class); + + private String finishIdAttr; + private MiniBrowser miniBrowser; + private PersistenceEntryManager entryManager; + private AuthorizationChallengeSessionService deviceSessionService; + + private boolean makeError(ExternalScriptContext context, AuthorizationChallengeSession deviceSessionObject, + boolean doRemoval, String errorId, JSONObject error, int status) { + + JSONObject jobj = new JSONObject(); + if (deviceSessionObject != null) { + + if (doRemoval) { + entryManager.remove(deviceSessionObject.getDn(), AuthorizationChallengeSession.class); + } else { + jobj.put("auth_session", deviceSessionObject.getId()); + } + } + + String errId = errorId.toLowerCase(); + jobj.put("error", errId); + jobj.put(errId, error); + + context.createWebApplicationException(status, jobj.toString(2) + "\n"); + return false; + + } + + private boolean makeUnexpectedError(ExternalScriptContext context, AuthorizationChallengeSession deviceSessionObject, + String description) { + + JSONObject jobj = new JSONObject(Map.of("description", description)); + return makeError(context, deviceSessionObject, true, "unexpected_error", jobj, 500); + + } + + private boolean makeMissingParamError(ExternalScriptContext context, String description) { + + JSONObject jobj = new JSONObject(Map.of("description", description)); + return makeError(context, null, false, "missing_param", jobj, 400); + + } + + private Pair prepareFlow(String sessionId, String flowName) { + + String msg = null; + try { + String qn = null, inputs = null; + + int i = flowName.indexOf("-"); + if (i == -1) { + qn = flowName; + } else if (i == 0) { + msg = "Flow name is empty"; + } else { + qn = flowName.substring(0, i); + scriptLogger.info("Parsing flow inputs"); + inputs = Base64Util.base64urldecodeToString(flowName.substring(i + 1)); + } + + if (qn != null) { + NativeJansFlowBridge bridge = CdiUtil.bean(NativeJansFlowBridge.class); + Boolean running = bridge.prepareFlow(sessionId, qn, inputs, true); + + if (running == null) { + msg = "Flow " + qn + " does not exist or cannot be launched from an application"; + } else if (running) { + msg = "Flow is already in course"; + } else { + return new Pair<>(bridge.getTriggerUrl(), null); + } + } + + } catch (Exception e) { + msg = e.getMessage(); + scriptLogger.error(msg, e); + } + return new Pair<>(null, msg); + + } + + private User extractUser(String userId) { + + UserService userService = CdiUtil.bean(UserService.class); + List matchingUsers = userService.getUsersByAttribute(finishIdAttr, userId, true, 2); + int matches = matchingUsers.size(); + + if (matches != 1) { + if (matches == 0) { + scriptLogger.warn("No user matches the required condition: {}={}", finishIdAttr, userId); + } else { + scriptLogger.warn("Several users match the required condition: {}={}", finishIdAttr, userId); + } + + return null; + } + return matchingUsers.get(0); + + } + + @Override + public boolean authorize(Object scriptContext) { + + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + + if (!CdiUtil.bean(FlowUtils.class).serviceEnabled()) + return makeUnexpectedError(context, null, "Agama engine is disabled"); + + if (!context.getAuthzRequest().isUseAuthorizationChallengeSession()) + return makeMissingParamError(context, "Please set 'use_auth_session=true' in your request"); + + ServletRequest servletRequest = context.getHttpRequest(); + AuthorizationChallengeSession deviceSessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject(); + + boolean noSO = deviceSessionObject == null; + scriptLogger.debug("There IS{} device session object", noSO ? " NO" : ""); + + Map deviceSessionObjectAttrs = null; + String sessionId = null, url = null, payload = null; + + if (noSO) { + + String fname = servletRequest.getParameter("flow_name"); + if (fname == null) + return makeMissingParamError(context, "Parameter 'flow_name' missing in request"); + + deviceSessionObject = deviceSessionService.newAuthorizationChallengeSession(); + sessionId = deviceSessionObject.getId(); + + Pair pre = prepareFlow(sessionId, fname); + url = pre.getFirst(); + + if (url == null) return makeUnexpectedError(context, deviceSessionObject, pre.getSecond()); + + deviceSessionObjectAttrs = deviceSessionObject.getAttributes().getAttributes(); + deviceSessionObjectAttrs.put("url", url); + deviceSessionObjectAttrs.put("client_id", servletRequest.getParameter("client_id")); + deviceSessionObjectAttrs.put("acr_values", servletRequest.getParameter("acr_values")); + deviceSessionObjectAttrs.put("scope", servletRequest.getParameter("scope")); + + deviceSessionService.persist(deviceSessionObject); + + } else { + sessionId = deviceSessionObject.getId(); + deviceSessionObjectAttrs = deviceSessionObject.getAttributes().getAttributes(); + String userId = deviceSessionObjectAttrs.get("userId"); + + if (userId != null) { + User user = extractUser(userId); + + if (user == null) + return makeUnexpectedError(context, deviceSessionObject, "Unable to determine identity of user"); + + context.getExecutionContext().setUser(user); + scriptLogger.debug("User {} is authenticated successfully", user.getUserId()); + + entryManager.remove(deviceSessionObject.getDn(), AuthorizationChallengeSession.class); + return true; + } + + url = deviceSessionObjectAttrs.get("url"); + if (url == null) + return makeUnexpectedError(context, deviceSessionObject, "Illegal state - url is missing in device session object"); + + payload = servletRequest.getParameter("data"); + if (payload == null) + return makeMissingParamError(context, "Parameter 'data' missing in request"); + } + + Pair p = miniBrowser.move(sessionId, url, payload); + MiniBrowser.Outcome result = p.getFirst(); + String strRes = result.toString(); + JSONObject jres = p.getSecond(); + + if (result == CLIENT_ERROR || result == ENGINE_ERROR) { + return makeError(context, deviceSessionObject, true, strRes, jres, 500); + + } else if (result == FLOW_PAUSED){ + url = p.getSecond().remove(MiniBrowser.FLOW_PAUSED_URL_KEY).toString(); + deviceSessionObjectAttrs.put("url", url); + deviceSessionService.merge(deviceSessionObject); + + scriptLogger.info("Next url will be {}", url); + return makeError(context, deviceSessionObject, false, strRes, jres, 401); + + } else if (result == FLOW_FINISHED) { + + try { + AgamaPersistenceService aps = CdiUtil.bean(AgamaPersistenceService.class); + FlowStatus fs = aps.getFlowStatus(sessionId); + + if (fs == null) + return makeUnexpectedError(context, deviceSessionObject, "Flow is not running"); + + FlowResult fr = fs.getResult(); + if (fr == null) + return makeUnexpectedError(context, deviceSessionObject, + "The flow finished but the resulting outcome was not found"); + + JSONObject jobj = new JSONObject(fr); + jobj.remove("aborted"); //just to avoid confusions and questions from users + + if (!fr.isSuccess()) { + scriptLogger.info("Flow DID NOT finished successfully"); + return makeError(context, deviceSessionObject, true, strRes, jobj, 401); + } + + String userId = Optional.ofNullable(fr.getData()).map(d -> d.get("userId")) + .map(Object::toString).orElse(null); + + if (userId == null) + return makeUnexpectedError(context, deviceSessionObject, "Unable to determine identity of user. " + + "No userId provided in flow result"); + + deviceSessionObjectAttrs.put("userId", userId); + deviceSessionService.merge(deviceSessionObject); + aps.terminateFlow(sessionId); + + return makeError(context, deviceSessionObject, false, strRes, jobj, 401); + + } catch (IOException e) { + return makeUnexpectedError(context, deviceSessionObject, e.getMessage()); + } + } else { + return makeUnexpectedError(context, deviceSessionObject, "Illegal state - unexpected outcome " + strRes); + } + + } + + @Override + public boolean init(Map configurationAttributes) { + scriptLogger.info("Initialized Agama AuthorizationChallenge Java custom script"); + return true; + } + + @Override + public boolean init(CustomScript customScript, Map configurationAttributes) { + + scriptLogger.info("Initialized Agama AuthorizationChallenge Java custom script."); + finishIdAttr = null; + String name = "finish_userid_db_attribute"; + SimpleCustomProperty prop = configurationAttributes.get(name); + + if (prop != null) { + finishIdAttr = prop.getValue2(); + if (StringHelper.isEmpty(finishIdAttr)) { + finishIdAttr = null; + } + } + + if (finishIdAttr == null) { + scriptLogger.info("Property '{}' is missing value", name); + return false; + } + scriptLogger.info("DB attribute '{}' will be used to map the identity of userId passed "+ + "in Finish directives (if any)", finishIdAttr); + + entryManager = CdiUtil.bean(PersistenceEntryManager.class); + deviceSessionService = CdiUtil.bean(AuthorizationChallengeSessionService.class); + miniBrowser = new MiniBrowser(CdiUtil.bean(AppConfiguration.class).getIssuer()); + return true; + + } + + @Override + public boolean destroy(Map configurationAttributes) { + scriptLogger.info("Destroyed Agama AuthorizationChallenge Java custom script."); + return true; + } + + @Override + public int getApiVersion() { + return 11; + } + + @Override + public Map getAuthenticationMethodClaims(Object context) { + return Map.of(); + } + +} diff --git a/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java b/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java index c0554577fed..1a7c02fb809 100644 --- a/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java +++ b/docs/script-catalog/authorization_challenge/AuthorizationChallenge.java @@ -221,4 +221,19 @@ public int getApiVersion() { public Map getAuthenticationMethodClaims(Object context) { return new HashMap<>(); } + + @Override + public void prepareAuthzRequest(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + final AuthorizationChallengeSession sessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject(); + if (sessionObject != null) { + final Map sessionAttributes = sessionObject.getAttributes().getAttributes(); + + // set scope from session into request object + final String scopeFromSession = sessionAttributes.get("scope"); + if (StringUtils.isNotBlank(scopeFromSession) && StringUtils.isBlank(context.getAuthzRequest().getScope())) { + context.getAuthzRequest().setScope(scopeFromSession); + } + } + } } \ No newline at end of file diff --git a/docs/script-catalog/authorization_challenge/authorization-challenge.md b/docs/script-catalog/authorization_challenge/authorization-challenge.md index 5c92608d8ca..07cfd20aa6d 100644 --- a/docs/script-catalog/authorization_challenge/authorization-challenge.md +++ b/docs/script-catalog/authorization_challenge/authorization-challenge.md @@ -44,12 +44,15 @@ The Authorization Challenage script implements the [AuthorizationChallenageType] |:-----|:------| |`def authorize(self, context)`| Called when the request is received. | |`def getAuthenticationMethodClaims(self, context)`| Called to get authn method claims. It is injected into `id_token`. Returns key-value map. | +|`def prepareAuthzRequest(self, context)`| Prepared authorization request before `authorize` method. It's good place to restore data from session if needed. | `authorize` method returns true/false which indicates to server whether to issue `authorization_code` in response or not. If parameters is not present then error has to be created and `false` returned. If all is good script has to return `true` and it's strongly recommended to set user `context.getExecutionContext().setUser(user);` so AS can keep tracking what exactly user is authenticated. +`prepareAuthzRequest` should typically be used for authorization request manipulation before `authorize` method is invoked. +Also if there is multi-step flow where some data are stored in `session` object, it is good place to restore data from session into request (please find example in sample below). ### Objects | Object name | Object description | @@ -458,6 +461,19 @@ public class AuthorizationChallenge implements AuthorizationChallengeType { @Override public Map getAuthenticationMethodClaims(Object context) { return new HashMap<>(); + } + + @Override + public void prepareAuthzRequest(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + final AuthorizationChallengeSession sessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject(); + if (sessionObject != null) { + final Map sessionAttributes = sessionObject.getAttributes().getAttributes(); + final String scopeFromSession = sessionAttributes.get("scope"); + if (StringUtils.isNotBlank(scopeFromSession) && StringUtils.isBlank(context.getAuthzRequest().getScope())) { + context.getAuthzRequest().setScope(scopeFromSession); + } + } } } diff --git a/docs/script-catalog/authorization_challenge/multi_step/AuthorizationChallenge.java b/docs/script-catalog/authorization_challenge/multi_step/AuthorizationChallenge.java index e310a0cb516..e64290519f6 100644 --- a/docs/script-catalog/authorization_challenge/multi_step/AuthorizationChallenge.java +++ b/docs/script-catalog/authorization_challenge/multi_step/AuthorizationChallenge.java @@ -196,4 +196,19 @@ public int getApiVersion() { public Map getAuthenticationMethodClaims(Object context) { return new HashMap<>(); } + + @Override + public void prepareAuthzRequest(Object scriptContext) { + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + final AuthorizationChallengeSession sessionObject = context.getAuthzRequest().getAuthorizationChallengeSessionObject(); + if (sessionObject != null) { + final Map sessionAttributes = sessionObject.getAttributes().getAttributes(); + + // set scope from session into request object + final String scopeFromSession = sessionAttributes.get("scope"); + if (StringUtils.isNotBlank(scopeFromSession) && StringUtils.isBlank(context.getAuthzRequest().getScope())) { + context.getAuthzRequest().setScope(scopeFromSession); + } + } + } } diff --git a/docs/script-catalog/person_authentication/other/fortinet/README.md b/docs/script-catalog/person_authentication/other/fortinet/README.md index 0d79f21d2c8..c1b44762186 100644 --- a/docs/script-catalog/person_authentication/other/fortinet/README.md +++ b/docs/script-catalog/person_authentication/other/fortinet/README.md @@ -5,7 +5,7 @@ This document explains how to configure the Gluu Server so that when a user logs ## Prerequisites -- A Gluu Server (installation instructions [here](../../../../janssen-server/install/)) which will play the role of RADIUS client +- A Gluu Server (installation instructions [here](../../../../janssen-server/install/README.md)) which will play the role of RADIUS client - The [Fortinet script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/fortinet/FortinetExternalAuthenticator.py) (included in the default Gluu Server distribution); - A Fortinet server which is the RADIUS server. - The jradius-client [jar library](https://sourceforge.net/projects/jradius-client/files/) added to oxAuth diff --git a/jans-auth-server/agama/engine/pom.xml b/jans-auth-server/agama/engine/pom.xml index 0a73dab51d0..e71e6898375 100644 --- a/jans-auth-server/agama/engine/pom.xml +++ b/jans-auth-server/agama/engine/pom.xml @@ -219,6 +219,15 @@ zip4j 2.11.5 + + + com.nimbusds + oauth2-oidc-sdk + + + org.json + json + @@ -237,11 +246,6 @@ ${project.version} test - - org.json - json - test - org.apache.logging.log4j diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java index c334e4ce8f2..439c8987227 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java @@ -58,7 +58,7 @@ public Boolean prepareFlow(String sessionId, String qname, String jsonInput, boo } if (st == null) { - int timeout = aps.getEffectiveFlowTimeout(qname); + int timeout = aps.getEffectiveFlowTimeout(qname, nativeClient); if (timeout <= 0) throw new Exception("Flow timeout negative or zero. " + "Check your AS configuration or flow definition"); long expireAt = System.currentTimeMillis() + 1000L * timeout; diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java new file mode 100644 index 00000000000..6859380f3e2 --- /dev/null +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/client/MiniBrowser.java @@ -0,0 +1,211 @@ +package io.jans.agama.engine.client; + +import com.nimbusds.oauth2.sdk.http.HTTPRequest; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; + +import io.jans.util.Pair; + +import jakarta.ws.rs.core.*; +import jakarta.ws.rs.core.Response.Status.Family; +import java.io.IOException; +import java.net.*; +import java.util.*; + +import org.json.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.nimbusds.oauth2.sdk.http.HTTPRequest.Method.*; +import static java.nio.charset.StandardCharsets.UTF_8; + +class WebResponse { + + private int status; + private String body; + private String contentType; + + private WebResponse() { } + + static WebResponse from(HTTPResponse response) { + + WebResponse wr = new WebResponse(); + wr.status = response.getStatusCode(); + wr.contentType = response.getHeaderValue(HttpHeaders.CONTENT_TYPE); + wr.body = response.getBody(); + return wr; + + } + + public int getStatus() { + return status; + } + + public String getBody() { + return body; + } + + public String getContentType() { + return contentType; + } + +} + +/** + * A micro HTTP client capable of interacting with the Agama engine in order to run JSON-based + * flows. It lessens the effort of exchanging messages with the engine and serves as a utility + * to make possible the fact of running Agama flows in native applications.
+ * The 'move' method implements the POST-REDIRECT-GET pattern of the engine: at every step, the + * URL passed is supplied with the given JSON contents, and the HTTP redirect is followed. The + * method returns one of the possible outcomes (see Outcome enum) plus some JSON data.
+ * This is an explanation of outcomes: CLIENT_ERROR (problems to connect to the URL or read the + * response), ENGINE_ERROR (the flow crashed, timed out, or an RFAC instruction was reached), + * FLOW_FINISHED (a Finish instruction was executed), and FLOW_PAUSED (RRF instruction was hit).
+ * The JSON data returned by 'move' contains error data (CLIENT_ERROR or ENGINE_ERROR), the data + * associated to the Finish instruction (FLOW_FINISHED), or the data supplied to the RRF instruction + * (FLOW_PAUSED). Only in the last case the method may receive a subsequent invocation, where the + * JSON data to supply is supposed to emulate the output of the RRF execution, that is, the result + * of having submitted a UI form in the app (desktop or mobile).
+ * Thus, it is the native app that takes charge of the UI rendering by receiving the same data + * the equivalent Freemarker template would receive (in the web world), and the data of the form + * submission is built by the native app too. In this case the path to the UI template (in RRF) + * has no effect, but it is anyways included in the output of 'move' for reference. + */ +public class MiniBrowser { + + public enum Outcome { CLIENT_ERROR, ENGINE_ERROR, FLOW_FINISHED, FLOW_PAUSED } + + public static final String FLOW_PAUSED_URL_KEY = "_url"; + + private static final Logger logger = LoggerFactory.getLogger(MiniBrowser.class); + + private String rootUrl; + private int connectionTimeout; + private int readTimeout; + private int maxErrorContentLength; + + public MiniBrowser(String rootUrl) { + this(rootUrl, 3500, 3500, 4096); + } + + public MiniBrowser(String rootUrl, + int connectionTimeout, int readTimeout, int maxErrorContentLength) { + + this.rootUrl = rootUrl; + this.connectionTimeout = connectionTimeout; + this.readTimeout = readTimeout; + this.maxErrorContentLength = maxErrorContentLength; + + } + + public Pair move(String phantomSid, String relativeUrl, String jsonPayload) { + + try { + return moveImpl(phantomSid, relativeUrl, jsonPayload); + } catch (Exception e) { + String error = e.getMessage(); + logger.error(error, e); + + JSONObject jobj = new JSONObject(Map.of("description", error)); + return new Pair<>(Outcome.CLIENT_ERROR, jobj); + } + + } + + private Pair moveImpl(String phantomSid, String relativeUrl, String jsonPayload) + throws Exception { + + String error = null; + String url = normalize(relativeUrl); + logger.info("Moving forward from {}", url); + + HTTPResponse response = sendRequest(phantomSid, new URL(url), jsonPayload); + WebResponse wr = WebResponse.from(response); + int status = wr.getStatus(); + + if (Family.familyOf(status).equals(Family.REDIRECTION)) { + String location = response.getHeaderValue(HttpHeaders.LOCATION); + + if (location != null) { + wr = null; + logger.info("Redirecting to {}", location); + + response = sendRequest(phantomSid, new URL(normalize(location)), null); + wr = WebResponse.from(response); + + if (MediaType.APPLICATION_JSON.equals(wr.getContentType()) && wr.getStatus() == 200) { + + logger.info("Returning JSON contents"); + JSONObject jobj = new JSONObject(wr.getBody()); + + jobj.put(FLOW_PAUSED_URL_KEY, location); + return new Pair<>(Outcome.FLOW_PAUSED, jobj); + } + + error = "Expecting OK JSON response for " + location; + + } else { + error = "Target of redirection is missing"; + } + } else if (MediaType.APPLICATION_JSON.equals(wr.getContentType()) && status == 200) { + + logger.info("Seems to have landed to the finish page"); + JSONObject jobj = new JSONObject(wr.getBody()); + + if (jobj.has("success")) return new Pair<>(Outcome.FLOW_FINISHED, jobj); + + error = "Unexpected response to " + url; + + } else { + error = "Unexpected response to " + url; + } + + logger.error(error); + JSONObject jobj = new JSONObject(Map.of("description", error)); + + String contentType = wr.getContentType(); + jobj.put("status", wr.getStatus()); + jobj.put("contentType", Optional.ofNullable((Object) contentType).orElse(JSONObject.NULL)); + + String body = wr.getBody(); + if (body == null) { + jobj.put("body", JSONObject.NULL); + } else if (MediaType.APPLICATION_JSON.equals(contentType)) { + jobj.put("body", new JSONObject(body)); + } else { + body = body.substring(0, Math.min(body.length(), maxErrorContentLength)); + jobj.put("body", body); + } + + return new Pair<>(Outcome.ENGINE_ERROR, jobj); + + } + + private HTTPResponse sendRequest(String phantomSid, URL url, String jsonPayload) throws IOException { + + boolean noPayload = jsonPayload == null; + HTTPRequest request = new HTTPRequest(noPayload ? GET : POST, url); + request.setConnectTimeout(connectionTimeout); + request.setReadTimeout(readTimeout); + //Ideally, redirects should be followed, but cookies are lost between requests :( + request.setFollowRedirects(false); + //... and without following redirects, the content-type has to be passed at every request :( + + //the presence of this header signals the engine not to read the incoming data as application/x-www-form-urlencoded + //and also to use the json version of engine's error pages + request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + //sending the session_id cookie helps maintain the state of the running flow between server/client + request.setHeader​(HttpHeaders.COOKIE, String.format("session_id=%s;", phantomSid)); + + if (!noPayload) { + request.setBody(jsonPayload); + } + return request.send(); + + } + + private String normalize(String relativeUrl) { + String url = relativeUrl.startsWith(rootUrl) ? "" : rootUrl; + return url + relativeUrl; + } + +} diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java index 89e95632222..659a6de7da5 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java @@ -111,19 +111,25 @@ public boolean flowEnabled(String flowName) { } - public int getEffectiveFlowTimeout(String flowName) { + public int getEffectiveFlowTimeout(String flowName, boolean nativeClient) { Flow fl = entryManager.findEntries(AGAMA_FLOWS_BASE, Flow.class, Filter.createEqualityFilter(Flow.ATTR_NAMES.QNAME, flowName), new String[]{ Flow.ATTR_NAMES.META }, 1).get(0); - int unauth = appConfiguration.getSessionIdUnauthenticatedUnusedLifetime(); + int unauth = appConfiguration.getSessionIdUnauthenticatedUnusedLifetime(); + if (nativeClient) { + unauth = Optional.ofNullable( + appConfiguration.getAuthorizationChallengeSessionLifetimeInSeconds()) + .orElse(unauth); + } + Integer flowTimeout = fl.getMetadata().getTimeout(); int timeout = Optional.ofNullable(flowTimeout).map(Integer::intValue).orElse(unauth); return Math.min(unauth, timeout); } - + public Flow getFlow(String flowName, boolean full) throws IOException { try { diff --git a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java index acb5ae64f19..c8f799c9f30 100644 --- a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java +++ b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/BaseTest.java @@ -155,11 +155,11 @@ void validateFinishPage(HtmlPage page, boolean success) { } void assertOK(Page page) { - assertEquals(page.getWebResponse().getStatusCode(), WebResponse.OK); + assertEquals(page.getWebResponse().getStatusCode(), 200); } void assertServerError(Page page) { - assertEquals(page.getWebResponse().getStatusCode(), WebResponse.INTERNAL_SERVER_ERROR); + assertEquals(page.getWebResponse().getStatusCode(), 500); } void assertTextContained(String text, String ...words) { diff --git a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java index 83ce021096a..09c64fa2d65 100644 --- a/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java +++ b/jans-auth-server/agama/engine/src/test/java/io/jans/agama/test/CustomConfigsFlowTest.java @@ -25,10 +25,10 @@ public void withTimeout() { int status = page.getWebResponse().getStatusCode(); String text = page.getVisibleText().toLowerCase(); - if (status == WebResponse.OK) { + if (status == 410) { //See timeout.ftlh assertTextContained(text, "took", "more", "expected"); - } else if (status == WebResponse.NOT_FOUND) { + } else if (status == 404) { //See mismatch.ftlh assertTextContained(text, "not", "found"); } else { diff --git a/jans-auth-server/server/conf/jans-config.json b/jans-auth-server/server/conf/jans-config.json index 04b5dde5654..1c50a375ba0 100644 --- a/jans-auth-server/server/conf/jans-config.json +++ b/jans-auth-server/server/conf/jans-config.json @@ -20,7 +20,8 @@ "par", "ssa", "global_token_revocation", - "status_list" + "status_list", + "access_evaluation" ], "issuer":"${config.oxauth.issuer}", "loginPage":"${config.oxauth.contextPath}/login.htm", diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java index e6543f948e4..5d71f5aa3dd 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java @@ -114,6 +114,8 @@ public Response requestAuthorization(AuthzRequest authzRequest) { public void prepareAuthzRequest(AuthzRequest authzRequest) { authzRequest.setScope(ServerUtil.urlDecode(authzRequest.getScope())); + externalAuthorizationChallengeService.externalPrepareAuthzRequest(authzRequest); + if (StringUtils.isNotBlank(authzRequest.getAuthorizationChallengeSession())) { final AuthorizationChallengeSession session = authorizationChallengeSessionService.getAuthorizationChallengeSession(authzRequest.getAuthorizationChallengeSession()); @@ -158,14 +160,14 @@ public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBi executionContext.setSessionId(sessionUser); if (user == null) { - log.trace("Executing external authentication challenge"); + log.trace("Executing external authentication challenge ... (requestedScopes: {})", scopes); final boolean ok = externalAuthorizationChallengeService.externalAuthorize(executionContext); if (!ok) { log.debug("Not allowed by authorization challenge script, client_id {}.", client.getClientId()); throw new WebApplicationException(errorResponseFactory - .newErrorResponse(Response.Status.BAD_REQUEST) - .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, state, "No allowed by authorization challenge script.")) + .newErrorResponse(Response.Status.UNAUTHORIZED) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, state, "Not allowed by authorization challenge script.")) .build()); } @@ -179,6 +181,8 @@ public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBi String grantAcr = executionContext.getScript() != null ? executionContext.getScript().getName() : authzRequest.getAcrValues(); + log.trace("Creating authorization code grant with: scope {}, acr {}", scopes, grantAcr); + AuthorizationCodeGrant authorizationGrant = authorizationGrantList.createAuthorizationCodeGrant(user, client, new Date()); authorizationGrant.setNonce(authzRequest.getNonce()); authorizationGrant.setJwtAuthorizationRequest(authzRequest.getJwtRequest()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java index b86ef6aa28f..c73914ab508 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java @@ -43,6 +43,7 @@ public class CacheGrant implements Serializable { private String acrValues; private String sessionDn; private int expiresIn = 1; + private boolean isAuthorizationChallenge; // CIBA private String authReqId; @@ -73,6 +74,7 @@ public CacheGrant(AuthorizationGrant grant, AppConfiguration appConfiguration) { codeChallengeMethod = grant.getCodeChallengeMethod(); claims = grant.getClaims(); sessionDn = grant.getSessionDn(); + isAuthorizationChallenge = grant.isAuthorizationChallenge(); } public CacheGrant(CIBAGrant grant, AppConfiguration appConfiguration) { @@ -263,6 +265,7 @@ public AuthorizationCodeGrant asCodeGrant(Instance g grant.setAcrValues(acrValues); grant.setNonce(nonce); grant.setClaims(claims); + grant.setAuthorizationChallenge(isAuthorizationChallenge); return grant; } @@ -335,11 +338,12 @@ public String getDeviceCode() { @Override public String toString() { - return "MemcachedGrant{" + + return "CacheGrant{" + "authorizationCode=" + authorizationCodeString + ", user=" + user + ", client=" + client + ", authenticationTime=" + authenticationTime + + ", isAuthorizationChallenge=" + isAuthorizationChallenge + '}'; } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java index 32d93f7070c..afe6ecb1505 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java @@ -3,6 +3,7 @@ import io.jans.as.model.authorize.AuthorizeErrorResponseType; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.authorize.ws.rs.AuthzRequest; import io.jans.as.server.model.common.ExecutionContext; import io.jans.as.server.service.external.context.ExternalScriptContext; import io.jans.model.custom.script.CustomScriptType; @@ -104,6 +105,10 @@ public boolean externalAuthorize(ExecutionContext executionContext) { } catch (Exception ex) { log.error(ex.getMessage(), ex); saveScriptError(script.getCustomScript(), ex); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, executionContext.getAuthzRequest().getState(), "Unable to run authorization challenge script.")) + .build()); } log.trace("Finished 'authorize' method, script name: {}, clientId: {}, result: {}", script.getName(), executionContext.getAuthzRequest().getClientId(), result); @@ -130,4 +135,45 @@ public CustomScriptConfiguration identifyScript(List acrValues) { log.trace("Unable to find script by acr_values {}", acrValues); return getCustomScriptConfigurationByName(appConfiguration.getAuthorizationChallengeDefaultAcr()); } + + public void externalPrepareAuthzRequest(AuthzRequest authzRequest) { + final List acrValues = authzRequest.getAcrValuesList(); + final CustomScriptConfiguration script = identifyScript(acrValues); + if (script == null) { + String msg = String.format("Unable to identify script by acr_values %s.", acrValues); + log.debug(msg); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, authzRequest.getState(), msg)) + .build()); + } + + log.trace("Executing python 'prepareAuthzRequest' method, script name: {}, clientId: {}, scope: {}, authorizationChallengeSession: {}", + script.getName(), authzRequest.getClientId(), authzRequest.getScope(), authzRequest.getAuthorizationChallengeSession()); + + ExecutionContext executionContext = ExecutionContext.of(authzRequest); + executionContext.setScript(script); + + try { + AuthorizationChallengeType authorizationChallengeType = (AuthorizationChallengeType) script.getExternalType(); + final ExternalScriptContext scriptContext = new ExternalScriptContext(executionContext); + authorizationChallengeType.prepareAuthzRequest(scriptContext); + + scriptContext.throwWebApplicationExceptionIfSet(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace("WebApplicationException from script", e); + } + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, executionContext.getAuthzRequest().getState(), "Unable to run 'prepareAuthzRequest' method authorization challenge script.")) + .build()); + } + + log.trace("Finished 'prepareAuthzRequest' method, script name: {}, clientId: {}", script.getName(), executionContext.getAuthzRequest().getClientId()); + } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java index b32651ec9f2..e51cf97022c 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java @@ -442,13 +442,14 @@ private Response processAuthorizationCode(String code, String scope, String code executionContext.setGrant(authorizationCodeGrant); log.trace("AuthorizationCodeGrant : '{}'", authorizationCodeGrant); + // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code + tokenRestWebServiceValidator.validateGrant(authorizationCodeGrant, client, code, executionContext.getAuditLog(), grant -> grantService.removeAllByAuthorizationCode(code)); + // validate redirectUri only for Authorization Code Flow. For First-Party App redirect uri is blank. It is perfectly valid case. + // redirect uri must be validated after grant is validated if (!authorizationCodeGrant.isAuthorizationChallenge()) { tokenRestWebServiceValidator.validateRedirectUri(redirectUri, executionContext.getAuditLog()); } - - // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code - tokenRestWebServiceValidator.validateGrant(authorizationCodeGrant, client, code, executionContext.getAuditLog(), grant -> grantService.removeAllByAuthorizationCode(code)); tokenRestWebServiceValidator.validatePKCE(authorizationCodeGrant, codeVerifier, executionContext.getAuditLog()); dPoPService.validateDpopThumprint(authorizationCodeGrant.getDpopJkt(), executionContext.getDpop()); diff --git a/jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js b/jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js new file mode 100644 index 00000000000..91bcb117205 --- /dev/null +++ b/jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.12.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +!function(){"use strict";var c={},l={};try{"undefined"!=typeof window&&(c=window),"undefined"!=typeof document&&(l=document)}catch(c){}var h=(c.navigator||{}).userAgent,z=void 0===h?"":h,v=c,a=l,m=(v.document,!!a.documentElement&&!!a.head&&"function"==typeof a.addEventListener&&a.createElement,~z.indexOf("MSIE")||z.indexOf("Trident/"),"___FONT_AWESOME___"),s=function(){try{return!0}catch(c){return!1}}();var e=v||{};e[m]||(e[m]={}),e[m].styles||(e[m].styles={}),e[m].hooks||(e[m].hooks={}),e[m].shims||(e[m].shims=[]);var t=e[m];function M(c,z){var l=(2>>0;h--;)l[h]=c[h];return l}function gc(c){return c.classList?bc(c.classList):(c.getAttribute("class")||"").split(" ").filter(function(c){return c})}function Ac(c,l){var h,z=l.split("-"),v=z[0],a=z.slice(1).join("-");return v!==c||""===a||(h=a,~T.indexOf(h))?null:a}function Sc(c){return"".concat(c).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function yc(h){return Object.keys(h||{}).reduce(function(c,l){return c+"".concat(l,": ").concat(h[l],";")},"")}function wc(c){return c.size!==Lc.size||c.x!==Lc.x||c.y!==Lc.y||c.rotate!==Lc.rotate||c.flipX||c.flipY}function kc(c){var l=c.transform,h=c.containerWidth,z=c.iconWidth,v={transform:"translate(".concat(h/2," 256)")},a="translate(".concat(32*l.x,", ").concat(32*l.y,") "),m="scale(".concat(l.size/16*(l.flipX?-1:1),", ").concat(l.size/16*(l.flipY?-1:1),") "),s="rotate(".concat(l.rotate," 0 0)");return{outer:v,inner:{transform:"".concat(a," ").concat(m," ").concat(s)},path:{transform:"translate(".concat(z/2*-1," -256)")}}}var xc={x:0,y:0,width:"100%",height:"100%"};function Zc(c){var l=!(1").concat(m.map(Jc).join(""),"")}var $c=function(){};function cl(c){return"string"==typeof(c.getAttribute?c.getAttribute(B):null)}var ll={replace:function(c){var l=c[0],h=c[1].map(function(c){return Jc(c)}).join("\n");if(l.parentNode&&l.outerHTML)l.outerHTML=h+(K.keepOriginalSource&&"svg"!==l.tagName.toLowerCase()?"\x3c!-- ".concat(l.outerHTML," --\x3e"):"");else if(l.parentNode){var z=document.createElement("span");l.parentNode.replaceChild(z,l),z.outerHTML=h}},nest:function(c){var l=c[0],h=c[1];if(~gc(l).indexOf(K.replacementClass))return ll.replace(c);var z=new RegExp("".concat(K.familyPrefix,"-.*"));delete h[0].attributes.style,delete h[0].attributes.id;var v=h[0].attributes.class.split(" ").reduce(function(c,l){return l===K.replacementClass||l.match(z)?c.toSvg.push(l):c.toNode.push(l),c},{toNode:[],toSvg:[]});h[0].attributes.class=v.toSvg.join(" ");var a=h.map(function(c){return Jc(c)}).join("\n");l.setAttribute("class",v.toNode.join(" ")),l.setAttribute(B,""),l.innerHTML=a}};function hl(c){c()}function zl(h,c){var z="function"==typeof c?c:$c;if(0===h.length)z();else{var l=hl;K.mutateApproach===y&&(l=o.requestAnimationFrame||hl),l(function(){var c=!0===K.autoReplaceSvg?ll.replace:ll[K.autoReplaceSvg]||ll.replace,l=_c.begin("mutate");h.map(c),l(),z()})}}var vl=!1;function al(){vl=!1}var ml=null;function sl(c){if(t&&K.observeMutations){var v=c.treeCallback,a=c.nodeCallback,m=c.pseudoElementsCallback,l=c.observeMutationsRoot,h=void 0===l?V:l;ml=new t(function(c){vl||bc(c).forEach(function(c){if("childList"===c.type&&0io.jans.casa.plugins acct-linking-agama 0.0.0-nightly - + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + diff --git a/jans-casa/plugins/email_2fa/agama/pom.xml b/jans-casa/plugins/email_2fa/agama/pom.xml index 8c7d6e4e5a7..6694fe7d193 100644 --- a/jans-casa/plugins/email_2fa/agama/pom.xml +++ b/jans-casa/plugins/email_2fa/agama/pom.xml @@ -6,6 +6,14 @@ io.jans.casa.plugins email_2fa-agama 0.0.0-nightly + + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + diff --git a/jans-casa/plugins/email_2fa/pom.xml b/jans-casa/plugins/email_2fa/pom.xml index ed8dd41867f..d9117c3f930 100644 --- a/jans-casa/plugins/email_2fa/pom.xml +++ b/jans-casa/plugins/email_2fa/pom.xml @@ -8,6 +8,14 @@ 0.0.0-nightly jar + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + + 11 11 diff --git a/jans-cedarling/.cargo/config.toml b/jans-cedarling/.cargo/config.toml new file mode 100644 index 00000000000..4ec2f3b8620 --- /dev/null +++ b/jans-cedarling/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' diff --git a/jans-cedarling/Cargo.toml b/jans-cedarling/Cargo.toml index 623620c7158..6649b2da915 100644 --- a/jans-cedarling/Cargo.toml +++ b/jans-cedarling/Cargo.toml @@ -7,13 +7,20 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" sparkv = { path = "sparkv" } +jsonwebtoken = "9.3.0" +jsonwebkey = "0.3.5" +chrono = "0.4" cedarling = { path = "cedarling" } test_utils = { path = "test_utils" } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = "0.3" +serde-wasm-bindgen = "0.6" [profile.release] strip = "symbols" -debug-assertions = true +debug-assertions = false lto = "thin" opt-level = "s" codegen-units = 1 diff --git a/jans-cedarling/bindings/cedarling_python/Cargo.toml b/jans-cedarling/bindings/cedarling_python/Cargo.toml index 2ad9e97c089..b28c005cdba 100644 --- a/jans-cedarling/bindings/cedarling_python/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_python/Cargo.toml @@ -2,15 +2,17 @@ name = "cedarling_python" version = "0.0.0" edition = "2021" +description = "Python binding to Cedarling" +license = "Apache-2.0" [lib] name = "cedarling_python" crate-type = ["cdylib"] -[dependencies] +# dependency for NOT wasm target +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] pyo3 = { version = "0.22.5", features = ["extension-module", "gil-refs"] } -cedarling = { workspace = true } +cedarling = { workspace = true, features = ["blocking"] } serde = { workspace = true } serde_json = { workspace = true } serde-pyobject = "0.4.0" -jsonwebtoken = "9.3.0" diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs index fdd607bb0a5..a83372f48d1 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs @@ -30,7 +30,7 @@ pub struct AuthorizeResult { impl AuthorizeResult { /// Returns true if request is allowed fn is_allowed(&self) -> bool { - self.inner.is_allowed() + self.inner.decision } /// Get the decision value for workload diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs index 6f967fa38be..5912207381b 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/mod.rs @@ -4,8 +4,8 @@ * * Copyright (c) 2024, Gluu, Inc. */ -use pyo3::prelude::*; use pyo3::Bound; +use pyo3::prelude::*; pub(crate) mod authorize_result; mod authorize_result_response; diff --git a/jans-cedarling/bindings/cedarling_python/src/cedarling.rs b/jans-cedarling/bindings/cedarling_python/src/cedarling.rs index 8de129b1e77..fb55e6c1852 100644 --- a/jans-cedarling/bindings/cedarling_python/src/cedarling.rs +++ b/jans-cedarling/bindings/cedarling_python/src/cedarling.rs @@ -62,14 +62,14 @@ use serde_pyobject::to_pyobject; #[derive(Clone)] #[pyclass] pub struct Cedarling { - inner: cedarling::Cedarling, + inner: cedarling::blocking::Cedarling, } #[pymethods] impl Cedarling { #[new] fn new(config: &BootstrapConfig) -> PyResult { - let inner = cedarling::Cedarling::new(config.inner()) + let inner = cedarling::blocking::Cedarling::new(config.inner()) .map_err(|err| PyValueError::new_err(err.to_string()))?; Ok(Self { inner }) } @@ -112,7 +112,7 @@ impl Cedarling { } } -fn log_entry_to_py(gil: Python, entry: &cedarling::bindings::LogEntry) -> PyResult { +fn log_entry_to_py(gil: Python, entry: &serde_json::Value) -> PyResult { to_pyobject(gil, entry) .map(|v| v.unbind()) .map_err(|err| err.0) diff --git a/jans-cedarling/bindings/cedarling_python/src/config/mod.rs b/jans-cedarling/bindings/cedarling_python/src/config/mod.rs index 8f1b8c3a70e..4943836d61d 100644 --- a/jans-cedarling/bindings/cedarling_python/src/config/mod.rs +++ b/jans-cedarling/bindings/cedarling_python/src/config/mod.rs @@ -4,8 +4,8 @@ * * Copyright (c) 2024, Gluu, Inc. */ -use pyo3::prelude::*; use pyo3::Bound; +use pyo3::prelude::*; pub(crate) mod bootstrap_config; diff --git a/jans-cedarling/bindings/cedarling_python/src/lib.rs b/jans-cedarling/bindings/cedarling_python/src/lib.rs index 685260123e0..26f92fd5604 100644 --- a/jans-cedarling/bindings/cedarling_python/src/lib.rs +++ b/jans-cedarling/bindings/cedarling_python/src/lib.rs @@ -4,9 +4,10 @@ * * Copyright (c) 2024, Gluu, Inc. */ +#![cfg(not(target_arch = "wasm32"))] -use pyo3::prelude::*; use pyo3::Bound; +use pyo3::prelude::*; mod authorize; mod cedarling; diff --git a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml new file mode 100644 index 00000000000..9749bcf08e8 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "cedarling_wasm" +version = "0.0.0" +edition = "2021" +description = "The Cedarling is a performant local authorization service that runs the Rust Cedar Engine" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] # Required for WASM output + +[dependencies] +# Common dependency for WASM interop +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +cedarling = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasm-bindgen-test = "0.3.49" + +[profile.release] +lto = true + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-O4', '--enable-reference-types', '--enable-gc'] + +[dev-dependencies] +# is used in testing +test_utils = { workspace = true } diff --git a/jans-cedarling/bindings/cedarling_wasm/README.md b/jans-cedarling/bindings/cedarling_wasm/README.md new file mode 100644 index 00000000000..f49af05a1a8 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/README.md @@ -0,0 +1,188 @@ +# Cedarling WASM + +This module is designed to build cedarling for browser wasm. + +## Building + +For building we use [`wasm-pack`](https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_Wasm) for install you can use command `cargo install wasm-pack` + +Build cedarling in release: + +```bash +wasm-pack build --release --target web +``` + +Build cedarling in dev mode + +```bash +wasm-pack build --target web --dev +``` + +Result files will be in `pkg` folder. + +## Testing + +For WASM testing we use `wasm-pack` and it allows to make test in `node`, `chrome`, `firefox`, `safari`. You just need specify appropriate flag. + +Example for firefox. + +```bash +wasm-pack test --firefox +``` + +## Run browser example + +To run example using `index.html` you need execute following steps: + +1. Build wasm cedarling. +2. Run webserver using `python3 -m http.server` or any other. +3. Visit example app [localhost](http://localhost:8000/), on this app you will get log in browser console. + - Also you can try use cedarling with web app using [cedarling_app](http://localhost:8000/cedarling_app.html), using custom bootstrap properties and request. + +## WASM Usage + +After building WASM bindings in folder `pkg` you can find where you can find `cedarling_wasm.js` and `cedarling_wasm.d.ts` where is defined interface for application. + +In `index.html` described simple usage of `cedarling wasm` API: + +```js + import { BOOTSTRAP_CONFIG, REQUEST } from "/example_data.js" // Import js objects: bootstrap config and request + import initWasm, { init } from "/pkg/cedarling_wasm.js"; + + async function main() { + await initWasm(); // Initialize the WebAssembly module + + let instance = await init(BOOTSTRAP_CONFIG); + let result = await instance.authorize(REQUEST); + console.log("result:", result); + } + main().catch(console.error); +``` + +Before using any function from library you need initialize WASM runtime by calling `initWasm` function. + +### Defined API + +```ts +/** + * Create a new instance of the Cedarling application. + * This function can take as config parameter the eather `Map` other `Object` + */ +export function init(config: any): Promise; + +/** + * The instance of the Cedarling application. + */ +export class Cedarling { + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Object` + */ + static new(config: object): Promise; + /** + * Create a new instance of the Cedarling application. + * Assume that config is `Map` + */ + static new_from_map(config: Map): Promise; + /** + * Authorize request + * makes authorization decision based on the [`Request`] + */ + authorize(request: any): Promise; + /** + * Get logs and remove them from the storage. + * Returns `Array` of `Map` + */ + pop_logs(): Array; + /** + * Get specific log entry. + * Returns `Map` with values or `null`. + */ + get_log_by_id(id: string): any; + /** + * Returns a list of all log ids. + * Returns `Array` of `String` + */ + get_log_ids(): Array; +} + +/** + * A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResult { + /** + * Convert `AuthorizeResult` to json string value + */ + json_string(): string; + /** + * Result of authorization where principal is `Jans::Workload` + */ + workload?: AuthorizeResultResponse; + /** + * Result of authorization where principal is `Jans::User` + */ + person?: AuthorizeResultResponse; + /** + * Result of authorization + * true means `ALLOW` + * false means `Deny` + * + * this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + */ + decision: boolean; +} + +/** + * A WASM wrapper for the Rust `cedar_policy::Response` struct. + * Represents the result of an authorization request. + */ +export class AuthorizeResultResponse { + /** + * Authorization decision + */ + readonly decision: boolean; + /** + * Diagnostics providing more information on how this decision was reached + */ + readonly diagnostics: Diagnostics; +} + +/** + * Diagnostics + * =========== + * + * Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation. + */ +export class Diagnostics { + /** + * `PolicyId`s of the policies that contributed to the decision. + * If no policies applied to the request, this set will be empty. + * + * The ids should be treated as unordered, + */ + readonly reason: (string)[]; + /** + * Errors that occurred during authorization. The errors should be + * treated as unordered, since policies may be evaluated in any order. + */ + readonly errors: (PolicyEvaluationError)[]; +} + +/** + * PolicyEvaluationError + * ===================== + * + * Represents an error that occurred when evaluating a Cedar policy. + */ +export class PolicyEvaluationError { + /** + * Id of the policy with an error + */ + readonly id: string; + /** + * Underlying evaluation error string representation + */ + readonly error: string; +} +``` diff --git a/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html new file mode 100644 index 00000000000..f2874db77ff --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/cedarling_app.html @@ -0,0 +1,261 @@ + + + + + + + + + Cedarling WASM App + + + + + + + + + + + +
+ +
+
+

Init cedarling

+ input bootstrap config json +
+ + +
+ + +
+ + +
+
+ + +
+
+ + + + + + + + + +
+ + + +
+ + + + + + + + + \ No newline at end of file diff --git a/jans-cedarling/bindings/cedarling_wasm/example_data.js b/jans-cedarling/bindings/cedarling_wasm/example_data.js new file mode 100644 index 00000000000..78ced6921c8 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/example_data.js @@ -0,0 +1,161 @@ +const BOOTSTRAP_CONFIG = { + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_POLICY_STORE_URI": "https://raw.githubusercontent.com/JanssenProject/jans/refs/heads/main/jans-cedarling/bindings/cedarling_python/example_files/policy-store.json", + "CEDARLING_LOG_TYPE": "memory", + "CEDARLING_LOG_LEVEL": "DEBUG", + "CEDARLING_LOG_TTL": 120, + "CEDARLING_DECISION_LOG_USER_CLAIMS ": ["aud", "sub", "email", "username"], + "CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS ": ["aud", "client_id", "rp_id"], + "CEDARLING_USER_AUTHZ": "enabled", + "CEDARLING_WORKLOAD_AUTHZ": "enabled", + "CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION": "AND", + "CEDARLING_LOCAL_JWKS": null, + "CEDARLING_LOCAL_POLICY_STORE": null, + "CEDARLING_POLICY_STORE_LOCAL_FN": null, + "CEDARLING_JWT_SIG_VALIDATION": "disabled", + "CEDARLING_JWT_STATUS_VALIDATION": "disabled", + "CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED": [ + "HS256", + "RS256" + ], + "CEDARLING_AT_ISS_VALIDATION": "disabled", + "CEDARLING_AT_JTI_VALIDATION": "disabled", + "CEDARLING_AT_NBF_VALIDATION": "disabled", + "CEDARLING_AT_EXP_VALIDATION": "disabled", + "CEDARLING_IDT_ISS_VALIDATION": "disabled", + "CEDARLING_IDT_SUB_VALIDATION": "disabled", + "CEDARLING_IDT_EXP_VALIDATION": "disabled", + "CEDARLING_IDT_IAT_VALIDATION": "disabled", + "CEDARLING_IDT_AUD_VALIDATION": "disabled", + "CEDARLING_USERINFO_ISS_VALIDATION": "disabled", + "CEDARLING_USERINFO_SUB_VALIDATION": "disabled", + "CEDARLING_USERINFO_AUD_VALIDATION": "disabled", + "CEDARLING_USERINFO_EXP_VALIDATION": "disabled", + "CEDARLING_ID_TOKEN_TRUST_MODE": "strict", + "CEDARLING_LOCK": "disabled", + "CEDARLING_LOCK_MASTER_CONFIGURATION_URI": null, + "CEDARLING_DYNAMIC_CONFIGURATION": "disabled", + "CEDARLING_LOCK_SSA_JWT": "", + "CEDARLING_AUDIT_HEALTH_INTERVAL": 0, + "CEDARLING_AUDIT_TELEMETRY_INTERVAL": 0, + "CEDARLING_LISTEN_SSE": "disabled" +}; + +// Payload of access_token: +// { +// "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", +// "code": "3e2a2012-099c-464f-890b-448160c2ab25", +// "iss": "https://account.gluu.org", +// "token_type": "Bearer", +// "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", +// "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", +// "acr": "simple_password_auth", +// "x5t#S256": "", +// "nbf": 1731953030, +// "scope": [ +// "role", +// "openid", +// "profile", +// "email" +// ], +// "auth_time": 1731953027, +// "exp": 1732121460, +// "iat": 1731953030, +// "jti": "uZUh1hDUQo6PFkBPnwpGzg", +// "username": "Default Admin User", +// "status": { +// "status_list": { +// "idx": 306, +// "uri": "https://jans.test/jans-auth/restv1/status_list" +// } +// } +// } +let ACCESS_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiY29kZSI6IjNlMmEyMDEyLTA5OWMtNDY0Zi04OTBiLTQ0ODE2MGMyYWIyNSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhdWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhY3IiOiJzaW1wbGVfcGFzc3dvcmRfYXV0aCIsIng1dCNTMjU2IjoiIiwibmJmIjoxNzMxOTUzMDMwLCJzY29wZSI6WyJyb2xlIiwib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl0sImF1dGhfdGltZSI6MTczMTk1MzAyNywiZXhwIjoxNzMyMTIxNDYwLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6InVaVWgxaERVUW82UEZrQlBud3BHemciLCJ1c2VybmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjMwNiwidXJpIjoiaHR0cHM6Ly9qYW5zLnRlc3QvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.Pt-Y7F-hfde_WP7ZYwyvvSS11rKYQWGZXTzjH_aJKC5VPxzOjAXqI3Igr6gJLsP1aOd9WJvOPchflZYArctopXMWClbX_TxpmADqyCMsz78r4P450TaMKj-WKEa9cL5KtgnFa0fmhZ1ZWolkDTQ_M00Xr4EIvv4zf-92Wu5fOrdjmsIGFot0jt-12WxQlJFfs5qVZ9P-cDjxvQSrO1wbyKfHQ_txkl1GDATXsw5SIpC5wct92vjAVm5CJNuv_PE8dHAY-KfPTxOuDYBuWI5uA2Yjd1WUFyicbJgcmYzUSVt03xZ0kQX9dxKExwU2YnpDorfwebaAPO7G114Bkw208g"; + +// Payload of id_token: +// { +// "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", +// "code": "3e2a2012-099c-464f-890b-448160c2ab25", +// "iss": "https://account.gluu.org", +// "token_type": "Bearer", +// "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", +// "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", +// "acr": "simple_password_auth", +// "x5t#S256": "", +// "nbf": 1731953030, +// "scope": [ +// "role", +// "openid", +// "profile", +// "email" +// ], +// "auth_time": 1731953027, +// "exp": 1732121460, +// "iat": 1731953030, +// "jti": "uZUh1hDUQo6PFkBPnwpGzg", +// "username": "Default Admin User", +// "status": { +// "status_list": { +// "idx": 306, +// "uri": "https://jans.test/jans-auth/restv1/status_list" +// } +// } +// } +let ID_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiYnhhQ1QwWlFYYnY0c2J6alNEck5pQSIsInN1YiI6InF6eG4xU2NyYjlsV3RHeFZlZE1Da3ktUWxfSUxzcFphUUE2Znl1WWt0dzAiLCJhbXIiOltdLCJpc3MiOiJodHRwczovL2FjY291bnQuZ2x1dS5vcmciLCJub25jZSI6IjI1YjJiMTZiLTMyYTItNDJkNi04YThlLWU1ZmE5YWI4ODhjMCIsInNpZCI6IjZkNDQzNzM0LWI3YTItNGVkOC05ZDNhLTE2MDZkMmY5OTI0NCIsImphbnNPcGVuSURDb25uZWN0VmVyc2lvbiI6Im9wZW5pZGNvbm5lY3QtMS4wIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYWNyIjoic2ltcGxlX3Bhc3N3b3JkX2F1dGgiLCJjX2hhc2giOiJWOGg0c085Tnp1TEthd1BPLTNETkxBIiwibmJmIjoxNzMxOTUzMDMwLCJhdXRoX3RpbWUiOjE3MzE5NTMwMjcsImV4cCI6MTczMTk1NjYzMCwiZ3JhbnQiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6ImlqTFpPMW9vUnlXcmdJbjdjSWROeUEiLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4IjozMDcsInVyaSI6Imh0dHBzOi8vamFucy50ZXN0L2phbnMtYXV0aC9yZXN0djEvc3RhdHVzX2xpc3QifX19.Nw7MRaJ5LtDak_LdEjrICgVOxDwd1p1I8WxD7IYw0_mKlIJ-J_78rGPski9p3L5ZNCpXiHtVbnhc4lJdmbh-y6mrD3_EY_AmjK50xpuf6YuUuNVtFENCSkj_irPLkIDG65HeZherWsvH0hUn4FVGv8Sw9fjny9Doi-HGHnKg9Qvphqre1U8hCphCVLQlzXAXmBkbPOC8tDwId5yigBKXP50cdqDcT-bjXf9leIdGgq0jxb57kYaFSElprLN9nUygM4RNCn9mtmo1l4IsdTlvvUb3OMAMQkRLfMkiKBjjeSF3819mYRLb3AUBaFH16ZdHFBzTSB6oA22TYpUqOLihMg"; + +// Payload of userinfo_token: +// { +// "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", +// "email_verified": true, +// "role": [ +// "CasaAdmin" +// ], +// "iss": "https://account.gluu.org", +// "given_name": "Admin", +// "middle_name": "Admin", +// "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", +// "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", +// "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", +// "updated_at": 1731698135, +// "name": "Default Admin User", +// "nickname": "Admin", +// "family_name": "User", +// "jti": "OIn3g1SPSDSKAYDzENVoug", +// "email": "admin@jans.test", +// "jansAdminUIRole": [ +// "api-admin" +// ] +// } +let USERINFO_TOKEN = "eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiQ2FzYUFkbWluIl0sImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsImdpdmVuX25hbWUiOiJBZG1pbiIsIm1pZGRsZV9uYW1lIjoiQWRtaW4iLCJpbnVtIjoiYTZhNzAzMDEtYWY0OS00OTAxLTk2ODctMGJjZGNmNGUzNGZhIiwiY2xpZW50X2lkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwidXBkYXRlZF9hdCI6MTczMTY5ODEzNSwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsIm5pY2tuYW1lIjoiQWRtaW4iLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJqdGkiOiJPSW4zZzFTUFNEU0tBWUR6RU5Wb3VnIiwiZW1haWwiOiJhZG1pbkBqYW5zLnRlc3QiLCJqYW5zQWRtaW5VSVJvbGUiOlsiYXBpLWFkbWluIl19.CIahQtRpoTkIQx8KttLPIKH7gvGG8OmYCMzz7wch6k792DVYQG1R7q3sS9Ema1rO5Fm_GgjOsR0yTTMKsyhHDLBwkDd3cnMLgsh2AwVFZvxtpafTlUAPfjvMAy9YTtkPcY6rNUhsYLSSOA83kt6pHdIv5nI-G6ybqgg-bLBRpwZDoOV0TulRhmuukdiuugTXHT6Bb-K3ZeYs8CwewztnxoFTSDghSzq7VZIraV8SLTBLx5_xswn9mefamyB2XNN3o6vXuMyf4BEbYSCuJ3pu6YtNgfyWwt9cF8PYe4PVLoXZuJKN-cy4qrtgy43QXPCg96jSQUJqgLb5ZL5_3udm2Q"; + +let REQUEST = { + "tokens": { + "access_token": ACCESS_TOKEN, + "id_token": ID_TOKEN, + "userinfo_token": USERINFO_TOKEN, + }, + "action": 'Jans::Action::"Read"', + "resource": { + "type": "Jans::Application", + "id": "some_id", + "app_id": "application_id", + "name": "Some Application", + "url": { + "host": "jans.test", + "path": "/protected-endpoint", + "protocol": "http" + } + }, + "context": { + "current_time": Math.floor(Date.now() / 1000), + "device_health": ["Healthy"], + "fraud_indicators": ["Allowed"], + "geolocation": ["America"], + "network": "127.0.0.1", + "network_type": "Local", + "operating_system": "Linux", + "user_agent": "Linux" + }, +}; + +export { BOOTSTRAP_CONFIG, ACCESS_TOKEN, ID_TOKEN, USERINFO_TOKEN, REQUEST } \ No newline at end of file diff --git a/jans-cedarling/bindings/cedarling_wasm/index.html b/jans-cedarling/bindings/cedarling_wasm/index.html new file mode 100644 index 00000000000..fb5078e883c --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/index.html @@ -0,0 +1,55 @@ + + + + + + + + + Hello world cedarling WASM example + + + + + + + +
+ +
+

It is Cedarling example WASM page

+ Result is written to the js console log. +
+ +

+
+ Click here to move to the Cedarling test app +
+
+ + + + + \ No newline at end of file diff --git a/jans-cedarling/bindings/cedarling_wasm/src/lib.rs b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs new file mode 100644 index 00000000000..5fb8c52eaf7 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/src/lib.rs @@ -0,0 +1,323 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use cedarling::bindings::cedar_policy; +use cedarling::{BootstrapConfig, BootstrapConfigRaw, LogStorage, Request}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_json::json; +use serde_wasm_bindgen::Error; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::js_sys::{Array, Map, Object, Reflect}; + +#[cfg(test)] +mod tests; + +/// The instance of the Cedarling application. +#[wasm_bindgen] +#[derive(Clone)] +pub struct Cedarling { + instance: cedarling::Cedarling, +} + +/// Create a new instance of the Cedarling application. +/// This function can take as config parameter the eather `Map` other `Object` +#[wasm_bindgen] +pub async fn init(config: JsValue) -> Result { + if config.is_instance_of::() { + // convert to map + let config_map: Map = config.unchecked_into(); + Cedarling::new_from_map(config_map).await + } else if let Some(config_object) = Object::try_from(&config) { + Cedarling::new(config_object).await + } else { + Err(Error::new("config should be Map or Object")) + } +} + +#[wasm_bindgen] +impl Cedarling { + /// Create a new instance of the Cedarling application. + /// Assume that config is `Object` + pub async fn new(config: &Object) -> Result { + let config: BootstrapConfigRaw = serde_wasm_bindgen::from_value(config.into())?; + + let config = BootstrapConfig::from_raw_config(&config).map_err(Error::new)?; + + cedarling::Cedarling::new(&config) + .await + .map(|instance| Cedarling { instance }) + .map_err(Error::new) + } + + /// Create a new instance of the Cedarling application. + /// Assume that config is `Map` + pub async fn new_from_map(config: Map) -> Result { + let conf_js_val = config.unchecked_into(); + + let conf_object = Object::from_entries(&conf_js_val)?; + Self::new(&conf_object).await + } + + /// Authorize request + /// makes authorization decision based on the [`Request`] + pub async fn authorize(&self, request: JsValue) -> Result { + // if `request` is map convert to object + let request_object: JsValue = if request.is_instance_of::() { + Object::from_entries(&request)?.into() + } else { + request + }; + + let cedar_request: Request = serde_wasm_bindgen::from_value(request_object)?; + + let result = self + .instance + .authorize(cedar_request) + .await + .map_err(Error::new)?; + Ok(result.into()) + } + + /// Get logs and remove them from the storage. + /// Returns `Array` of `Map` + pub fn pop_logs(&self) -> Result { + let result = Array::new(); + for log in self.instance.pop_logs() { + let js_log = convert_json_to_object(&log)?; + result.push(&js_log); + } + Ok(result) + } + + /// Get specific log entry. + /// Returns `Map` with values or `null`. + pub fn get_log_by_id(&self, id: &str) -> Result { + let result = if let Some(log_json_value) = self.instance.get_log_by_id(id) { + convert_json_to_object(&log_json_value)? + } else { + JsValue::NULL + }; + Ok(result) + } + + /// Returns a list of all log ids. + /// Returns `Array` of `String` + pub fn get_log_ids(&self) -> Array { + let result = Array::new(); + for log_id in self.instance.get_log_ids() { + let js_id = log_id.into(); + result.push(&js_id); + } + result + } +} + +/// convert json to js object +fn convert_json_to_object(json_value: &serde_json::Value) -> Result { + let js_map_value = serde_wasm_bindgen::to_value(json_value)?; + to_object_recursive(js_map_value) +} + +/// recurcive convert [`Map`] to object +fn to_object_recursive(value: JsValue) -> Result { + if value.is_instance_of::() { + // Convert the Map into an Object where keys and values are recursively processed + let map = Map::unchecked_from_js(value); + let obj = Object::new(); + for entry in map.entries().into_iter() { + let entry = Array::unchecked_from_js(entry?); + let key = entry.get(0); + let val = to_object_recursive(entry.get(1))?; + Reflect::set(&obj, &key, &val)?; + } + Ok(obj.into()) + } else if value.is_instance_of::() { + // Recursively process arrays + let array = Array::unchecked_from_js(value); + let serialized_array = Array::new(); + for item in array.iter() { + serialized_array.push(&to_object_recursive(item)?); + } + Ok(serialized_array.into()) + } else if value.is_object() { + // Recursively process plain objects + let obj = Object::unchecked_from_js(value); + let keys = Object::keys(&obj); + let serialized_obj = Object::new(); + for key in keys.iter() { + let val = Reflect::get(&obj, &key)?; + Reflect::set(&serialized_obj, &key, &to_object_recursive(val)?)?; + } + Ok(serialized_obj.into()) + } else { + // Return primitive values as-is + Ok(value) + } +} + +/// A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct. +/// Represents the result of an authorization request. +#[wasm_bindgen] +#[derive(serde::Serialize)] +pub struct AuthorizeResult { + /// Result of authorization where principal is `Jans::Workload` + #[wasm_bindgen(getter_with_clone)] + pub workload: Option, + /// Result of authorization where principal is `Jans::User` + #[wasm_bindgen(getter_with_clone)] + pub person: Option, + + /// Result of authorization + /// true means `ALLOW` + /// false means `Deny` + /// + /// this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + pub decision: bool, +} + +#[wasm_bindgen] +impl AuthorizeResult { + /// Convert `AuthorizeResult` to json string value + pub fn json_string(&self) -> String { + json!(self).to_string() + } +} + +impl From for AuthorizeResult { + fn from(value: cedarling::AuthorizeResult) -> Self { + Self { + workload: value + .workload + .map(|v| AuthorizeResultResponse { inner: Rc::new(v) }), + person: value + .person + .map(|v| AuthorizeResultResponse { inner: Rc::new(v) }), + decision: value.decision, + } + } +} + +/// A WASM wrapper for the Rust `cedar_policy::Response` struct. +/// Represents the result of an authorization request. +#[wasm_bindgen] +#[derive(Clone)] +pub struct AuthorizeResultResponse { + // It can be premature optimization, but RC allows avoiding clone actual structure + inner: Rc, +} + +#[wasm_bindgen] +impl AuthorizeResultResponse { + /// Authorization decision + #[wasm_bindgen(getter)] + pub fn decision(&self) -> bool { + self.inner.decision() == cedar_policy::Decision::Allow + } + + /// Diagnostics providing more information on how this decision was reached + #[wasm_bindgen(getter)] + pub fn diagnostics(&self) -> Diagnostics { + Diagnostics { + inner: self.inner.diagnostics().clone(), + } + } +} + +impl Serialize for AuthorizeResultResponse { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Diagnostics", 2)?; + state.serialize_field("decision", &self.decision())?; + state.serialize_field("diagnostics", &self.diagnostics())?; + state.end() + } +} + +/// Diagnostics +/// =========== +/// +/// Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation. +#[wasm_bindgen] +pub struct Diagnostics { + inner: cedar_policy::Diagnostics, +} + +#[wasm_bindgen] +impl Diagnostics { + /// `PolicyId`s of the policies that contributed to the decision. + /// If no policies applied to the request, this set will be empty. + /// + /// The ids should be treated as unordered, + #[wasm_bindgen(getter)] + pub fn reason(&self) -> Vec { + self.inner.reason().map(|v| v.to_string()).collect() + } + + /// Errors that occurred during authorization. The errors should be + /// treated as unordered, since policies may be evaluated in any order. + #[wasm_bindgen(getter)] + pub fn errors(&self) -> Vec { + self.inner + .errors() + .map(|err| { + let mapped_error: cedarling::bindings::PolicyEvaluationError = err.into(); + PolicyEvaluationError { + inner: mapped_error, + } + }) + .collect() + } +} + +impl Serialize for Diagnostics { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Diagnostics", 2)?; + state.serialize_field("reason", &self.reason())?; + state.serialize_field("errors", &self.errors())?; + state.end() + } +} + +/// PolicyEvaluationError +/// ===================== +/// +/// Represents an error that occurred when evaluating a Cedar policy. +#[wasm_bindgen] +pub struct PolicyEvaluationError { + inner: cedarling::bindings::PolicyEvaluationError, +} + +#[wasm_bindgen] +impl PolicyEvaluationError { + /// Id of the policy with an error + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.inner.id.clone() + } + + /// Underlying evaluation error string representation + #[wasm_bindgen(getter)] + pub fn error(&self) -> String { + self.inner.error.clone() + } +} + +impl Serialize for PolicyEvaluationError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("PolicyEvaluationError", 2)?; + state.serialize_field("id", &self.id())?; + state.serialize_field("error", &self.error())?; + state.end() + } +} diff --git a/jans-cedarling/bindings/cedarling_wasm/src/tests.rs b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs new file mode 100644 index 00000000000..740e2cb7f72 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/src/tests.rs @@ -0,0 +1,411 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +// allow dead code to avoid highlight test functions (by linter) that is used only using WASM +#![allow(dead_code)] + +use std::sync::LazyLock; + +use crate::*; + +use cedarling::{ResourceData, Tokens}; +use serde::Deserialize; +use serde_json::json; +use test_utils::token_claims::generate_token_using_claims; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +// Reuse json policy store file from python example. +// Because for `BootstrapConfigRaw` we need to use JSON +static POLICY_STORE_RAW_YAML: &str = + include_str!("../../../bindings/cedarling_python/example_files/policy-store.json"); + +static BOOTSTRAP_CONFIG: LazyLock = LazyLock::new(|| { + json!({ + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_LOCAL_POLICY_STORE": POLICY_STORE_RAW_YAML, + "CEDARLING_LOG_TYPE": "std_out", + "CEDARLING_LOG_LEVEL": "INFO", + "CEDARLING_USER_AUTHZ": "enabled", + "CEDARLING_WORKLOAD_AUTHZ": "enabled", + "CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION": "AND", + "CEDARLING_ID_TOKEN_TRUST_MODE": "strict", + + }) +}); + +/// test init with map value using `Cedarling::new_from_map` +#[wasm_bindgen_test] +async fn test_cedarling_new_from_map() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + console_log!("conf_map_js_value: {conf_map_js_value:?}"); + + let conf_js_map: Map = conf_map_js_value.unchecked_into(); + console_log!("conf_js_map: {conf_js_map:?}"); + let _instance = Cedarling::new_from_map(conf_js_map.clone()) + .await + .inspect(|_| console_log!("Cedarling::new_from_map initialized successfully")) + .expect("Cedarling::new_from_map should be initialized"); +} + +/// test init with map value using `init` +#[wasm_bindgen_test] +async fn test_init_conf_as_map() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + console_log!("conf_map_js_value: {conf_map_js_value:?}"); + + let _instance = init(conf_map_js_value) + .await + .inspect(|_| console_log!("init initialized successfully")) + .expect("init function should be initialized with js map"); +} + +/// test init with object value using `Cedarling::new` +#[wasm_bindgen_test] +async fn test_cedarling_new_from_object() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let _instance = Cedarling::new(&conf_object) + .await + .expect("Cedarling::new_from_map should be initialized"); +} + +/// test init with object value using `init` +#[wasm_bindgen_test] +async fn test_init_conf_as_object() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let _instance = init(conf_object.into()) + .await + .expect("init function should be initialized with js map"); +} + +/// Test execution of cedarling. +/// Policy store and tokens data is used from python example. +/// +/// Policies used: +/// @444da5d85403f35ea76519ed1a18a33989f855bf1cf8 +/// permit( +/// principal is Jans::Workload, +/// action in [Jans::Action::"Read"], +/// resource is Jans::Application +/// )when{ +/// resource.name == "Some Application" +/// }; +/// +/// @840da5d85403f35ea76519ed1a18a33989f855bf1cf8 +/// permit( +/// principal is Jans::User, +/// action in [Jans::Action::"Read"], +/// resource is Jans::Application +/// )when{ +/// resource.name == "Some Application" +/// }; +/// +#[wasm_bindgen_test] +async fn test_run_cedarling() { + let bootstrap_config_json = BOOTSTRAP_CONFIG.clone(); + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let instance = init(conf_object.into()) + .await + .expect("init function should be initialized with js map"); + + let request = Request { + tokens: Tokens { + access_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "code": "3e2a2012-099c-464f-890b-448160c2ab25", + "iss": "https://account.gluu.org", + "token_type": "Bearer", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "x5t#S256": "", + "nbf": 1731953030, + "scope": [ + "role", + "openid", + "profile", + "email" + ], + "auth_time": 1731953027, + "exp": 1732121460, + "iat": 1731953030, + "jti": "uZUh1hDUQo6PFkBPnwpGzg", + "username": "Default Admin User", + "status": { + "status_list": { + "idx": 306, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + id_token: Some(generate_token_using_claims(json!({ + "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "amr": [], + "iss": "https://account.gluu.org", + "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", + "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "c_hash": "V8h4sO9NzuLKawPO-3DNLA", + "nbf": 1731953030, + "auth_time": 1731953027, + "exp": 1731956630, + "grant": "authorization_code", + "iat": 1731953030, + "jti": "ijLZO1ooRyWrgIn7cIdNyA", + "status": { + "status_list": { + "idx": 307, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + userinfo_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "email_verified": true, + "role": [ + "CasaAdmin" + ], + "iss": "https://account.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "updated_at": 1731698135, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "OIn3g1SPSDSKAYDzENVoug", + "email": "admin@jans.test", + "jansAdminUIRole": [ + "api-admin" + ] + }))), + }, + context: json!({ + "current_time": 1735349685, // unix time + "device_health": ["Healthy"], + "fraud_indicators": ["Allowed"], + "geolocation": ["America"], + "network": "127.0.0.1", + "network_type": "Local", + "operating_system": "Linux", + "user_agent": "Linux" + }), + action: "Jans::Action::\"Read\"".to_string(), + resource: ResourceData::deserialize(json!({ + "type": "Jans::Application", + "id": "some_id", + "app_id": "application_id", + "name": "Some Application", + "url": { + "host": "jans.test", + "path": "/protected-endpoint", + "protocol": "http" + } + })) + .expect("ResourceData should be deserialized correctly"), + }; + + let js_request = + serde_wasm_bindgen::to_value(&request).expect("Request should be converted to JsObject"); + + let result = instance + .authorize(js_request) + .await + .expect("authorize request should be executed"); + + assert!(result.decision, "decision should be allowed") +} + +/// Test memory log interface. +/// In this scenario we check that memory log interface return some data +#[wasm_bindgen_test] +async fn test_memory_log_interface() { + let bootstrap_config_json = json!({ + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_LOCAL_POLICY_STORE": POLICY_STORE_RAW_YAML, + "CEDARLING_LOG_TYPE": "memory", + "CEDARLING_LOG_TTL": 120, + "CEDARLING_LOG_LEVEL": "INFO", + "CEDARLING_USER_AUTHZ": "enabled", + "CEDARLING_WORKLOAD_AUTHZ": "enabled", + "CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION": "AND", + "CEDARLING_ID_TOKEN_TRUST_MODE": "strict", + + }); + + let conf_map_js_value = serde_wasm_bindgen::to_value(&bootstrap_config_json) + .expect("serde json value should be converted to JsValue"); + + let conf_object = + Object::from_entries(&conf_map_js_value).expect("map value should be converted to object"); + + let instance = init(conf_object.into()) + .await + .expect("init function should be initialized with js map"); + + let request = Request { + tokens: Tokens { + access_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "code": "3e2a2012-099c-464f-890b-448160c2ab25", + "iss": "https://account.gluu.org", + "token_type": "Bearer", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "x5t#S256": "", + "nbf": 1731953030, + "scope": [ + "role", + "openid", + "profile", + "email" + ], + "auth_time": 1731953027, + "exp": 1732121460, + "iat": 1731953030, + "jti": "uZUh1hDUQo6PFkBPnwpGzg", + "username": "Default Admin User", + "status": { + "status_list": { + "idx": 306, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + id_token: Some(generate_token_using_claims(json!({ + "at_hash": "bxaCT0ZQXbv4sbzjSDrNiA", + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "amr": [], + "iss": "https://account.gluu.org", + "nonce": "25b2b16b-32a2-42d6-8a8e-e5fa9ab888c0", + "sid": "6d443734-b7a2-4ed8-9d3a-1606d2f99244", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "acr": "simple_password_auth", + "c_hash": "V8h4sO9NzuLKawPO-3DNLA", + "nbf": 1731953030, + "auth_time": 1731953027, + "exp": 1731956630, + "grant": "authorization_code", + "iat": 1731953030, + "jti": "ijLZO1ooRyWrgIn7cIdNyA", + "status": { + "status_list": { + "idx": 307, + "uri": "https://jans.test/jans-auth/restv1/status_list" + } + } + }))), + userinfo_token: Some(generate_token_using_claims(json!({ + "sub": "qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0", + "email_verified": true, + "role": [ + "CasaAdmin" + ], + "iss": "https://account.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "a6a70301-af49-4901-9687-0bcdcf4e34fa", + "client_id": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "aud": "d7f71bea-c38d-4caf-a1ba-e43c74a11a62", + "updated_at": 1731698135, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "OIn3g1SPSDSKAYDzENVoug", + "email": "admin@jans.test", + "jansAdminUIRole": [ + "api-admin" + ] + }))), + }, + context: json!({ + "current_time": 1735349685, // unix time + "device_health": ["Healthy"], + "fraud_indicators": ["Allowed"], + "geolocation": ["America"], + "network": "127.0.0.1", + "network_type": "Local", + "operating_system": "Linux", + "user_agent": "Linux" + }), + action: "Jans::Action::\"Read\"".to_string(), + resource: ResourceData::deserialize(json!({ + "type": "Jans::Application", + "id": "some_id", + "app_id": "application_id", + "name": "Some Application", + "url": { + "host": "jans.test", + "path": "/protected-endpoint", + "protocol": "http" + } + })) + .expect("ResourceData should be deserialized correctly"), + }; + + let js_request = + serde_wasm_bindgen::to_value(&request).expect("Request should be converted to JsObject"); + + let _result = instance + .authorize(js_request) + .await + .expect("authorize request should be executed"); + + let js_log_ids = instance.get_log_ids(); + let logs_count = js_log_ids.length(); + + for js_log_id in js_log_ids { + let log_id_str = js_log_id.as_string().expect("js_log_id should be string"); + + let log_val = instance + .get_log_by_id(log_id_str.as_str()) + .expect("get_log_by_id should not throw error"); + + assert_ne!(log_val, JsValue::NULL, "log result should be not null") + } + + let pop_logs_result = instance.pop_logs().expect("pop_logs not throw error"); + assert_eq!( + logs_count, + pop_logs_result.length(), + "length of ids and logs should be the same" + ); + + let pop_logs_result2 = instance.pop_logs().expect("pop_logs not throw error"); + assert_eq!( + pop_logs_result2.length(), + 0, + "logs should be removed from storage, storage should be empty" + ); +} diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index bad3ee54ef6..689991b1919 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -2,10 +2,16 @@ name = "cedarling" version = "0.0.0-nightly" edition = "2021" +description = "The Cedarling: a high-performance local authorization service powered by the Rust Cedar Engine." +license = "Apache-2.0" + +[features] +# blocking feature allows to use blocking cedarling client +blocking = ["tokio/rt-multi-thread"] [dependencies] serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } serde_yml = "0.0.12" thiserror = { workspace = true } sparkv = { workspace = true } @@ -14,8 +20,8 @@ cedar-policy = "4.2" base64 = "0.22.1" url = "2.5.2" lazy_static = "1.5.0" -jsonwebtoken = "9.3.0" -reqwest = { version = "0.12.8", features = ["blocking", "json"] } +jsonwebtoken = { workspace = true } +reqwest = { version = "0.12.8", features = ["json"] } bytes = "1.7.2" typed-builder = "0.20.0" semver = { version = "1.0.23", features = ["serde"] } @@ -27,11 +33,17 @@ derive_more = { version = "1.0.0", features = [ ] } time = { version = "0.3.36", features = ["wasm-bindgen"] } regex = "1.11.1" -chrono = "0.4.38" +chrono = { workspace = true } +tokio = { version = "1.42.0", features = ["macros", "time"] } +rand = "0.8.5" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["console"] } + [dev-dependencies] # is used in testing test_utils = { workspace = true } rand = "0.8.5" -jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] } +jsonwebkey = { workspace = true, features = ["generate", "jwt-convert"] } mockito = "1.5.0" diff --git a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs index 10e82e7fb03..62f567e4a7d 100644 --- a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs @@ -15,7 +15,8 @@ use jsonwebtoken::Algorithm; static POLICY_STORE_RAW_YAML: &str = include_str!("../../test_files/policy-store_with_trusted_issuers_ok.yaml"); -fn main() -> Result<(), Box> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { // Configure JWT validation settings. Enable the JwtService to validate JWT tokens // using specific algorithms: `HS256` and `RS256`. Only tokens signed with these algorithms // will be accepted; others will be marked as invalid during validation. @@ -54,29 +55,32 @@ fn main() -> Result<(), Box> { user_workload_operator: WorkloadBoolOp::And, ..Default::default() }, - })?; + }) + .await?; // Perform an authorization request to Cedarling. // This request checks if the provided tokens have sufficient permission to perform an action // on a specific resource. Each token (access, ID, and userinfo) is required for the // authorization process, alongside resource and action details. - let result = cedarling.authorize(Request { + let result = cedarling + .authorize(Request { tokens: Tokens { - access_token: Some(access_token), - id_token: Some(id_token), - userinfo_token: Some(userinfo_token), + access_token: Some(access_token), + id_token: Some(id_token), + userinfo_token: Some(userinfo_token), }, - action: "Jans::Action::\"Update\"".to_string(), - context: serde_json::json!({}), - resource: ResourceData { - id: "random_id".to_string(), - resource_type: "Jans::Issue".to_string(), - payload: HashMap::from_iter([( - "org_id".to_string(), - serde_json::Value::String("some_long_id".to_string()), - )]), - }, - }); + action: "Jans::Action::\"Update\"".to_string(), + context: serde_json::json!({}), + resource: ResourceData { + id: "random_id".to_string(), + resource_type: "Jans::Issue".to_string(), + payload: HashMap::from_iter([( + "org_id".to_string(), + serde_json::Value::String("some_long_id".to_string()), + )]), + }, + }) + .await; // Handle authorization result. If there's an error, print it. if let Err(ref e) = &result { diff --git a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs index e99d2aace33..334f51c6730 100644 --- a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs @@ -12,7 +12,8 @@ use cedarling::{ static POLICY_STORE_RAW: &str = include_str!("../../test_files/policy-store_ok.yaml"); -fn main() -> Result<(), Box> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { let cedarling = Cedarling::new(&BootstrapConfig { application_name: "test_app".to_string(), log_config: LogConfig { @@ -32,7 +33,8 @@ fn main() -> Result<(), Box> { decision_log_workload_claims: vec!["org_id".to_string()], ..Default::default() }, - })?; + }) + .await?; // the following tokens are expired // access_token claims: @@ -111,33 +113,35 @@ fn main() -> Result<(), Box> { // } let userinfo_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJzdWIiOiJib0c4ZGZjNU1LVG4zN283Z3NkQ2V5cUw4THBXUXRnb080MW0xS1p3ZHEwIiwiY2xpZW50X2lkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwidXNlcm5hbWUiOiJhZG1pbkBnbHV1Lm9yZyIsIm5hbWUiOiJEZWZhdWx0IEFkbWluIFVzZXIiLCJlbWFpbCI6ImFkbWluQGdsdXUub3JnIiwiY291bnRyeSI6IlVTIiwianRpIjoidXNyaW5mb190a25fanRpIn0.NoR53vPZFpfb4vFk85JH9RPx7CHsaJMZwrH3fnB-N60".to_string(); - let result = cedarling.authorize(Request { + let result = cedarling + .authorize(Request { tokens: Tokens { - access_token: Some(access_token), - id_token: Some(id_token), - userinfo_token: Some(userinfo_token), + access_token: Some(access_token), + id_token: Some(id_token), + userinfo_token: Some(userinfo_token), }, - action: "Jans::Action::\"Update\"".to_string(), - context: serde_json::json!({}), - resource: ResourceData { - id: "random_id".to_string(), - resource_type: "Jans::Issue".to_string(), - payload: HashMap::from_iter([ - ( - "org_id".to_string(), - serde_json::Value::String("some_long_id".to_string()), - ), - ( - "country".to_string(), - serde_json::Value::String("US".to_string()), - ), - ]), - }, - }); + action: "Jans::Action::\"Update\"".to_string(), + context: serde_json::json!({}), + resource: ResourceData { + id: "random_id".to_string(), + resource_type: "Jans::Issue".to_string(), + payload: HashMap::from_iter([ + ( + "org_id".to_string(), + serde_json::Value::String("some_long_id".to_string()), + ), + ( + "country".to_string(), + serde_json::Value::String("US".to_string()), + ), + ]), + }, + }) + .await; match result { Ok(result) => { - println!("\n\nis allowed: {}", result.is_allowed()); + println!("\n\nis allowed: {}", result.decision); }, Err(e) => eprintln!("Error while authorizing: {}\n {:?}\n\n", e, e), } diff --git a/jans-cedarling/cedarling/examples/log_init.rs b/jans-cedarling/cedarling/examples/log_init.rs index 585beb5f406..d8f1538b755 100644 --- a/jans-cedarling/cedarling/examples/log_init.rs +++ b/jans-cedarling/cedarling/examples/log_init.rs @@ -9,18 +9,18 @@ // and `use std::env` prevents that compilation. #![cfg(not(target_family = "wasm"))] -use std::env; - use cedarling::{ AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogLevel, LogStorage, LogTypeConfig, MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, WorkloadBoolOp, }; +use std::env; // The human-readable policy and schema file is located in next folder: // `test_files\policy-store_ok` static POLICY_STORE_RAW: &str = include_str!("../../test_files/policy-store_ok.yaml"); -fn main() -> Result<(), Box> { +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { // Collect command-line arguments let args: Vec = env::args().collect(); @@ -61,7 +61,8 @@ fn main() -> Result<(), Box> { user_workload_operator: WorkloadBoolOp::And, ..Default::default() }, - })?; + }) + .await?; println!("Stage 1:"); let logs_ids = cedarling.get_log_ids(); diff --git a/jans-cedarling/cedarling/src/authz/authorize_result.rs b/jans-cedarling/cedarling/src/authz/authorize_result.rs index 8d5da5cfa59..86f0ebf9bc8 100644 --- a/jans-cedarling/cedarling/src/authz/authorize_result.rs +++ b/jans-cedarling/cedarling/src/authz/authorize_result.rs @@ -1,13 +1,14 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashSet; +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ use cedar_policy::Decision; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; +use std::collections::HashSet; use crate::bootstrap_config::WorkloadBoolOp; @@ -15,13 +16,19 @@ use crate::bootstrap_config::WorkloadBoolOp; /// based on the [Request](crate::models::request::Request) and policy store #[derive(Debug, Clone, Serialize)] pub struct AuthorizeResult { - user_workload_operator: WorkloadBoolOp, /// Result of authorization where principal is `Jans::Workload` #[serde(serialize_with = "serialize_opt_response")] pub workload: Option, /// Result of authorization where principal is `Jans::User` #[serde(serialize_with = "serialize_opt_response")] pub person: Option, + + /// Result of authorization + /// true means `ALLOW` + /// false means `Deny` + /// + /// this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1). + pub decision: bool, } /// Custom serializer for an Option which converts `None` to an empty string and vice versa. @@ -67,50 +74,47 @@ impl AuthorizeResult { person: Option, ) -> Self { Self { - user_workload_operator, + decision: calc_decision(&user_workload_operator, &workload, &person), workload, person, } } - /// Evaluates the authorization result to determine if the request is allowed. - /// - /// This function checks the decision based on the following rule: - /// - The `workload` must allow the request (PRINCIPAL). - /// - Either the `person` must also allow the request. - /// - /// This approach represents decision-making model, where the - /// `workload` (i.e., primary principal) needs to permit the request and - /// additional conditions `person` must also indicate allowance. - /// - /// If person and wokload is present will be used operator (AND or OR) based on `CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION` bootstrap property. - pub fn is_allowed(&self) -> bool { - let workload_allowed = self - .workload - .as_ref() - .map(|response| response.decision() == Decision::Allow); - - let person_allowed = self - .person - .as_ref() - .map(|response| response.decision() == Decision::Allow); - - // cover each possible case when any of value is Some or None - match (workload_allowed, person_allowed) { - (None, None) => false, - (None, Some(person)) => person, - (Some(workload), None) => workload, - (Some(workload), Some(person)) => self.user_workload_operator.calc(workload, person), - } - } - /// Decision of result /// works based on [`AuthorizeResult::is_allowed`] - pub fn decision(&self) -> Decision { - if self.is_allowed() { + pub fn cedar_decision(&self) -> Decision { + if self.decision { Decision::Allow } else { Decision::Deny } } } + +/// Evaluates the authorization result to determine if the request is allowed. +/// +/// If present only workload result return true if decision is `ALLOW`. +/// If present only person result return true if decision is `ALLOW`. +/// If person and workload is present will be used operator (AND or OR) based on `CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION` bootstrap property. +/// If none present return false. +fn calc_decision( + user_workload_operator: &WorkloadBoolOp, + workload: &Option, + person: &Option, +) -> bool { + let workload_allowed = workload + .as_ref() + .map(|response| response.decision() == Decision::Allow); + + let person_allowed = person + .as_ref() + .map(|response| response.decision() == Decision::Allow); + + // cover each possible case when any of value is Some or None + match (workload_allowed, person_allowed) { + (None, None) => false, + (None, Some(person)) => person, + (Some(workload), None) => workload, + (Some(workload), Some(person)) => user_workload_operator.calc(workload, person), + } +} diff --git a/jans-cedarling/cedarling/src/authz/entities/create.rs b/jans-cedarling/cedarling/src/authz/entities/create.rs index f1346c22594..517cae03e69 100644 --- a/jans-cedarling/cedarling/src/authz/entities/create.rs +++ b/jans-cedarling/cedarling/src/authz/entities/create.rs @@ -9,10 +9,10 @@ use std::str::FromStr; use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression}; use super::trait_as_expression::AsExpression; +use crate::common::cedar_schema::CedarSchemaJson; use crate::common::cedar_schema::cedar_json::{ CedarSchemaEntityShape, CedarSchemaRecord, CedarType, GetCedarTypeError, SchemaDefinedType, }; -use crate::common::cedar_schema::CedarSchemaJson; use crate::common::policy_store::ClaimMappings; use crate::jwt::{Token, TokenClaim, TokenClaimTypeError, TokenClaims}; diff --git a/jans-cedarling/cedarling/src/authz/entities/mod.rs b/jans-cedarling/cedarling/src/authz/entities/mod.rs index cad03f4e371..6d9310e791d 100644 --- a/jans-cedarling/cedarling/src/authz/entities/mod.rs +++ b/jans-cedarling/cedarling/src/authz/entities/mod.rs @@ -16,20 +16,20 @@ mod test_create; use std::collections::HashSet; use cedar_policy::{Entity, EntityUid}; +pub use create::{CEDAR_POLICY_SEPARATOR, CreateCedarEntityError}; use create::{ - build_entity_uid, create_entity, parse_namespace_and_typename, EntityMetadata, - EntityParsedTypeName, + EntityMetadata, EntityParsedTypeName, build_entity_uid, create_entity, + parse_namespace_and_typename, }; -pub use create::{CreateCedarEntityError, CEDAR_POLICY_SEPARATOR}; pub use user::*; pub use workload::*; -use super::request::ResourceData; use super::AuthorizeError; +use super::request::ResourceData; +use crate::AuthorizationConfig; use crate::common::cedar_schema::CedarSchemaJson; use crate::common::policy_store::{ClaimMappings, PolicyStore, TokenKind}; use crate::jwt::Token; -use crate::AuthorizationConfig; const DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME: &str = "Access_token"; const DEFAULT_ID_TKN_ENTITY_TYPE_NAME: &str = "id_token"; diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create.rs b/jans-cedarling/cedarling/src/authz/entities/test_create.rs index b00c2c335aa..3cc5cc331ff 100644 --- a/jans-cedarling/cedarling/src/authz/entities/test_create.rs +++ b/jans-cedarling/cedarling/src/authz/entities/test_create.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; -use test_utils::{assert_eq, SortedJson}; +use test_utils::{SortedJson, assert_eq}; use super::create::*; use crate::common::cedar_schema::CedarSchemaJson; @@ -183,7 +183,9 @@ fn get_token_claim_type_string_error() { "expected type: {origin_type}, but got: {actual_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -237,7 +239,9 @@ fn get_token_claim_type_long_error() { "expected type: {origin_type}, but got: {actual_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -291,7 +295,9 @@ fn get_token_claim_type_entity_uid_error() { "expected type: {origin_type}, but got: {actual_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -351,7 +357,9 @@ fn get_token_claim_type_boolean_error() { {expected_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -411,7 +419,9 @@ fn get_token_claim_type_set_error() { {expected_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } @@ -474,7 +484,9 @@ fn get_token_claim_type_set_of_set_error() { {expected_type}" ); } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); + panic!( + "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" + ); } } diff --git a/jans-cedarling/cedarling/src/authz/entities/user.rs b/jans-cedarling/cedarling/src/authz/entities/user.rs index da43a4f6480..fd50773bad9 100644 --- a/jans-cedarling/cedarling/src/authz/entities/user.rs +++ b/jans-cedarling/cedarling/src/authz/entities/user.rs @@ -91,6 +91,7 @@ mod test { use cedar_policy::{Entity, RestrictedExpression}; use serde_json::json; use test_utils::assert_eq; + use tokio::test; use super::create_user_entity; use crate::authz::entities::DecodedTokens; @@ -100,13 +101,14 @@ mod test { use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; #[test] - fn can_create_from_id_token() { + async fn can_create_from_id_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -141,13 +143,14 @@ mod test { } #[test] - fn can_create_from_userinfo_token() { + async fn can_create_from_userinfo_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -182,13 +185,14 @@ mod test { } #[test] - fn errors_when_tokens_have_missing_claims() { + async fn errors_when_tokens_have_missing_claims() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -221,13 +225,14 @@ mod test { } #[test] - fn errors_when_tokens_unavailable() { + async fn errors_when_tokens_unavailable() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; diff --git a/jans-cedarling/cedarling/src/authz/entities/workload.rs b/jans-cedarling/cedarling/src/authz/entities/workload.rs index 6ce1acafb8a..d10509c08d7 100644 --- a/jans-cedarling/cedarling/src/authz/entities/workload.rs +++ b/jans-cedarling/cedarling/src/authz/entities/workload.rs @@ -86,6 +86,7 @@ mod test { use cedar_policy::{Entity, RestrictedExpression}; use serde_json::json; use test_utils::assert_eq; + use tokio::test; use super::create_workload_entity; use crate::authz::entities::DecodedTokens; @@ -95,13 +96,14 @@ mod test { use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; #[test] - fn can_create_from_id_token() { + async fn can_create_from_id_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -136,13 +138,14 @@ mod test { } #[test] - fn can_create_from_access_token() { + async fn can_create_from_access_token() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -177,13 +180,14 @@ mod test { } #[test] - fn errors_when_tokens_have_missing_claims() { + async fn errors_when_tokens_have_missing_claims() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; @@ -212,13 +216,14 @@ mod test { } #[test] - fn errors_when_tokens_unavailable() { + async fn errors_when_tokens_unavailable() { let entity_mapping = None; let policy_store = load_policy_store(&PolicyStoreConfig { source: PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok_2.yaml").into(), ), }) + .await .expect("Should load policy store") .store; diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index 198b6425785..c92a1c84f18 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -18,10 +18,12 @@ use crate::common::app_types; use crate::common::cedar_schema::cedar_json::{BuildJsonCtxError, FindActionError}; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{self, TokenStr}; + use crate::log::interface::LogWriter; use crate::log::{ - AuthorizationLogInfo, BaseLogEntry, DecisionLogEntry, Diagnostics, LogEntry, LogLevel, - LogTokensInfo, LogType, Logger, PrincipalLogEntry, UserAuthorizeInfo, WorkloadAuthorizeInfo, + AuthorizationLogInfo, BaseLogEntry, DecisionLogEntry, Diagnostics, DiagnosticsRefs, LogEntry, + LogLevel, LogTokensInfo, LogType, Logger, PrincipalLogEntry, UserAuthorizeInfo, + WorkloadAuthorizeInfo, }; mod authorize_result; @@ -29,17 +31,17 @@ mod merge_json; pub(crate) mod entities; pub(crate) mod request; -use std::time::Instant; pub use authorize_result::AuthorizeResult; use cedar_policy::{ContextJsonError, Entities, Entity, EntityUid}; +use chrono::Utc; use entities::{ - create_resource_entity, create_role_entities, create_token_entities, create_user_entity, - create_workload_entity, CreateCedarEntityError, CreateUserEntityError, + CEDAR_POLICY_SEPARATOR, CreateCedarEntityError, CreateUserEntityError, CreateWorkloadEntityError, DecodedTokens, ResourceEntityError, RoleEntityError, - CEDAR_POLICY_SEPARATOR, + create_resource_entity, create_role_entities, create_token_entities, create_user_entity, + create_workload_entity, }; -use merge_json::{merge_json_values, MergeError}; +use merge_json::{MergeError, merge_json_values}; use request::Request; use serde_json::Value; @@ -82,32 +84,42 @@ impl Authz { } // decode JWT tokens to structs AccessTokenData, IdTokenData, UserInfoTokenData using jwt service - pub(crate) fn decode_tokens<'a>( + pub(crate) async fn decode_tokens<'a>( &'a self, request: &'a Request, ) -> Result, AuthorizeError> { - let access_token = request - .tokens - .access_token - .as_ref() - .map(|tkn| self.config.jwt_service.process_token(TokenStr::Access(tkn))) - .transpose()?; - let id_token = request - .tokens - .id_token - .as_ref() - .map(|tkn| self.config.jwt_service.process_token(TokenStr::Id(tkn))) - .transpose()?; - let userinfo_token = request - .tokens - .userinfo_token - .as_ref() - .map(|tkn| { + let access_token = if let Some(tkn) = request.tokens.access_token.as_ref() { + Some( self.config .jwt_service - .process_token(TokenStr::Userinfo(tkn)) - }) - .transpose()?; + .process_token(TokenStr::Access(tkn.as_str())) + .await?, + ) + } else { + None + }; + + let id_token = if let Some(tkn) = request.tokens.id_token.as_ref() { + Some( + self.config + .jwt_service + .process_token(TokenStr::Id(tkn.as_str())) + .await?, + ) + } else { + None + }; + + let userinfo_token = if let Some(tkn) = request.tokens.userinfo_token.as_ref() { + Some( + self.config + .jwt_service + .process_token(TokenStr::Userinfo(tkn.as_str())) + .await?, + ) + } else { + None + }; Ok(DecodedTokens { access_token, @@ -119,17 +131,19 @@ impl Authz { /// Evaluate Authorization Request /// - evaluate if authorization is granted for *person* /// - evaluate if authorization is granted for *workload* - pub fn authorize(&self, request: Request) -> Result { - let start_time = Instant::now(); + pub async fn authorize(&self, request: Request) -> Result { + let start_time = Utc::now(); + let schema = &self.config.policy_store.schema; - let tokens = self.decode_tokens(&request)?; + + let tokens = self.decode_tokens(&request).await?; // Parse action UID. let action = cedar_policy::EntityUid::from_str(request.action.as_str()) .map_err(AuthorizeError::Action)?; // Parse [`cedar_policy::Entity`]-s to [`AuthorizeEntitiesData`] that hold all entities (for usability). - let entities_data: AuthorizeEntitiesData = self.build_entities(&request, &tokens)?; + let entities_data: AuthorizeEntitiesData = self.build_entities(&request, &tokens).await?; // Get entity UIDs what we will be used on authorize check let resource_uid = entities_data.resource.uid(); @@ -239,7 +253,9 @@ impl Authz { ); // measure time how long request executes - let elapsed_ms = start_time.elapsed().as_millis(); + let elapsed_ms = Utc::now() + .signed_duration_since(start_time) + .num_milliseconds(); // FROM THIS POINT WE ONLY MAKE LOGS @@ -251,27 +267,13 @@ impl Authz { let entities_json: serde_json::Value = serde_json::from_slice(entities_raw_json.as_slice()) .map_err(AuthorizeError::EntitiesToJson)?; - // DEBUG LOG - // Log all result information about both authorize checks. - // Where principal is `"Jans::Workload"` and where principal is `"Jans::User"`. - self.config.log_service.as_ref().log( - LogEntry::new_with_data( - self.config.pdp_id, - Some(self.config.application_name.clone()), - LogType::System, - ) - .set_level(LogLevel::DEBUG) - .set_auth_info(AuthorizationLogInfo { - action: request.action.clone(), - context: request.context.clone(), - resource: resource_uid.to_string(), - entities: entities_json, - person_authorize_info: user_authz_info, - workload_authorize_info: workload_authz_info, - authorized: result.is_allowed(), - }) - .set_message("Result of authorize.".to_string()), - ); + let user_authz_diagnostic = user_authz_info + .as_ref() + .map(|auth_info| &auth_info.diagnostics); + + let workload_authz_diagnostic = user_authz_info + .as_ref() + .map(|auth_info| &auth_info.diagnostics); let tokens_logging_info = LogTokensInfo { access: tokens.access_token.as_ref().map(|tkn| { @@ -301,6 +303,7 @@ impl Authz { }; // Decision log + // we log decision log before debug log, to avoid cloning diagnostic info self.config.log_service.as_ref().log_any(&DecisionLogEntry { base: BaseLogEntry::new(self.config.pdp_id, LogType::Decision), policystore_id: self.config.policy_store.id.as_str(), @@ -311,11 +314,37 @@ impl Authz { lock_client_id: None, action: request.action.clone(), resource: resource_uid.to_string(), - decision: result.decision().into(), + decision: result.decision.into(), tokens: tokens_logging_info, decision_time_ms: elapsed_ms, + diagnostics: DiagnosticsRefs::new(&[ + &user_authz_diagnostic, + &workload_authz_diagnostic, + ]), }); + // DEBUG LOG + // Log all result information about both authorize checks. + // Where principal is `"Jans::Workload"` and where principal is `"Jans::User"`. + self.config.log_service.as_ref().log( + LogEntry::new_with_data( + self.config.pdp_id, + Some(self.config.application_name.clone()), + LogType::System, + ) + .set_level(LogLevel::DEBUG) + .set_auth_info(AuthorizationLogInfo { + action: request.action.clone(), + context: request.context.clone(), + resource: resource_uid.to_string(), + entities: entities_json, + person_authorize_info: user_authz_info, + workload_authorize_info: workload_authz_info, + authorized: result.decision, + }) + .set_message("Result of authorize.".to_string()), + ); + Ok(result) } @@ -345,10 +374,10 @@ impl Authz { /// Build all the Cedar [`Entities`] from a [`Request`] /// /// [`Entities`]: Entity - pub fn build_entities( + pub async fn build_entities( &self, request: &Request, - tokens: &DecodedTokens, + tokens: &DecodedTokens<'_>, ) -> Result { let policy_store = &self.config.policy_store; let auth_conf = &self.config.authorization; @@ -430,7 +459,9 @@ fn build_context( id_mapping.insert(type_name, type_id.to_string()); } - let entities_context = action_schema.build_ctx_entity_refs_json(id_mapping)?; + let entities_context = action_schema + .build_ctx_entity_refs_json(id_mapping) + .unwrap(); let context = merge_json_values(entities_context, request_context)?; diff --git a/jans-cedarling/cedarling/src/authz/request.rs b/jans-cedarling/cedarling/src/authz/request.rs index d618c803b98..a5c80de20b9 100644 --- a/jans-cedarling/cedarling/src/authz/request.rs +++ b/jans-cedarling/cedarling/src/authz/request.rs @@ -9,7 +9,7 @@ use std::str::FromStr; use cedar_policy::{EntityId, EntityTypeName, EntityUid, ParseErrors}; /// Box to store authorization data -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Request { /// Contains the JWTs that will be used for the AuthZ request pub tokens: Tokens, @@ -22,7 +22,7 @@ pub struct Request { } /// Contains the JWTs that will be used for the AuthZ request -#[derive(Debug, Clone, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Tokens { /// Access token raw value #[serde(default)] @@ -37,7 +37,7 @@ pub struct Tokens { /// Cedar policy resource data /// fields represent EntityUid -#[derive(serde::Deserialize, Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct ResourceData { /// entity type name #[serde(rename = "type")] diff --git a/jans-cedarling/cedarling/src/blocking.rs b/jans-cedarling/cedarling/src/blocking.rs new file mode 100644 index 00000000000..e665cc43e3b --- /dev/null +++ b/jans-cedarling/cedarling/src/blocking.rs @@ -0,0 +1,56 @@ +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ + +//! Blocking client of Cedarling + +use crate::Cedarling as AsyncCedarling; +use crate::{ + AuthorizeError, AuthorizeResult, BootstrapConfig, InitCedarlingError, LogStorage, Request, +}; +use std::sync::Arc; +use tokio::runtime::Runtime; + +/// The blocking instance of the Cedarling application. +/// It is safe to share between threads. +#[derive(Clone)] +pub struct Cedarling { + runtime: Arc, + instance: AsyncCedarling, +} + +impl Cedarling { + /// Builder + pub fn new(config: &BootstrapConfig) -> Result { + let rt = Runtime::new().map_err(InitCedarlingError::RuntimeInit)?; + + rt.block_on(AsyncCedarling::new(config)) + .map(|async_instance| Cedarling { + instance: async_instance, + runtime: Arc::new(rt), + }) + } + + /// Authorize request + /// makes authorization decision based on the [`Request`] + pub fn authorize(&self, request: Request) -> Result { + self.runtime.block_on(self.instance.authorize(request)) + } +} + +impl LogStorage for Cedarling { + fn pop_logs(&self) -> Vec { + self.instance.pop_logs() + } + + fn get_log_by_id(&self, id: &str) -> Option { + self.instance.get_log_by_id(id) + } + + fn get_log_ids(&self) -> Vec { + self.instance.get_log_ids() + } +} diff --git a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs index be2b1726e35..5b9d688c2a9 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs @@ -493,8 +493,9 @@ impl BootstrapConfig { .local_jwks .as_ref() .map(|path| { - fs::read_to_string(path) - .map_err(|e| BootstrapConfigLoadingError::LoadLocalJwks(path.to_string(), e)) + fs::read_to_string(path).map_err(|e| { + BootstrapConfigLoadingError::LoadLocalJwks(path.to_string(), e.to_string()) + }) }) .transpose()?; diff --git a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs index 59720f4a1a6..91e6065352d 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs @@ -11,8 +11,8 @@ pub(crate) mod jwt_config; pub(crate) mod log_config; pub(crate) mod policy_store_config; -use std::path::Path; -use std::{fs, io}; +#[cfg(not(target_arch = "wasm32"))] +use std::{fs, io, path::Path}; pub use authorization_config::AuthorizationConfig; // reimport to useful import values in root module @@ -53,6 +53,7 @@ impl BootstrapConfig { /// /// let config = BootstrapConfig::load_from_file("../test_files/bootstrap_props.json").unwrap(); /// ``` + #[cfg(not(target_arch = "wasm32"))] pub fn load_from_file(path: &str) -> Result { let file_ext = Path::new(path) .extension() @@ -94,12 +95,14 @@ pub enum BootstrapConfigLoadingError { /// Supported formats include: /// - `.json` /// - `.yaml` or `.yml` + #[cfg(not(target_arch = "wasm32"))] #[error( "Unsupported bootstrap config file format for: {0}. Supported formats include: JSON, YAML" )] InvalidFileFormat(String), /// Error returned when the file cannot be read. + #[cfg(not(target_arch = "wasm32"))] #[error("Failed to read {0}: {1}")] ReadFile(String, io::Error), @@ -130,14 +133,12 @@ pub enum BootstrapConfigLoadingError { MissingPolicyStore, /// Error returned when the policy store file is in an unsupported format. - #[error( - "Unsupported policy store file format for: {0}. Supported formats include: JSON, YAML" - )] + #[error("Unsupported policy store file format for: {0}. Supported formats include: JSON, YAML")] UnsupportedPolicyStoreFileFormat(String), /// Error returned when failing to load a local JWKS #[error("Failed to load local JWKS from {0}: {1}")] - LoadLocalJwks(String, std::io::Error), + LoadLocalJwks(String, String), /// Error returned when both `CEDARLING_USER_AUTHZ` and `CEDARLING_WORKLOAD_AUTHZ` are disabled. /// These two authentication configurations cannot be disabled at the same time. diff --git a/jans-cedarling/cedarling/src/common/app_types.rs b/jans-cedarling/cedarling/src/common/app_types.rs index 2280454f9fc..9228a649e61 100644 --- a/jans-cedarling/cedarling/src/common/app_types.rs +++ b/jans-cedarling/cedarling/src/common/app_types.rs @@ -5,7 +5,7 @@ //! Module that contains structures used as configuration internally in the application //! It is usefull to use it with DI container -use uuid7::{uuid4, Uuid}; +use uuid7::{Uuid, uuid4}; /// Value is used as ID for application /// represents a unique ID for application diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs index 281facefb9d..64f69ef1e9d 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs @@ -106,7 +106,7 @@ mod tests { use action::CtxAttribute; use serde_json::json; - use test_utils::{assert_eq, SortedJson}; + use test_utils::{SortedJson, assert_eq}; use super::entity_types::*; use super::*; @@ -121,139 +121,101 @@ mod tests { serde_json::from_str(json_value).expect("failed to parse json"); let entity_types = HashMap::from_iter(vec![ - ( - "Access_token".to_string(), - CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter(vec![ - ( - "aud".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }, - ), - ( - "exp".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Long".to_string(), - }), - required: true, - }, - ), - ( - "iat".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Primitive(PrimitiveType { - kind: PrimitiveTypeKind::Long, - }), - required: true, - }, - ), - ( - "scope".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Set(Box::new( - SetEntityType { - element: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - }, - )), - - required: false, - }, - ), - ]), - }), - }, - ), - ("Role".to_string(), CedarSchemaEntityShape { shape: None }), - ( - "TrustedIssuer".to_string(), - CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([( - "issuer_entity_id".to_string(), - CedarSchemaEntityAttribute { - required: true, - cedar_type: CedarSchemaEntityType::Typed(EntityType { - name: "Url".to_string(), - kind: "EntityOrCommon".to_string(), - }), - }, - )]), - }), - }, - ), - ("Issue".to_string(), CedarSchemaEntityShape { shape: None }), - ]); - - let common_types = HashMap::from_iter([( - "Url".to_string(), - CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([ - ( - "host".to_string(), - CedarSchemaEntityAttribute { + ("Access_token".to_string(), CedarSchemaEntityShape { + shape: Some(CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter(vec![ + ("aud".to_string(), CedarSchemaEntityAttribute { cedar_type: CedarSchemaEntityType::Typed(EntityType { kind: "EntityOrCommon".to_string(), name: "String".to_string(), }), required: true, - }, - ), - ( - "path".to_string(), - CedarSchemaEntityAttribute { + }), + ("exp".to_string(), CedarSchemaEntityAttribute { cedar_type: CedarSchemaEntityType::Typed(EntityType { kind: "EntityOrCommon".to_string(), - name: "String".to_string(), + name: "Long".to_string(), }), required: true, - }, - ), - ( - "protocol".to_string(), + }), + ("iat".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Primitive(PrimitiveType { + kind: PrimitiveTypeKind::Long, + }), + required: true, + }), + ("scope".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Set(Box::new(SetEntityType { + element: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + })), + + required: false, + }), + ]), + }), + }), + ("Role".to_string(), CedarSchemaEntityShape { shape: None }), + ("TrustedIssuer".to_string(), CedarSchemaEntityShape { + shape: Some(CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter([( + "issuer_entity_id".to_string(), CedarSchemaEntityAttribute { + required: true, cedar_type: CedarSchemaEntityType::Typed(EntityType { + name: "Url".to_string(), kind: "EntityOrCommon".to_string(), - name: "String".to_string(), }), - required: true, }, - ), - ]), - }, - )]); - - let actions = HashMap::from([( - "Update".to_string(), - ActionSchema { - resource_types: HashSet::from(["Issue"].map(|x| x.to_string())), - principal_types: HashSet::from(["Access_token", "Role"].map(|x| x.to_string())), - context: None, - }, - )]); + )]), + }), + }), + ("Issue".to_string(), CedarSchemaEntityShape { shape: None }), + ]); + + let common_types = HashMap::from_iter([("Url".to_string(), CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter([ + ("host".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), + ("path".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), + ("protocol".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), + ]), + })]); + + let actions = HashMap::from([("Update".to_string(), ActionSchema { + resource_types: HashSet::from(["Issue"].map(|x| x.to_string())), + principal_types: HashSet::from(["Access_token", "Role"].map(|x| x.to_string())), + context: None, + })]); let schema_to_compare = CedarSchemaJson { - namespace: HashMap::from_iter(vec![( - "Jans".to_string(), - CedarSchemaEntities { - entity_types, - common_types, - actions, - }, - )]), + namespace: HashMap::from_iter(vec![("Jans".to_string(), CedarSchemaEntities { + entity_types, + common_types, + actions, + })]), }; assert_eq!( diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs index 5b1b6fc67f4..efc98a2ad7e 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs @@ -6,8 +6,8 @@ use std::collections::{HashMap, HashSet}; use serde::ser::SerializeMap; -use serde::{de, Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde::{Deserialize, Serialize, de}; +use serde_json::{Value, json}; use super::entity_types::{ CedarSchemaEntityAttribute, CedarSchemaEntityType, PrimitiveType, PrimitiveTypeKind, @@ -60,7 +60,6 @@ impl Action<'_> { if let Some(ctx_entities) = &self.context_entities { for attr in ctx_entities.iter() { - println!("attr: {:?}", attr); if let CedarType::TypeName(type_name) = &attr.kind { let id = match id_mapping.get(&attr.key) { Some(val) => val, @@ -336,15 +335,15 @@ mod test { use std::collections::{HashMap, HashSet}; use serde::Deserialize; - use serde_json::{json, Value}; + use serde_json::{Value, json}; use super::ActionSchema; + use crate::common::cedar_schema::cedar_json::CedarSchemaRecord; use crate::common::cedar_schema::cedar_json::action::RecordOrType; use crate::common::cedar_schema::cedar_json::entity_types::{ CedarSchemaEntityAttribute, CedarSchemaEntityType, EntityType, PrimitiveType, PrimitiveTypeKind, }; - use crate::common::cedar_schema::cedar_json::CedarSchemaRecord; type ActionType = String; #[derive(Deserialize, Debug, PartialEq)] @@ -371,14 +370,11 @@ mod test { fn build_expected(ctx: Option) -> MockJsonSchema { MockJsonSchema { - actions: HashMap::from([( - "Update".to_string(), - ActionSchema { - resource_types: HashSet::from(["Issue"].map(|s| s.to_string())), - principal_types: HashSet::from(["Workload", "User"].map(|s| s.to_string())), - context: ctx, - }, - )]), + actions: HashMap::from([("Update".to_string(), ActionSchema { + resource_types: HashSet::from(["Issue"].map(|s| s.to_string())), + principal_types: HashSet::from(["Workload", "User"].map(|s| s.to_string())), + context: ctx, + })]), } } @@ -416,26 +412,20 @@ mod test { let expected = build_expected(Some(RecordOrType::Record(CedarSchemaRecord { entity_type: "Record".to_string(), attributes: HashMap::from([ - ( - "token".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Access_token".to_string(), - }), - required: true, - }, - ), - ( - "username".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }, - ), + ("token".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "Access_token".to_string(), + }), + required: true, + }), + ("username".to_string(), CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed(EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }), + required: true, + }), ]), }))); diff --git a/jans-cedarling/cedarling/src/common/policy_store.rs b/jans-cedarling/cedarling/src/common/policy_store.rs index 48ca81d04d3..794a00babd8 100644 --- a/jans-cedarling/cedarling/src/common/policy_store.rs +++ b/jans-cedarling/cedarling/src/common/policy_store.rs @@ -241,10 +241,11 @@ impl<'de> Deserialize<'de> for TokenKind { "id_token" => Ok(TokenKind::Id), "userinfo_token" => Ok(TokenKind::Userinfo), "access_token" => Ok(TokenKind::Access), - _ => Err(serde::de::Error::unknown_variant( - &token_kind, - &["access_token", "id_token", "userinfo_token"], - )), + _ => Err(serde::de::Error::unknown_variant(&token_kind, &[ + "access_token", + "id_token", + "userinfo_token", + ])), } } } diff --git a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs index ac32b442762..d959e724d21 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use regex; use regex::Regex; -use serde::{de, Deserialize}; +use serde::{Deserialize, de}; use serde_json::Value; /// Structure for storing `claim mappings` @@ -281,20 +281,14 @@ mod test { "Acme::Email".to_string(), r#"^(?P[^@]+)@(?P.+)$"#.to_string(), HashMap::from([ - ( - "UID".to_string(), - RegexFieldMapping { - attr: "uid".to_string(), - r#type: RegexFieldMappingType::String, - }, - ), - ( - "DOMAIN".to_string(), - RegexFieldMapping { - attr: "domain".to_string(), - r#type: RegexFieldMappingType::String, - }, - ), + ("UID".to_string(), RegexFieldMapping { + attr: "uid".to_string(), + r#type: RegexFieldMappingType::String, + }), + ("DOMAIN".to_string(), RegexFieldMapping { + attr: "domain".to_string(), + r#type: RegexFieldMappingType::String, + }), ]), ) .expect("regexp should parse correctly"); @@ -361,21 +355,15 @@ mod test { "Acme::Email".to_string(), r#"^(?P[^@]+)@(?P.+)$"#.to_string(), HashMap::from([ - ( - "UID".to_string(), - RegexFieldMapping { - attr: "uid".to_string(), - r#type: RegexFieldMappingType::String, - }, - ), - ( - "DOMAIN".to_string(), - RegexFieldMapping { - attr: "domain".to_string(), - - r#type: RegexFieldMappingType::String, - }, - ), + ("UID".to_string(), RegexFieldMapping { + attr: "uid".to_string(), + r#type: RegexFieldMappingType::String, + }), + ("DOMAIN".to_string(), RegexFieldMapping { + attr: "domain".to_string(), + + r#type: RegexFieldMappingType::String, + }), ]), ) .expect("regexp should parse correctly"); diff --git a/jans-cedarling/cedarling/src/common/policy_store/test.rs b/jans-cedarling/cedarling/src/common/policy_store/test.rs index 97e75f30218..4887fb07a85 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/test.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/test.rs @@ -10,7 +10,7 @@ use serde::Deserialize; use serde_json::json; use test_utils::assert_eq; -use super::{parse_option_string, AgamaPolicyStore, ParsePolicySetMessage, PolicyStore}; +use super::{AgamaPolicyStore, ParsePolicySetMessage, PolicyStore, parse_option_string}; use crate::common::policy_store::parse_cedar_version; /// Tests successful deserialization of a valid policy store JSON. @@ -86,10 +86,12 @@ fn test_base64_decoding_error_in_policy_store() { }); let policy_result = serde_json::from_str::(policy_store_json.to_string().as_str()); - assert!(policy_result - .unwrap_err() - .to_string() - .contains(&ParsePolicySetMessage::Base64.to_string())); + assert!( + policy_result + .unwrap_err() + .to_string() + .contains(&ParsePolicySetMessage::Base64.to_string()) + ); } /// Tests for parsing error due to broken UTF-8 in the policy store. @@ -136,10 +138,12 @@ fn test_policy_parsing_error_in_policy_store() { }); let policy_result = serde_json::from_str::(policy_store_json.to_string().as_str()); - assert!(policy_result - .unwrap_err() - .to_string() - .contains(&ParsePolicySetMessage::String.to_string())); + assert!( + policy_result + .unwrap_err() + .to_string() + .contains(&ParsePolicySetMessage::String.to_string()) + ); } /// Tests for broken policy parsing error in the policy store. diff --git a/jans-cedarling/cedarling/src/http/blocking.rs b/jans-cedarling/cedarling/src/http/blocking.rs deleted file mode 100644 index 90a8a0ad1fb..00000000000 --- a/jans-cedarling/cedarling/src/http/blocking.rs +++ /dev/null @@ -1,77 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::thread::sleep; -use std::time::Duration; - -use reqwest::blocking::Client; - -use super::{HttpClientError, HttpGet, Response}; - -/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality -/// with retry logic. -/// -/// The `HttpClient` struct allows for sending GET requests with a retry mechanism -/// that attempts to fetch the requested resource up to a maximum number of times -/// if an error occurs. -#[derive(Debug)] -pub struct BlockingHttpClient { - client: reqwest::blocking::Client, - max_retries: u32, - retry_delay: Duration, -} - -impl BlockingHttpClient { - pub fn new(max_retries: u32, retry_delay: Duration) -> Result { - let client = Client::builder() - .build() - .map_err(HttpClientError::Initialization)?; - - Ok(Self { - client, - max_retries, - retry_delay, - }) - } -} - -impl HttpGet for BlockingHttpClient { - /// Sends a GET request to the specified URI with retry logic. - /// - /// This method will attempt to fetch the resource up to 3 times, with an increasing delay - /// between each attempt. - fn get(&self, uri: &str) -> Result { - // Fetch the JWKS from the jwks_uri - let mut attempts = 0; - let response = loop { - match self.client.get(uri).send() { - // Exit loop on success - Ok(response) => break response, - - Err(e) if attempts < self.max_retries => { - attempts += 1; - // TODO: pass this message to the logger - eprintln!( - "Request failed (attempt {} of {}): {}. Retrying...", - attempts, self.max_retries, e - ); - sleep(self.retry_delay * attempts); - }, - // Exit if max retries exceeded - Err(e) => return Err(HttpClientError::MaxHttpRetriesReached(e)), - } - }; - - let response = response - .error_for_status() - .map_err(HttpClientError::HttpStatus)?; - - Ok(Response { - text: response - .text() - .map_err(HttpClientError::DecodeResponseUtf8)?, - }) - } -} diff --git a/jans-cedarling/cedarling/src/http/mod.rs b/jans-cedarling/cedarling/src/http/mod.rs index 85cc01ef862..374260cf54b 100644 --- a/jans-cedarling/cedarling/src/http/mod.rs +++ b/jans-cedarling/cedarling/src/http/mod.rs @@ -3,38 +3,74 @@ // // Copyright (c) 2024, Gluu, Inc. -#[cfg(not(target_family = "wasm"))] -mod blocking; -#[cfg(target_family = "wasm")] -mod wasm; - -use std::time::Duration; - +use reqwest::Client; use serde::Deserialize; +use std::time::Duration; -trait HttpGet { - /// Sends a GET request to the specified URI - fn get(&self, uri: &str) -> Result; -} - +/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality +/// with retry logic. +/// +/// The `HttpClient` struct allows for sending GET requests with a retry mechanism +/// that attempts to fetch the requested resource up to a maximum number of times +/// if an error occurs. +#[derive(Debug)] pub struct HttpClient { - client: Box, + client: reqwest::Client, + max_retries: u32, + retry_delay: Duration, } impl HttpClient { pub fn new(max_retries: u32, retry_delay: Duration) -> Result { - #[cfg(not(target_family = "wasm"))] - let client = blocking::BlockingHttpClient::new(max_retries, retry_delay)?; - #[cfg(target_family = "wasm")] - let client = wasm::WasmHttpClient::new(max_retries, retry_delay)?; + let client = Client::builder() + .build() + .map_err(HttpClientError::Initialization)?; Ok(Self { - client: Box::new(client), + client, + max_retries, + retry_delay, }) } +} - pub fn get(&self, uri: &str) -> Result { - self.client.get(uri) +impl HttpClient { + /// Sends a GET request to the specified URI with retry logic. + /// + /// This method will attempt to fetch the resource up to 3 times, with an increasing delay + /// between each attempt. + pub async fn get(&self, uri: &str) -> Result { + // Fetch the JWKS from the jwks_uri + let mut attempts = 0; + let response = loop { + match self.client.get(uri).send().await { + // Exit loop on success + Ok(response) => break response, + + Err(e) if attempts < self.max_retries => { + attempts += 1; + // TODO: pass this message to the logger + eprintln!( + "Request failed (attempt {} of {}): {}. Retrying...", + attempts, self.max_retries, e + ); + tokio::time::sleep(self.retry_delay * attempts).await; + }, + // Exit if max retries exceeded + Err(e) => return Err(HttpClientError::MaxHttpRetriesReached(e)), + } + }; + + let response = response + .error_for_status() + .map_err(HttpClientError::HttpStatus)?; + + Ok(Response { + text: response + .text() + .await + .map_err(HttpClientError::DecodeResponseUtf8)?, + }) } } @@ -75,17 +111,18 @@ pub enum HttpClientError { #[cfg(test)] mod test { - use std::time::Duration; + use crate::http::{HttpClient, HttpClientError}; use mockito::Server; use serde_json::json; + use std::time::Duration; use test_utils::assert_eq; + use tokio; + use tokio::join; - use crate::http::{HttpClient, HttpClientError}; - - #[test] - fn can_fetch() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn can_fetch() { + let mut mock_server = Server::new_async().await; let expected = json!({ "issuer": mock_server.url(), @@ -98,16 +135,16 @@ mod test { .with_header("content-type", "application/json") .with_body(expected.to_string()) .expect(1) - .create(); + .create_async(); let client = HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient."); - let response = client - .get(&format!( - "{}/.well-known/openid-configuration", - mock_server.url() - )) + let link = &format!("{}/.well-known/openid-configuration", mock_server.url()); + let req_fut = client.get(link); + let (req_result, mock_result) = join!(req_fut, mock_endpoint); + + let response = req_result .expect("Should get response") .json::() .expect("Should deserialize JSON response."); @@ -117,14 +154,14 @@ mod test { "Expected: {expected:?}\nBut got: {response:?}" ); - mock_endpoint.assert(); + mock_result.assert(); } - #[test] - fn errors_when_max_http_retries_exceeded() { + #[tokio::test] + async fn errors_when_max_http_retries_exceeded() { let client = HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient"); - let response = client.get("0.0.0.0"); + let response = client.get("0.0.0.0").await; assert!( matches!(response, Err(HttpClientError::MaxHttpRetriesReached(_))), @@ -132,23 +169,23 @@ mod test { ); } - #[test] - fn errors_on_http_error_status() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn errors_on_http_error_status() { + let mut mock_server = Server::new_async().await; - let mock_endpoint = mock_server + let mock_endpoint_fut = mock_server .mock("GET", "/.well-known/openid-configuration") .with_status(500) .expect(1) - .create(); + .create_async(); let client = HttpClient::new(3, Duration::from_millis(1)).expect("Should create HttpClient."); - let response = client.get(&format!( - "{}/.well-known/openid-configuration", - mock_server.url() - )); + let link = &format!("{}/.well-known/openid-configuration", mock_server.url()); + let client_fut = client.get(link); + + let (mock_endpoint, response) = join!(mock_endpoint_fut, client_fut); assert!( matches!(response, Err(HttpClientError::HttpStatus(_))), diff --git a/jans-cedarling/cedarling/src/http/wasm.rs b/jans-cedarling/cedarling/src/http/wasm.rs deleted file mode 100644 index b17d28a5c58..00000000000 --- a/jans-cedarling/cedarling/src/http/wasm.rs +++ /dev/null @@ -1,36 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::time::Duration; - -use super::{HttpClientError, HttpGet, Response}; - -/// A wrapper around `reqwest::blocking::Client` providing HTTP request functionality -/// with retry logic. -/// -/// The `HttpClient` struct allows for sending GET requests with a retry mechanism -/// that attempts to fetch the requested resource up to a maximum number of times -/// if an error occurs. -#[derive(Debug)] -pub struct WasmHttpClient { - _max_retries: u32, - _retry_delay: Duration, -} - -impl WasmHttpClient { - pub fn new(_max_retries: u32, _retry_delay: Duration) -> Result { - todo!(); - } -} - -impl HttpGet for WasmHttpClient { - /// Sends a GET request to the specified URI with retry logic. - /// - /// This method will attempt to fetch the resource up to 3 times, with an increasing delay - /// between each attempt. - fn get(&self, _uri: &str) -> Result { - todo!() - } -} diff --git a/jans-cedarling/cedarling/src/init/policy_store.rs b/jans-cedarling/cedarling/src/init/policy_store.rs index c4259c872e6..6a9af2a8721 100644 --- a/jans-cedarling/cedarling/src/init/policy_store.rs +++ b/jans-cedarling/cedarling/src/init/policy_store.rs @@ -61,7 +61,7 @@ fn extract_first_policy_store( /// Loads the policy store based on the provided configuration. /// /// This function supports multiple sources for loading policies. -pub(crate) fn load_policy_store( +pub(crate) async fn load_policy_store( config: &PolicyStoreConfig, ) -> Result { let policy_store = match &config.source { @@ -76,7 +76,7 @@ pub(crate) fn load_policy_store( extract_first_policy_store(&agama_policy_store)? }, PolicyStoreSource::LockMaster(policy_store_uri) => { - load_policy_store_from_lock_master(policy_store_uri)? + load_policy_store_from_lock_master(policy_store_uri).await? }, PolicyStoreSource::FileJson(path) => { let policy_json = fs::read_to_string(path) @@ -98,11 +98,11 @@ pub(crate) fn load_policy_store( /// Loads the policy store from the Lock Master. /// /// The URI is from the `CEDARLING_POLICY_STORE_URI` bootstrap property. -fn load_policy_store_from_lock_master( +async fn load_policy_store_from_lock_master( uri: &str, ) -> Result { let client = HttpClient::new(3, Duration::from_secs(3))?; - let agama_policy_store = client.get(uri)?.json::()?; + let agama_policy_store = client.get(uri).await?.json::()?; extract_first_policy_store(&agama_policy_store) } @@ -119,29 +119,31 @@ mod test { // works correctly anymore here since we already have tests for those in // src/common/policy_store/test.rs... - #[test] - fn can_load_from_json_file() { + #[tokio::test] + async fn can_load_from_json_file() { load_policy_store(&PolicyStoreConfig { source: crate::PolicyStoreSource::FileJson( Path::new("../test_files/policy-store_generated.json").into(), ), }) + .await .expect("Should load policy store from JSON file"); } - #[test] - fn can_load_from_yaml_file() { + #[tokio::test] + async fn can_load_from_yaml_file() { load_policy_store(&PolicyStoreConfig { source: crate::PolicyStoreSource::FileYaml( Path::new("../test_files/policy-store_ok.yaml").into(), ), }) + .await .expect("Should load policy store from YAML file"); } - #[test] - fn can_load_from_lock_master() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn can_load_from_lock_master() { + let mut mock_server = Server::new_async().await; let policy_store_json = include_str!("../../../test_files/policy-store_lock_master_ok.json").to_string(); @@ -159,6 +161,7 @@ mod test { load_policy_store(&PolicyStoreConfig { source: crate::PolicyStoreSource::LockMaster(uri), }) + .await .expect("Should load policy store from Lock Master file"); mock_endpoint.assert(); diff --git a/jans-cedarling/cedarling/src/init/service_config.rs b/jans-cedarling/cedarling/src/init/service_config.rs index 9abbdd643e8..f6de2deb7ef 100644 --- a/jans-cedarling/cedarling/src/init/service_config.rs +++ b/jans-cedarling/cedarling/src/init/service_config.rs @@ -1,13 +1,14 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ -use bootstrap_config::BootstrapConfig; - -use super::policy_store::{load_policy_store, PolicyStoreLoadError}; +use super::policy_store::{PolicyStoreLoadError, load_policy_store}; use crate::bootstrap_config; use crate::common::policy_store::PolicyStoreWithID; +use bootstrap_config::BootstrapConfig; /// Configuration that hold validated infomation from bootstrap config #[derive(Clone)] @@ -23,8 +24,8 @@ pub enum ServiceConfigError { } impl ServiceConfig { - pub fn new(bootstrap: &BootstrapConfig) -> Result { - let policy_store = load_policy_store(&bootstrap.policy_store_config)?; + pub async fn new(bootstrap: &BootstrapConfig) -> Result { + let policy_store = load_policy_store(&bootstrap.policy_store_config).await?; Ok(Self { policy_store }) } diff --git a/jans-cedarling/cedarling/src/init/service_factory.rs b/jans-cedarling/cedarling/src/init/service_factory.rs index 1403a34f245..9b7ea155cd9 100644 --- a/jans-cedarling/cedarling/src/init/service_factory.rs +++ b/jans-cedarling/cedarling/src/init/service_factory.rs @@ -1,18 +1,21 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ //! Module to lazily initialize internal cedarling services use std::sync::Arc; -use super::service_config::ServiceConfig; -use crate::authz::{Authz, AuthzConfig}; use crate::bootstrap_config::BootstrapConfig; -use crate::common::app_types; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{JwtService, JwtServiceInitError}; + +use super::service_config::ServiceConfig; +use crate::authz::{Authz, AuthzConfig}; +use crate::common::app_types; use crate::log; #[derive(Clone)] @@ -71,20 +74,20 @@ impl<'a> ServiceFactory<'a> { } // get jwt service - pub fn jwt_service(&mut self) -> Result, ServiceInitError> { + pub async fn jwt_service(&mut self) -> Result, ServiceInitError> { if let Some(jwt_service) = &self.container.jwt_service { Ok(jwt_service.clone()) } else { let config = &self.bootstrap_config.jwt_config; let trusted_issuers = self.policy_store().trusted_issuers.clone(); - let service = Arc::new(JwtService::new(config, trusted_issuers)?); + let service = Arc::new(JwtService::new(config, trusted_issuers).await?); self.container.jwt_service = Some(service.clone()); Ok(service) } } // get authz service - pub fn authz_service(&mut self) -> Result, ServiceInitError> { + pub async fn authz_service(&mut self) -> Result, ServiceInitError> { if let Some(authz) = &self.container.authz_service { Ok(authz.clone()) } else { @@ -93,7 +96,7 @@ impl<'a> ServiceFactory<'a> { pdp_id: self.pdp_id(), application_name: self.application_name(), policy_store: self.policy_store(), - jwt_service: self.jwt_service()?, + jwt_service: self.jwt_service().await?, authorization: self.bootstrap_config.authorization_config.clone(), }; let service = Arc::new(Authz::new(config)); diff --git a/jans-cedarling/cedarling/src/jwt/jwk_store.rs b/jans-cedarling/cedarling/src/jwt/jwk_store.rs index a02b693c595..b964fbf41ea 100644 --- a/jans-cedarling/cedarling/src/jwt/jwk_store.rs +++ b/jans-cedarling/cedarling/src/jwt/jwk_store.rs @@ -7,8 +7,9 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::sync::Arc; -use jsonwebtoken::jwk::Jwk; use jsonwebtoken::DecodingKey; +use jsonwebtoken::jwk::Jwk; + use serde::Deserialize; use serde_json::Value; use time::OffsetDateTime; @@ -153,19 +154,21 @@ impl JwkStore { } /// Creates a JwkStore by fetching the keys from the given [`TrustedIssuer`]. - pub fn new_from_trusted_issuer( + pub async fn new_from_trusted_issuer( store_id: TrustedIssuerId, issuer: &TrustedIssuer, http_client: &HttpClient, ) -> Result { // fetch openid configuration - let response = http_client.get(&issuer.openid_configuration_endpoint)?; + let response = http_client + .get(&issuer.openid_configuration_endpoint) + .await?; let openid_config = response .json::() .map_err(JwkStoreError::FetchOpenIdConfig)?; // fetch jwks - let response = http_client.get(&openid_config.jwks_uri)?; + let response = http_client.get(&openid_config.jwks_uri).await?; let mut store = Self::new_from_jwks_str(store_id, response.text())?; store.issuer = Some(openid_config.issuer.into()); @@ -233,8 +236,8 @@ mod test { use std::collections::HashMap; use std::time::Duration; - use jsonwebtoken::jwk::JwkSet; use jsonwebtoken::DecodingKey; + use jsonwebtoken::jwk::JwkSet; use mockito::Server; use serde_json::json; use time::OffsetDateTime; @@ -313,9 +316,9 @@ mod test { ); } - #[test] - fn can_load_from_trusted_issuers() { - let mut mock_server = Server::new(); + #[tokio::test] + async fn can_load_from_trusted_issuers() { + let mut mock_server = Server::new_async().await; // Setup OpenId config endpoint let openid_config_json = json!({ @@ -349,10 +352,8 @@ mod test { "alg": "RS256", "kty": "RSA", "kid": kid2, - } + }]}); - ] - }); let jwks_endpoint = mock_server .mock("GET", "/jwks") .with_status(200) @@ -375,6 +376,7 @@ mod test { let mut result = JwkStore::new_from_trusted_issuer("test".into(), &source_iss, &http_client) + .await .expect("Should load JwkStore from Trusted Issuer"); // We edit the `last_updated` from the result so that the comparison // wont fail because of the timestamp. diff --git a/jans-cedarling/cedarling/src/jwt/key_service.rs b/jans-cedarling/cedarling/src/jwt/key_service.rs index 0f794d77bad..df36cf3d92a 100644 --- a/jans-cedarling/cedarling/src/jwt/key_service.rs +++ b/jans-cedarling/cedarling/src/jwt/key_service.rs @@ -8,10 +8,10 @@ use std::sync::Arc; use std::time::Duration; use jsonwebtoken::DecodingKey; -use serde_json::{json, Value}; +use serde_json::{Value, json}; -use super::jwk_store::{JwkStore, JwkStoreError}; use super::TrustedIssuerId; +use super::jwk_store::{JwkStore, JwkStoreError}; use crate::common::policy_store::TrustedIssuer; use crate::http::{HttpClient, HttpClientError}; @@ -64,7 +64,7 @@ impl KeyService { /// Loads key stores using a JSON string. /// /// Enables loading key stores from a local JSON file. - pub fn new_from_trusted_issuers( + pub async fn new_from_trusted_issuers( trusted_issuers: &HashMap, ) -> Result { let http_client = HttpClient::new(3, Duration::from_secs(3))?; @@ -74,7 +74,7 @@ impl KeyService { let iss_id: Arc = iss_id.as_str().into(); key_stores.insert( iss_id.clone(), - JwkStore::new_from_trusted_issuer(iss_id, iss, &http_client)?, + JwkStore::new_from_trusted_issuer(iss_id, iss, &http_client).await?, ); } @@ -173,12 +173,12 @@ mod test { ); } - #[test] - fn can_load_jwk_stores_from_multiple_trusted_issuers() { + #[tokio::test] + async fn can_load_jwk_stores_from_multiple_trusted_issuers() { let kid1 = "a50f6e70ef4b548a5fd9142eecd1fb8f54dce9ee"; let kid2 = "73e25f9789119c7875d58087a78ac23f5ef2eda3"; - let mut mock_server = Server::new(); + let mut mock_server = Server::new_async().await; // Setup first OpenID config endpoint let openid_config_endpoint1 = mock_server @@ -247,31 +247,26 @@ mod test { .create(); let key_service = KeyService::new_from_trusted_issuers(&HashMap::from([ - ( - "first".to_string(), - TrustedIssuer { - name: "First IDP".to_string(), - description: "".to_string(), - openid_configuration_endpoint: format!( - "{}/first/.well-known/openid-configuration", - mock_server.url() - ), - ..Default::default() - }, - ), - ( - "second".to_string(), - TrustedIssuer { - name: "Second IDP".to_string(), - description: "".to_string(), - openid_configuration_endpoint: format!( - "{}/second/.well-known/openid-configuration", - mock_server.url() - ), - ..Default::default() - }, - ), + ("first".to_string(), TrustedIssuer { + name: "First IDP".to_string(), + description: "".to_string(), + openid_configuration_endpoint: format!( + "{}/first/.well-known/openid-configuration", + mock_server.url() + ), + ..Default::default() + }), + ("second".to_string(), TrustedIssuer { + name: "Second IDP".to_string(), + description: "".to_string(), + openid_configuration_endpoint: format!( + "{}/second/.well-known/openid-configuration", + mock_server.url() + ), + ..Default::default() + }), ])) + .await .expect("Should load KeyService from trusted issuers"); assert!( diff --git a/jans-cedarling/cedarling/src/jwt/mod.rs b/jans-cedarling/cedarling/src/jwt/mod.rs index 97d88d9d822..9f671ac871e 100644 --- a/jans-cedarling/cedarling/src/jwt/mod.rs +++ b/jans-cedarling/cedarling/src/jwt/mod.rs @@ -99,7 +99,7 @@ pub struct JwtService { } impl JwtService { - pub fn new( + pub async fn new( config: &JwtConfig, trusted_issuers: Option>, ) -> Result { @@ -110,6 +110,7 @@ impl JwtService { // Case: Trusted issuers provided (true, None, Some(issuers)) => Some( KeyService::new_from_trusted_issuers(issuers) + .await .map_err(JwtServiceInitError::KeyService)?, ), // Case: Local JWKS provided @@ -177,7 +178,7 @@ impl JwtService { }) } - pub fn process_token<'a>( + pub async fn process_token<'a>( &'a self, token: TokenStr<'a>, ) -> Result, JwtProcessingError> { @@ -217,13 +218,14 @@ mod test { use jsonwebtoken::Algorithm; use serde_json::json; use test_utils::assert_eq; + use tokio::test; use super::test_utils::*; use super::{JwtService, Token, TokenClaims, TokenStr}; use crate::{IdTokenTrustMode, JwtConfig, TokenValidationConfig}; #[test] - pub fn can_validate_token() { + pub async fn can_validate_token() { // Generate token let keys = generate_keypair_hs256(Some("some_hs256_key")).expect("Should generate keys"); let access_tkn_claims = json!({ @@ -270,11 +272,13 @@ mod test { }, None, ) + .await .expect("Should create JwtService"); // Test access_token let access_tkn = jwt_service .process_token(TokenStr::Access(&access_tkn)) + .await .expect("Should process access_token"); let expected_claims = serde_json::from_value::(access_tkn_claims) .expect("Should create expected access_token claims"); @@ -283,6 +287,7 @@ mod test { // Test id_token let id_tkn = jwt_service .process_token(TokenStr::Id(&id_tkn)) + .await .expect("Should process id_token"); let expected_claims = serde_json::from_value::(id_tkn_claims) .expect("Should create expected id_token claims"); @@ -291,6 +296,7 @@ mod test { // Test userinfo_token let userinfo_tkn = jwt_service .process_token(TokenStr::Userinfo(&userinfo_tkn)) + .await .expect("Should process userinfo_token"); let expected_claims = serde_json::from_value::(userinfo_tkn_claims) .expect("Should create expected userinfo_token claims"); diff --git a/jans-cedarling/cedarling/src/jwt/validator.rs b/jans-cedarling/cedarling/src/jwt/validator.rs index 55464cf42e5..6391e17812d 100644 --- a/jans-cedarling/cedarling/src/jwt/validator.rs +++ b/jans-cedarling/cedarling/src/jwt/validator.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use base64::prelude::*; pub use config::*; -use jsonwebtoken::{self as jwt, decode_header, Algorithm, Validation}; +use jsonwebtoken::{self as jwt, Algorithm, Validation, decode_header}; use serde_json::Value; use url::Url; diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 79633d4e9a7..7f60bcc1d6c 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -21,22 +21,26 @@ mod jwt; mod lock; mod log; +#[cfg(not(target_arch = "wasm32"))] +#[cfg(feature = "blocking")] +pub mod blocking; + #[doc(hidden)] #[cfg(test)] mod tests; use std::sync::Arc; -pub use authz::request::{Request, ResourceData, Tokens}; #[cfg(test)] use authz::AuthorizeEntitiesData; use authz::Authz; +pub use authz::request::{Request, ResourceData, Tokens}; pub use authz::{AuthorizeError, AuthorizeResult}; pub use bootstrap_config::*; use common::app_types; +use init::ServiceFactory; use init::service_config::{ServiceConfig, ServiceConfigError}; use init::service_factory::ServiceInitError; -use init::ServiceFactory; use log::interface::LogWriter; use log::{LogEntry, LogType}; pub use log::{LogLevel, LogStorage}; @@ -62,6 +66,13 @@ pub enum InitCedarlingError { /// Error while initializing a Service #[error(transparent)] ServiceInit(#[from] ServiceInitError), + /// Error while parse [`BootstrapConfigRaw`] + #[error(transparent)] + BootstrapConfigLoading(#[from] BootstrapConfigLoadingError), + #[cfg(feature = "blocking")] + /// Error while init tokio runtime + #[error(transparent)] + RuntimeInit(std::io::Error), } /// The instance of the Cedarling application. @@ -74,11 +85,12 @@ pub struct Cedarling { impl Cedarling { /// Create a new instance of the Cedarling application. - pub fn new(config: &BootstrapConfig) -> Result { + pub async fn new(config: &BootstrapConfig) -> Result { let log = log::init_logger(&config.log_config); let pdp_id = app_types::PdpID::new(); let service_config = ServiceConfig::new(config) + .await .inspect(|_| { log.log( LogEntry::new_with_data(pdp_id, None, LogType::System) @@ -99,36 +111,36 @@ impl Cedarling { Ok(Cedarling { log, - authz: service_factory.authz_service()?, + authz: service_factory.authz_service().await?, }) } /// Authorize request /// makes authorization decision based on the [`Request`] - pub fn authorize(&self, request: Request) -> Result { - self.authz.authorize(request) + pub async fn authorize(&self, request: Request) -> Result { + self.authz.authorize(request).await } /// Get entites derived from `cedar-policy` schema and tokens for `authorize` request. #[doc(hidden)] #[cfg(test)] - pub fn authorize_entities_data( + pub async fn authorize_entities_data( &self, request: &Request, ) -> Result { - let tokens = self.authz.decode_tokens(request)?; - self.authz.build_entities(request, &tokens) + let tokens = self.authz.decode_tokens(request).await?; + self.authz.build_entities(request, &tokens).await } } // implements LogStorage for Cedarling // we can use this methods outside crate only when import trait impl LogStorage for Cedarling { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { self.log.pop_logs() } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { self.log.get_log_by_id(id) } diff --git a/jans-cedarling/cedarling/src/log/interface.rs b/jans-cedarling/cedarling/src/log/interface.rs index cd529e1f422..eb2a07bcb0e 100644 --- a/jans-cedarling/cedarling/src/log/interface.rs +++ b/jans-cedarling/cedarling/src/log/interface.rs @@ -53,10 +53,10 @@ pub(crate) trait Loggable: serde::Serialize { /// interface for getting log entries from the storage pub trait LogStorage { /// return logs and remove them from the storage - fn pop_logs(&self) -> Vec; + fn pop_logs(&self) -> Vec; /// get specific log entry - fn get_log_by_id(&self, id: &str) -> Option; + fn get_log_by_id(&self, id: &str) -> Option; /// returns a list of all log ids fn get_log_ids(&self) -> Vec; diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index 78639461369..b3d4d009a2e 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -10,10 +10,10 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::hash::Hash; -use uuid7::{uuid7, Uuid}; +use uuid7::Uuid; -use super::interface::Loggable; use super::LogLevel; +use super::interface::Loggable; use crate::bootstrap_config::AuthorizationConfig; use crate::common::app_types::{self, ApplicationName}; use crate::common::policy_store::PoliciesContainer; @@ -29,6 +29,7 @@ pub struct LogEntry { /// it is unwrap to flatten structure #[serde(flatten)] pub base: BaseLogEntry, + /// message of the event pub msg: String, /// name of application from [bootstrap properties](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties) @@ -195,6 +196,12 @@ impl From for Decision { } } +impl From for Decision { + fn from(value: bool) -> Self { + if value { Self::Allow } else { Self::Deny } + } +} + /// An error occurred when evaluating a policy #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct PolicyEvaluationError { @@ -229,6 +236,37 @@ pub struct Diagnostics { pub errors: Vec, } +/// DiagnosticsRefs structure actually same as Diagnostics but hold reference on data +/// And allows to not clone data. +/// Usefull for logging. +#[derive(Debug, Default, Clone, PartialEq, serde::Serialize)] +pub struct DiagnosticsRefs<'a> { + /// `PolicyId`s of the policies that contributed to the decision. + /// If no policies applied to the request, this set will be empty. + pub reason: HashSet<&'a PolicyInfo>, + /// Errors that occurred during authorization. The errors should be + /// treated as unordered, since policies may be evaluated in any order. + pub errors: Vec<&'a PolicyEvaluationError>, +} + +impl DiagnosticsRefs<'_> { + pub fn new<'a>(diagnostics: &[&'a Option<&Diagnostics>]) -> DiagnosticsRefs<'a> { + let policy_info_iter = diagnostics + .iter() + .filter_map(|diagnostic_opt| diagnostic_opt.map(|diagnostic| &diagnostic.reason)) + .flatten(); + let diagnostic_err_iter = diagnostics + .iter() + .filter_map(|diagnostic_opt| diagnostic_opt.map(|diagnostic| &diagnostic.errors)) + .flatten(); + + DiagnosticsRefs { + reason: HashSet::from_iter(policy_info_iter), + errors: diagnostic_err_iter.collect(), + } + } +} + /// Policy diagnostic info #[derive(Debug, Default, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct PolicyInfo { @@ -289,6 +327,8 @@ pub struct DecisionLogEntry<'a> { /// If this Cedarling has registered with a Lock Server, what is the client_id it received #[serde(skip_serializing_if = "Option::is_none")] pub lock_client_id: Option, + /// diagnostic info about policy and errors as result of cedarling + pub diagnostics: DiagnosticsRefs<'a>, /// action UID for request pub action: String, /// resource UID for request @@ -298,7 +338,7 @@ pub struct DecisionLogEntry<'a> { /// Dictionary with the token type and claims which should be included in the log pub tokens: LogTokensInfo<'a>, /// time in milliseconds spent for decision - pub decision_time_ms: u128, + pub decision_time_ms: i64, } impl Loggable for &DecisionLogEntry<'_> { @@ -311,6 +351,30 @@ impl Loggable for &DecisionLogEntry<'_> { } } +/// Custom uuid generation function to avoid using std::time because it makes panic in WASM +// +// TODO: maybe using wasm we can use `js_sys::Date::now()` +// Static variable initialize only once at start of program and available during all program live cycle. +// Import inside function guarantee that it is used only inside function. +fn gen_uuid7() -> Uuid { + use std::sync::{LazyLock, Mutex}; + use uuid7::V7Generator; + + static GLOBAL_V7_GENERATOR: LazyLock< + Mutex>>, + > = LazyLock::new(|| Mutex::new(V7Generator::with_rand08(rand::rngs::OsRng))); + + let mut g = GLOBAL_V7_GENERATOR.lock().expect("mutex should be locked"); + + let custom_unix_ts_ms = chrono::Utc::now().timestamp_millis(); + + // from docs + // The rollback_allowance parameter specifies the amount of unix_ts_ms rollback that is considered significant. + // A suggested value is 10_000 (milliseconds). + const ROLLBACK_ALLOWANCE: u64 = 10_000; + g.generate_or_reset_core(custom_unix_ts_ms as u64, ROLLBACK_ALLOWANCE) +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct BaseLogEntry { /// unique identifier for this event @@ -342,7 +406,7 @@ impl BaseLogEntry { // We use uuid v7 because it is generated based on the time and sortable. // and we need sortable ids to use it in the sparkv database. // Sparkv store data in BTree. So we need have correct order of ids. - request_id: uuid7(), + request_id: gen_uuid7(), timestamp: Some(local_time_string), log_kind: log_type, pdp_id: pdp_id.0, diff --git a/jans-cedarling/cedarling/src/log/log_strategy.rs b/jans-cedarling/cedarling/src/log/log_strategy.rs index c2c96134fc3..0e06e19ee1b 100644 --- a/jans-cedarling/cedarling/src/log/log_strategy.rs +++ b/jans-cedarling/cedarling/src/log/log_strategy.rs @@ -7,7 +7,6 @@ use super::interface::{LogStorage, LogWriter, Loggable}; use super::memory_logger::MemoryLogger; use super::nop_logger::NopLogger; use super::stdout_logger::StdOutLogger; -use super::LogEntry; use crate::bootstrap_config::log_config::{LogConfig, LogTypeConfig}; /// LogStrategy implements strategy pattern for logging. @@ -47,14 +46,14 @@ impl LogWriter for LogStrategy { // Implementation of LogStorage // for cases where we not use memory logger we return default value impl LogStorage for LogStrategy { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { match self { Self::MemoryLogger(memory_logger) => memory_logger.pop_logs(), _ => Vec::new(), } } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { match self { Self::MemoryLogger(memory_logger) => memory_logger.get_log_by_id(id), _ => None, diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs index bb0f1aacb4b..e744a559ec6 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -3,13 +3,13 @@ // // Copyright (c) 2024, Gluu, Inc. +use chrono::Duration; use std::sync::Mutex; -use std::time::Duration; use sparkv::{Config as ConfigSparKV, SparKV}; +use super::LogLevel; use super::interface::{LogStorage, LogWriter, Loggable}; -use super::{LogEntry, LogLevel}; use crate::bootstrap_config::log_config::MemoryLogConfig; const STORAGE_MUTEX_EXPECT_MESSAGE: &str = "MemoryLogger storage mutex should unlock"; @@ -25,7 +25,11 @@ pub(crate) struct MemoryLogger { impl MemoryLogger { pub fn new(config: MemoryLogConfig, log_level: LogLevel) -> Self { let sparkv_config = ConfigSparKV { - default_ttl: Duration::from_secs(config.log_ttl), + default_ttl: Duration::new( + config.log_ttl.try_into().expect("u64 that fits in a i64"), + 0, + ) + .expect("a valid duration"), ..Default::default() }; @@ -61,7 +65,7 @@ impl LogWriter for MemoryLogger { // Implementation of LogStorage impl LogStorage for MemoryLogger { - fn pop_logs(&self) -> Vec { + fn pop_logs(&self) -> Vec { // TODO: implement more efficient implementation let mut storage_guard = self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE); @@ -71,16 +75,16 @@ impl LogStorage for MemoryLogger { keys.iter() .filter_map(|key| storage_guard.pop(key)) // we call unwrap, because we know that the value is valid json - .map(|str_json| serde_json::from_str::(str_json.as_str()) + .map(|str_json| serde_json::from_str::(str_json.as_str()) .expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) .collect() } - fn get_log_by_id(&self, id: &str) -> Option { + fn get_log_by_id(&self, id: &str) -> Option { self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE) .get(id) // we call unwrap, because we know that the value is valid json - .map(|str_json| serde_json::from_str::(str_json.as_str()).expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) + .map(|str_json| serde_json::from_str::(str_json.as_str()).expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) } fn get_log_ids(&self) -> Vec { @@ -130,32 +134,40 @@ mod tests { LogType::System, ); + assert!( + entry1.base.request_id < entry2.base.request_id, + "entry1.base.request_id should be lower than in entry2" + ); + // log entries logger.log(entry1.clone()); logger.log(entry2.clone()); + let entry1_json = serde_json::json!(entry1); + let entry2_json = serde_json::json!(entry2); + // check that we have two entries in the log database assert_eq!(logger.get_log_ids().len(), 2); assert_eq!( logger .get_log_by_id(&entry1.get_request_id().to_string()) .unwrap(), - entry1, + entry1_json, "Failed to get log entry by id" ); assert_eq!( logger .get_log_by_id(&entry2.get_request_id().to_string()) .unwrap(), - entry2, + entry2_json, "Failed to get log entry by id" ); // get logs using `pop_logs` let logs = logger.pop_logs(); assert_eq!(logs.len(), 2); - assert_eq!(logs[0], entry1, "First log entry is incorrect"); - assert_eq!(logs[1], entry2, "Second log entry is incorrect"); + assert_eq!(logs[0], entry1_json, "First log entry is incorrect"); + assert_eq!(logs[1], entry2_json, "Second log entry is incorrect"); // check that we have no entries in the log database assert!( @@ -184,11 +196,14 @@ mod tests { logger.log(entry1.clone()); logger.log(entry2.clone()); + let entry1_json = serde_json::json!(entry1); + let entry2_json = serde_json::json!(entry2); + // check that we have two entries in the log database let logs = logger.pop_logs(); assert_eq!(logs.len(), 2); - assert_eq!(logs[0], entry1, "First log entry is incorrect"); - assert_eq!(logs[1], entry2, "Second log entry is incorrect"); + assert_eq!(logs[0], entry1_json, "First log entry is incorrect"); + assert_eq!(logs[1], entry2_json, "Second log entry is incorrect"); // check that we have no entries in the log database assert!( diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/mod.rs b/jans-cedarling/cedarling/src/log/stdout_logger/mod.rs new file mode 100644 index 00000000000..d4d10c2aa0d --- /dev/null +++ b/jans-cedarling/cedarling/src/log/stdout_logger/mod.rs @@ -0,0 +1,16 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +// conditionally compile logger for native platform and WASM + +#[cfg(not(target_arch = "wasm32"))] +mod native_logger; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use native_logger::*; + +#[cfg(target_arch = "wasm32")] +mod wasm_logger; +#[cfg(target_arch = "wasm32")] +pub(crate) use wasm_logger::*; diff --git a/jans-cedarling/cedarling/src/log/stdout_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/native_logger.rs similarity index 91% rename from jans-cedarling/cedarling/src/log/stdout_logger.rs rename to jans-cedarling/cedarling/src/log/stdout_logger/native_logger.rs index 68b0c55f50c..60d828873e5 100644 --- a/jans-cedarling/cedarling/src/log/stdout_logger.rs +++ b/jans-cedarling/cedarling/src/log/stdout_logger/native_logger.rs @@ -6,8 +6,8 @@ use std::io::Write; use std::sync::{Arc, Mutex}; -use super::interface::{LogWriter, Loggable}; -use super::LogLevel; +use crate::log::LogLevel; +use crate::log::interface::{LogWriter, Loggable}; /// A logger that write to std output. pub(crate) struct StdOutLogger { @@ -37,6 +37,7 @@ impl StdOutLogger { // Implementation of LogWriter impl LogWriter for StdOutLogger { + #[cfg(not(target_arch = "wasm32"))] fn log_any(&self, entry: T) { if !entry.can_log(self.log_level) { // do nothing @@ -55,6 +56,14 @@ impl LogWriter for StdOutLogger { ) .unwrap(); } + + #[cfg(target_arch = "wasm32")] + fn log_any(&self, entry: T) { + if !entry.can_log(self.log_level) { + // do nothing + return; + } + } } // Test writer created for mocking LogWriter @@ -93,9 +102,9 @@ impl Write for TestWriter { mod tests { use std::io::Write; - use super::super::{LogEntry, LogType}; use super::*; use crate::common::app_types::PdpID; + use crate::log::{LogEntry, LogType}; #[test] fn write_log_ok() { diff --git a/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs new file mode 100644 index 00000000000..542b51fb036 --- /dev/null +++ b/jans-cedarling/cedarling/src/log/stdout_logger/wasm_logger.rs @@ -0,0 +1,61 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::log::LogLevel; +use crate::log::interface::{LogWriter, Loggable}; + +use web_sys::console; +use web_sys::js_sys::Array; +use web_sys::wasm_bindgen::JsValue; + +/// A logger that write to std output. +pub(crate) struct StdOutLogger { + log_level: LogLevel, +} + +impl StdOutLogger { + pub(crate) fn new(log_level: LogLevel) -> Self { + Self { log_level } + } +} + +// Implementation of LogWriter +impl LogWriter for StdOutLogger { + fn log_any(&self, entry: T) { + if !entry.can_log(self.log_level) { + // do nothing + return; + } + + let json_string = serde_json::json!(entry).to_string(); + let js_string = JsValue::from(json_string); + + let js_array = Array::new(); + js_array.push(&js_string); + + match entry.get_log_level() { + Some(LogLevel::FATAL) => { + // error is highest level of logging + console::error(&js_array); + }, + Some(LogLevel::ERROR) => { + console::error(&js_array); + }, + Some(LogLevel::WARN) => { + console::warn(&js_array); + }, + Some(LogLevel::INFO) => { + console::info(&js_array); + }, + Some(LogLevel::DEBUG) => { + console::debug(&js_array); + }, + Some(LogLevel::TRACE) => { + console::trace(&js_array); + }, + None => console::log(&js_array), + } + } +} diff --git a/jans-cedarling/cedarling/src/log/test.rs b/jans-cedarling/cedarling/src/log/test.rs index 10fb54ab8a2..c1a0c31627b 100644 --- a/jans-cedarling/cedarling/src/log/test.rs +++ b/jans-cedarling/cedarling/src/log/test.rs @@ -12,6 +12,7 @@ use std::io::Write; use interface::{LogWriter, Loggable}; use nop_logger::NopLogger; use stdout_logger::StdOutLogger; +use test_utils::assert_eq; use super::*; use crate::bootstrap_config::log_config; @@ -115,28 +116,31 @@ fn test_log_memory_logger() { strategy.log(entry1.clone()); strategy.log(entry2.clone()); + let entry1_json = serde_json::json!(entry1); + let entry2_json = serde_json::json!(entry2); + // check that we have two entries in the log database assert_eq!(strategy.get_log_ids().len(), 2); assert_eq!( strategy .get_log_by_id(&entry1.get_request_id().to_string()) .unwrap(), - entry1, + entry1_json, "Failed to get log entry by id" ); assert_eq!( strategy .get_log_by_id(&entry2.get_request_id().to_string()) .unwrap(), - entry2, + entry2_json, "Failed to get log entry by id" ); // get logs using `pop_logs` let logs = strategy.pop_logs(); assert_eq!(logs.len(), 2); - assert_eq!(logs[0], entry1, "First log entry is incorrect"); - assert_eq!(logs[1], entry2, "Second log entry is incorrect"); + assert_eq!(logs[0], entry1_json, "First log entry is incorrect"); + assert_eq!(logs[1], entry2_json, "Second log entry is incorrect"); // check that we have no entries in the log database assert!( diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs index 57330ab2240..29af3b0835e 100644 --- a/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_different_principals.rs @@ -6,14 +6,15 @@ //! In this module we test authorize different action //! where not all principals can be applied //! -//! all case scenario should have `result.is_allowed() == true` +//! all case scenario should have `result.decision == true` //! because we have checked different scenarios in `cases_authorize_without_check_jwt.rs` use lazy_static::lazy_static; use test_utils::assert_eq; +use tokio::test; use super::utils::*; -use crate::{cmp_decision, cmp_policy, WorkloadBoolOp}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ +use crate::{WorkloadBoolOp, cmp_decision, cmp_policy}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_2.yaml"); @@ -59,14 +60,15 @@ lazy_static! { /// Check if action executes for next principals: Workload, User #[test] -fn success_test_for_all_principals() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_for_all_principals() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"Update\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -92,12 +94,12 @@ fn success_test_for_all_principals() { "reason of permit person should be '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload #[test] -fn success_test_for_principal_workload() { +async fn success_test_for_principal_workload() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -106,13 +108,15 @@ fn success_test_for_principal_workload() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForWorkload\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -128,12 +132,12 @@ fn success_test_for_principal_workload() { assert!(result.person.is_none(), "result for person should be none"); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: User #[test] -fn success_test_for_principal_user() { +async fn success_test_for_principal_user() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -142,13 +146,15 @@ fn success_test_for_principal_user() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForUser\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -167,13 +173,13 @@ fn success_test_for_principal_user() { "result for workload should be none" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Person (only) /// check for user and role #[test] -fn success_test_for_principal_person_role() { +async fn success_test_for_principal_person_role() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -182,13 +188,15 @@ fn success_test_for_principal_person_role() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForUserAndRole\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_policy!( @@ -208,12 +216,12 @@ fn success_test_for_principal_person_role() { "result for workload should be none" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload AND Person (Role) #[test] -fn success_test_for_principal_workload_role() { +async fn success_test_for_principal_workload_role() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -222,13 +230,15 @@ fn success_test_for_principal_workload_role() { user_workload_operator: WorkloadBoolOp::And, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForWorkloadAndRole\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -253,13 +263,13 @@ fn success_test_for_principal_workload_role() { "reason of permit person should be '3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload (true) OR Person (false) /// is used operator OR #[test] -fn success_test_for_principal_workload_true_or_user_false() { +async fn success_test_for_principal_workload_true_or_user_false() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -268,13 +278,15 @@ fn success_test_for_principal_workload_true_or_user_false() { user_workload_operator: WorkloadBoolOp::Or, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForWorkload\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -299,13 +311,13 @@ fn success_test_for_principal_workload_true_or_user_false() { "reason of permit person should be empty" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload (false) OR Person (true) /// is used operator OR #[test] -fn success_test_for_principal_workload_false_or_user_true() { +async fn success_test_for_principal_workload_false_or_user_true() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -314,13 +326,15 @@ fn success_test_for_principal_workload_false_or_user_true() { user_workload_operator: WorkloadBoolOp::Or, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"UpdateForUser\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -345,13 +359,13 @@ fn success_test_for_principal_workload_false_or_user_true() { "reason of permit person should be '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if action executes for next principals: Workload (false) OR Person (false) /// is used operator OR #[test] -fn success_test_for_principal_workload_false_or_user_false() { +async fn success_test_for_principal_workload_false_or_user_false() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -360,13 +374,15 @@ fn success_test_for_principal_workload_false_or_user_false() { user_workload_operator: WorkloadBoolOp::Or, ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"AlwaysDeny\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -391,12 +407,12 @@ fn success_test_for_principal_workload_false_or_user_false() { "reason of permit person should be empty" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } /// Check if action executes when principal workload can't be applied #[test] -fn test_where_principal_workload_cant_be_applied() { +async fn test_where_principal_workload_cant_be_applied() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -405,13 +421,15 @@ fn test_where_principal_workload_cant_be_applied() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"NoApplies\"".to_string(); let result = cedarling .authorize(request) + .await .expect_err("request should be parsed with error"); assert!(matches!( @@ -422,7 +440,7 @@ fn test_where_principal_workload_cant_be_applied() { /// Check if action executes when principal user can't be applied #[test] -fn test_where_principal_user_cant_be_applied() { +async fn test_where_principal_user_cant_be_applied() { let cedarling = get_cedarling_with_authorization_conf( PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()), crate::AuthorizationConfig { @@ -431,13 +449,15 @@ fn test_where_principal_user_cant_be_applied() { user_workload_operator: Default::default(), ..Default::default() }, - ); + ) + .await; let mut request = AuthRequestBase.clone(); request.action = "Jans::Action::\"NoApplies\"".to_string(); let result = cedarling .authorize(request) + .await .expect_err("request should be parsed with error"); assert!( diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs index 853e5e94313..bfb69114ba3 100644 --- a/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_namespace_jans2.rs @@ -4,9 +4,10 @@ // Copyright (c) 2024, Gluu, Inc. use test_utils::assert_eq; +use tokio::test; use super::utils::*; -use crate::{cmp_decision, cmp_policy}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ +use crate::{cmp_decision, cmp_policy}; // macros is defined in the cedarling\src\tests\utils\cedarling_util.rs static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_namespace_Jans2.yaml"); @@ -15,8 +16,8 @@ static POLICY_STORE_RAW_YAML: &str = /// In previous we hardcoded creating entities in namespace `Jans` /// in `POLICY_STORE_RAW_YAML` is used namespace `Jans2` #[test] -fn test_namespace_jans2() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn test_namespace_jans2() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -58,6 +59,7 @@ fn test_namespace_jans2() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -84,5 +86,5 @@ fn test_namespace_jans2() { "reason of permit person should be '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs index 496415fedc9..53b9eba0b37 100644 --- a/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs @@ -4,9 +4,10 @@ // Copyright (c) 2024, Gluu, Inc. use test_utils::assert_eq; +use tokio::test; use super::utils::*; -use crate::{cmp_decision, cmp_policy}; /* macros is defined in the cedarling\src\tests\utils\cedarling_util.rs */ +use crate::{cmp_decision, cmp_policy}; // macros is defined in the cedarling\src\tests\utils\cedarling_util.rs static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_2.yaml"); static POLICY_STORE_ABAC_YAML: &str = include_str!("../../../test_files/policy-store_ok_abac.yaml"); @@ -18,8 +19,8 @@ static POLICY_STORE_ABAC_YAML: &str = include_str!("../../../test_files/policy-s /// we check here that field are parsed from JWT tokens /// and correctly executed using correct cedar-policy id #[test] -fn success_test_role_string() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_role_string() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -61,6 +62,7 @@ fn success_test_role_string() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -86,7 +88,7 @@ fn success_test_role_string() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// forbid test case where all check of role is forbid @@ -96,8 +98,8 @@ fn success_test_role_string() { /// we check here that field are parsed from JWT tokens /// and correctly executed using correct cedar-policy id #[test] -fn forbid_test_role_guest() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn forbid_test_role_guest() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -139,6 +141,7 @@ fn forbid_test_role_guest() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -164,7 +167,7 @@ fn forbid_test_role_guest() { "reason of permit person should be '2' and '4'" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } /// Success test case where all check a successful @@ -174,8 +177,8 @@ fn forbid_test_role_guest() { /// we check here that field are parsed from JWT tokens /// and correctly executed using correct cedar-policy id #[test] -fn success_test_role_array() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_role_array() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -217,6 +220,7 @@ fn success_test_role_array() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -243,7 +247,7 @@ fn success_test_role_array() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Success test case where all check a successful @@ -253,8 +257,8 @@ fn success_test_role_array() { /// and correctly executed using correct cedar-policy id /// if role field is not present, just ignore role check #[test] -fn success_test_no_role() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_no_role() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -297,6 +301,7 @@ fn success_test_no_role() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -324,7 +329,7 @@ fn success_test_no_role() { ); assert!( - result.is_allowed(), + result.decision, "request result should be allowed, because workload and user allowed" ); } @@ -334,8 +339,8 @@ fn success_test_no_role() { /// we check here that field for `Jans::User` is present in `id_token` /// it is `country` field of `Jans::User` and role field is present #[test] -fn success_test_user_data_in_id_token() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn success_test_user_data_in_id_token() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -378,6 +383,7 @@ fn success_test_user_data_in_id_token() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -404,13 +410,13 @@ fn success_test_user_data_in_id_token() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } // check all forbid #[test] -fn all_forbid() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn all_forbid() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -456,6 +462,7 @@ fn all_forbid() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -482,13 +489,13 @@ fn all_forbid() { "reason of forbid person should empty, no forbid rule" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only workload permit and other not #[test] -fn only_workload_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_workload_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -532,6 +539,7 @@ fn only_workload_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -564,13 +572,13 @@ fn only_workload_permit() { "reason of forbid person should empty, no forbid rule" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only person permit and other not #[test] -fn only_person_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_person_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -615,6 +623,7 @@ fn only_person_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -641,13 +650,13 @@ fn only_person_permit() { "reason of forbid person should '2'" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only user role permit and other not #[test] -fn only_user_role_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_user_role_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -691,6 +700,7 @@ fn only_user_role_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -717,13 +727,13 @@ fn only_user_role_permit() { "reason of forbid person '3', permit for role Admin" ); - assert!(!result.is_allowed(), "request result should be not allowed"); + assert!(!result.decision, "request result should be not allowed"); } // check only workload and person permit and role not #[test] -fn only_workload_and_person_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_workload_and_person_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -766,6 +776,7 @@ fn only_workload_and_person_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -792,13 +803,13 @@ fn only_workload_and_person_permit() { "reason of permit person should '2'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } // check only workload and role permit and user not #[test] -fn only_workload_and_role_permit() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn only_workload_and_role_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -841,6 +852,7 @@ fn only_workload_and_role_permit() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -867,12 +879,13 @@ fn only_workload_and_role_permit() { "reason of forbid person should be none, but we have permit for role" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } #[test] -fn success_test_role_string_with_abac() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_ABAC_YAML.to_string())); +async fn success_test_role_string_with_abac() { + let cedarling = + get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_ABAC_YAML.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -925,6 +938,7 @@ fn success_test_role_string_with_abac() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( diff --git a/jans-cedarling/cedarling/src/tests/mapping_entities.rs b/jans-cedarling/cedarling/src/tests/mapping_entities.rs index 97878ca393e..28f11b40192 100644 --- a/jans-cedarling/cedarling/src/tests/mapping_entities.rs +++ b/jans-cedarling/cedarling/src/tests/mapping_entities.rs @@ -12,13 +12,14 @@ use std::collections::HashSet; use std::sync::LazyLock; +use tokio::test; use cedarling_util::get_raw_config; use test_utils::assert_eq; use super::utils::*; use crate::common::policy_store::TokenKind; -use crate::{cmp_decision, cmp_policy, AuthorizeError, Cedarling, CreateCedarEntityError}; +use crate::{AuthorizeError, Cedarling, CreateCedarEntityError, cmp_decision, cmp_policy}; static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_entity_mapping.yaml"); @@ -66,16 +67,19 @@ static REQUEST: LazyLock = LazyLock::new(|| { /// we not specify any mapping to check if it works correctly with default mapping #[test] -fn test_default_mapping() { +async fn test_default_mapping() { let raw_config = get_raw_config(POLICY_STORE_RAW_YAML); let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_decision!( @@ -101,7 +105,7 @@ fn test_default_mapping() { "reason of permit person should be '2','3'" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Validate mapping entities. @@ -112,7 +116,7 @@ fn test_default_mapping() { /// /// Note: Verified that the mapped entity types are present in the logs. #[test] -fn test_custom_mapping() { +async fn test_custom_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_user = Some("MappedUser".to_string()); @@ -123,13 +127,16 @@ fn test_custom_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let mut request = REQUEST.clone(); request.action = "Jans::Action::\"UpdateMappedWorkloadAndUser\"".to_string(); let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); cmp_policy!( @@ -156,12 +163,12 @@ fn test_custom_mapping() { "request result should be allowed for person" ); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } /// Check if we get error on mapping user to undefined entity #[test] -fn test_failed_user_mapping() { +async fn test_failed_user_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); let entity_type = "MappedUserNotExist".to_string(); @@ -170,12 +177,15 @@ fn test_failed_user_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); match err { @@ -206,20 +216,24 @@ fn test_failed_user_mapping() { /// Check if we get error on mapping workload to undefined entity #[test] -fn test_failed_workload_mapping() { +async fn test_failed_workload_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); + let entity_type = "MappedWorkloadNotExist".to_string(); raw_config.mapping_workload = Some(entity_type.clone()); let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); match err { @@ -252,7 +266,7 @@ fn test_failed_workload_mapping() { /// Check if we get error on mapping id_token to undefined entity #[test] -fn test_failed_id_token_mapping() { +async fn test_failed_id_token_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_id_token = Some("MappedIdTokenNotExist".to_string()); @@ -260,12 +274,15 @@ fn test_failed_id_token_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); assert!( @@ -279,7 +296,7 @@ fn test_failed_id_token_mapping() { /// Check if we get error on mapping access_token to undefined entity #[test] -fn test_failed_access_token_mapping() { +async fn test_failed_access_token_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_access_token = Some("MappedAccess_tokenNotExist".to_string()); @@ -287,12 +304,15 @@ fn test_failed_access_token_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); assert!( @@ -306,7 +326,7 @@ fn test_failed_access_token_mapping() { /// Check if we get error on mapping userinfo_token to undefined entity #[test] -fn test_failed_userinfo_token_mapping() { +async fn test_failed_userinfo_token_mapping() { let mut raw_config = get_raw_config(POLICY_STORE_RAW_YAML); raw_config.mapping_userinfo_token = Some("MappedUserinfo_tokenNotExist".to_string()); @@ -314,12 +334,15 @@ fn test_failed_userinfo_token_mapping() { let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = REQUEST.clone(); let err = cedarling .authorize(request) + .await .expect_err("request should be parsed with mapping error"); assert!( @@ -337,13 +360,15 @@ fn test_failed_userinfo_token_mapping() { /// Because we specify mapping from each token in policy store /// We use iss in JWT tokens to enable mapping for trusted issuer in policy store #[test] -fn test_role_many_tokens_mapping() { +async fn test_role_many_tokens_mapping() { let raw_config = get_raw_config(POLICY_STORE_RAW_YAML); let config = crate::BootstrapConfig::from_raw_config(&raw_config) .expect("raw config should parse without errors"); - let cedarling = Cedarling::new(&config).expect("could be created without error"); + let cedarling = Cedarling::new(&config) + .await + .expect("could be created without error"); let request = // deserialize `Request` from json Request::deserialize(serde_json::json!( @@ -392,6 +417,7 @@ fn test_role_many_tokens_mapping() { // iterate over roles that created and filter expected roles let roles_left = cedarling .authorize_entities_data(&request) + .await .expect("should get authorize_entities_data without errors") .roles .into_iter() diff --git a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs index 3a8c4f952d9..a90f7d2b949 100644 --- a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs +++ b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs @@ -3,7 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. -use test_utils::{assert_eq, SortedJson}; +use test_utils::{SortedJson, assert_eq}; +use tokio::test; use super::utils::*; @@ -11,8 +12,8 @@ static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/agama-sto /// Test loading policy store with mappings JWT payload to custom `cedar-entities` types in schema #[test] -fn check_mapping_tokens_data() { - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); +async fn check_mapping_tokens_data() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())).await; // deserialize `Request` from json // JWT tokens payload from using `tarp` with `https://test-casa.gluu.info/.well-known/openid-configuration` @@ -110,6 +111,7 @@ fn check_mapping_tokens_data() { let entities = cedarling .authorize_entities_data(&request) + .await // log err to be human readable .inspect_err(|err| println!("Error: {}", err.to_string())) .expect("request should be parsed without errors"); diff --git a/jans-cedarling/cedarling/src/tests/success_test_json.rs b/jans-cedarling/cedarling/src/tests/success_test_json.rs index 8bb96a27633..83ad2521a85 100644 --- a/jans-cedarling/cedarling/src/tests/success_test_json.rs +++ b/jans-cedarling/cedarling/src/tests/success_test_json.rs @@ -4,17 +4,18 @@ // Copyright (c) 2024, Gluu, Inc. use super::utils::*; +use tokio::test; /// Test success scenario wiht authorization // test duplicate code of example file `authorize.rs` (authorization without JWT validation) #[test] -fn success_test_json() { +async fn success_test_json() { // The human-readable policy and schema file is located in next folder: // `test_files\policy-store_ok` // Is used to check that the JSON policy is loaded correctly static POLICY_STORE_RAW_JSON: &str = include_str!("../../../test_files/policy-store_ok.yaml"); - let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_JSON.to_string())); + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_JSON.to_string())).await; // deserialize `Request` from json let request = Request::deserialize(serde_json::json!( @@ -105,7 +106,8 @@ fn success_test_json() { let result = cedarling .authorize(request) + .await .expect("request should be parsed without errors"); - assert!(result.is_allowed(), "request result should be allowed"); + assert!(result.decision, "request result should be allowed"); } diff --git a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs index df7f7b295e5..7901e385cc9 100644 --- a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs +++ b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs @@ -50,13 +50,14 @@ pub fn get_config(policy_source: PolicyStoreSource) -> BootstrapConfig { } /// create [`Cedarling`] from [`PolicyStoreSource`] -pub fn get_cedarling(policy_source: PolicyStoreSource) -> Cedarling { +pub async fn get_cedarling(policy_source: PolicyStoreSource) -> Cedarling { Cedarling::new(&get_config(policy_source)) + .await .expect("bootstrap config should initialize correctly") } /// create [`Cedarling`] from [`PolicyStoreSource`] -pub fn get_cedarling_with_authorization_conf( +pub async fn get_cedarling_with_authorization_conf( policy_source: PolicyStoreSource, auth_conf: AuthorizationConfig, ) -> Cedarling { @@ -72,6 +73,7 @@ pub fn get_cedarling_with_authorization_conf( jwt_config: JwtConfig::new_without_validation(), authorization_config: auth_conf, }) + .await .expect("bootstrap config should initialize correctly") } diff --git a/jans-cedarling/cedarling/src/tests/utils/mod.rs b/jans-cedarling/cedarling/src/tests/utils/mod.rs index b31c1d6c11a..31b5c16046f 100644 --- a/jans-cedarling/cedarling/src/tests/utils/mod.rs +++ b/jans-cedarling/cedarling/src/tests/utils/mod.rs @@ -10,6 +10,5 @@ pub use serde_json::json; pub use crate::{PolicyStoreSource, Request}; pub mod cedarling_util; -pub mod token_claims; pub use cedarling_util::{get_cedarling, get_cedarling_with_authorization_conf}; -pub use token_claims::generate_token_using_claims; +pub use test_utils::token_claims::generate_token_using_claims; diff --git a/jans-cedarling/flask-sidecar/Dockerfile b/jans-cedarling/flask-sidecar/Dockerfile index e291c95ca0e..db631723d47 100644 --- a/jans-cedarling/flask-sidecar/Dockerfile +++ b/jans-cedarling/flask-sidecar/Dockerfile @@ -31,7 +31,7 @@ RUN pip3 install "poetry==$POETRY_VERSION" gunicorn \ # =============== # Project setup # =============== -ENV JANS_SOURCE_VERSION=92cc167ade06492e25425fa14426722c9882b0ec +ENV JANS_SOURCE_VERSION=2779a7e70e23be1c0afc810abd27910c60fcd9b1 COPY docker-entrypoint.sh / RUN chmod +x /docker-entrypoint.sh diff --git a/jans-cedarling/rust-toolchain.toml b/jans-cedarling/rust-toolchain.toml new file mode 100644 index 00000000000..0193dee3606 --- /dev/null +++ b/jans-cedarling/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.83.0" diff --git a/jans-cedarling/sparkv/Cargo.toml b/jans-cedarling/sparkv/Cargo.toml index a09e6d7f132..ec0f51db975 100644 --- a/jans-cedarling/sparkv/Cargo.toml +++ b/jans-cedarling/sparkv/Cargo.toml @@ -13,3 +13,4 @@ homepage = "https://crates.io/crates/sparkv" [dependencies] thiserror = { workspace = true } +chrono = { workspace = true } diff --git a/jans-cedarling/sparkv/README.md b/jans-cedarling/sparkv/README.md index ab4aa01dc7e..b7278655800 100644 --- a/jans-cedarling/sparkv/README.md +++ b/jans-cedarling/sparkv/README.md @@ -26,7 +26,7 @@ sparkv.set("your-key", "your-value"); // write let value = sparkv.get("your-key").unwrap(); // read // Write with unique TTL -sparkv.set_with_ttl("diff-ttl", "your-value", std::time::Duration::from_secs(60)); +sparkv.set_with_ttl("diff-ttl", "your-value", chrono::Duration::new(60, 0)); ``` See `config.rs` for more configuration options. diff --git a/jans-cedarling/sparkv/src/config.rs b/jans-cedarling/sparkv/src/config.rs index 356ab16fbbb..d1f0c966c5c 100644 --- a/jans-cedarling/sparkv/src/config.rs +++ b/jans-cedarling/sparkv/src/config.rs @@ -5,12 +5,14 @@ * Copyright (c) 2024 U-Zyn Chua */ +use chrono::Duration; + #[derive(Debug, PartialEq, Clone, Copy)] pub struct Config { pub max_items: usize, pub max_item_size: usize, - pub max_ttl: std::time::Duration, - pub default_ttl: std::time::Duration, + pub max_ttl: Duration, + pub default_ttl: Duration, pub auto_clear_expired: bool, } @@ -19,8 +21,8 @@ impl Config { Config { max_items: 10_000, max_item_size: 500_000, - max_ttl: std::time::Duration::from_secs(60 * 60), - default_ttl: std::time::Duration::from_secs(5 * 60), // 5 minutes + max_ttl: Duration::new(60 * 60, 0).expect("a valid duration"), + default_ttl: Duration::new(5 * 60, 0).expect("a valid duration"), // 5 minutes auto_clear_expired: true, } } @@ -41,8 +43,14 @@ mod tests { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); assert_eq!(config.max_item_size, 500_000); - assert_eq!(config.max_ttl, std::time::Duration::from_secs(60 * 60)); - assert_eq!(config.default_ttl, std::time::Duration::from_secs(5 * 60)); + assert_eq!( + config.max_ttl, + Duration::new(60 * 60, 0).expect("a valid duration") + ); + assert_eq!( + config.default_ttl, + Duration::new(5 * 60, 0).expect("a valid duration") + ); assert!(config.auto_clear_expired); } } diff --git a/jans-cedarling/sparkv/src/expentry.rs b/jans-cedarling/sparkv/src/expentry.rs index 014c7f98226..a702962a93e 100644 --- a/jans-cedarling/sparkv/src/expentry.rs +++ b/jans-cedarling/sparkv/src/expentry.rs @@ -6,16 +6,18 @@ */ use super::kventry::KvEntry; +use chrono::Duration; +use chrono::prelude::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExpEntry { pub key: String, - pub expired_at: std::time::Instant, + pub expired_at: DateTime, } impl ExpEntry { - pub fn new(key: &str, expiration: std::time::Duration) -> Self { - let expired_at: std::time::Instant = std::time::Instant::now() + expiration; + pub fn new(key: &str, expiration: Duration) -> Self { + let expired_at: DateTime = Utc::now() + expiration; Self { key: String::from(key), expired_at, @@ -30,7 +32,7 @@ impl ExpEntry { } pub fn is_expired(&self) -> bool { - self.expired_at < std::time::Instant::now() + self.expired_at < Utc::now() } } @@ -57,10 +59,10 @@ mod tests { #[test] fn test_new() { - let item = ExpEntry::new("key", std::time::Duration::from_secs(10)); + let item = ExpEntry::new("key", Duration::new(10, 0).expect("a valid duration")); assert_eq!(item.key, "key"); - assert!(item.expired_at > std::time::Instant::now() + std::time::Duration::from_secs(9)); - assert!(item.expired_at <= std::time::Instant::now() + std::time::Duration::from_secs(10)); + assert!(item.expired_at > Utc::now() + Duration::new(9, 0).expect("a valid duration")); + assert!(item.expired_at <= Utc::now() + Duration::new(10, 0).expect("a valid duration")); } #[test] @@ -68,7 +70,7 @@ mod tests { let kv_entry = KvEntry::new( "keyFromKV", "value from KV", - std::time::Duration::from_secs(10), + Duration::new(10, 0).expect("a valid duration"), ); let exp_item = ExpEntry::from_kv_entry(&kv_entry); assert_eq!(exp_item.key, "keyFromKV"); @@ -77,17 +79,16 @@ mod tests { #[test] fn test_cmp() { - let item_small = ExpEntry::new("k1", std::time::Duration::from_secs(10)); - let item_big = ExpEntry::new("k2", std::time::Duration::from_secs(8000)); + let item_small = ExpEntry::new("k1", Duration::new(10, 0).expect("a valid duration")); + let item_big = ExpEntry::new("k2", Duration::new(8000, 0).expect("a valid duration")); assert!(item_small > item_big); // reverse order assert!(item_big < item_small); // reverse order } #[test] fn test_is_expired() { - let item = ExpEntry::new("k1", std::time::Duration::from_millis(1)); - assert!(!item.is_expired()); - std::thread::sleep(std::time::Duration::from_millis(2)); + let item = ExpEntry::new("k1", Duration::new(0, 100).expect("a valid duration")); + std::thread::sleep(std::time::Duration::from_nanos(200)); assert!(item.is_expired()); } } diff --git a/jans-cedarling/sparkv/src/kventry.rs b/jans-cedarling/sparkv/src/kventry.rs index ea817bd812e..8fd8efbe6aa 100644 --- a/jans-cedarling/sparkv/src/kventry.rs +++ b/jans-cedarling/sparkv/src/kventry.rs @@ -4,17 +4,19 @@ * * Copyright (c) 2024 U-Zyn Chua */ +use chrono::Duration; +use chrono::prelude::*; #[derive(Debug, Clone, PartialEq, Eq)] pub struct KvEntry { pub key: String, pub value: String, - pub expired_at: std::time::Instant, + pub expired_at: DateTime, } impl KvEntry { - pub fn new(key: &str, value: &str, expiration: std::time::Duration) -> Self { - let expired_at: std::time::Instant = std::time::Instant::now() + expiration; + pub fn new(key: &str, value: &str, expiration: Duration) -> Self { + let expired_at: DateTime = Utc::now() + expiration; Self { key: String::from(key), value: String::from(value), @@ -29,10 +31,14 @@ mod tests { #[test] fn test_new() { - let item = KvEntry::new("key", "value", std::time::Duration::from_secs(10)); + let item = KvEntry::new( + "key", + "value", + Duration::new(10, 0).expect("a valid duration"), + ); assert_eq!(item.key, "key"); assert_eq!(item.value, "value"); - assert!(item.expired_at > std::time::Instant::now() + std::time::Duration::from_secs(9)); - assert!(item.expired_at <= std::time::Instant::now() + std::time::Duration::from_secs(10)); + assert!(item.expired_at > Utc::now() + Duration::new(9, 0).expect("a valid duration")); + assert!(item.expired_at <= Utc::now() + Duration::new(10, 0).expect("a valid duration")); } } diff --git a/jans-cedarling/sparkv/src/lib.rs b/jans-cedarling/sparkv/src/lib.rs index 8c76171f013..b5bdd8ca9e9 100644 --- a/jans-cedarling/sparkv/src/lib.rs +++ b/jans-cedarling/sparkv/src/lib.rs @@ -15,6 +15,9 @@ pub use error::Error; pub use expentry::ExpEntry; pub use kventry::KvEntry; +use chrono::Duration; +use chrono::prelude::*; + pub struct SparKV { pub config: Config, data: std::collections::BTreeMap, @@ -39,12 +42,7 @@ impl SparKV { self.set_with_ttl(key, value, self.config.default_ttl) } - pub fn set_with_ttl( - &mut self, - key: &str, - value: &str, - ttl: std::time::Duration, - ) -> Result<(), Error> { + pub fn set_with_ttl(&mut self, key: &str, value: &str, ttl: Duration) -> Result<(), Error> { self.clear_expired_if_auto(); self.ensure_capacity_ignore_key(key)?; self.ensure_item_size(value)?; @@ -66,7 +64,7 @@ impl SparKV { // Only returns if it is not yet expired pub fn get_item(&self, key: &str) -> Option<&KvEntry> { let item = self.data.get(key)?; - if item.expired_at > std::time::Instant::now() { + if item.expired_at > Utc::now() { Some(item) } else { None @@ -151,7 +149,7 @@ impl SparKV { Ok(()) } - fn ensure_max_ttl(&self, ttl: std::time::Duration) -> Result<(), Error> { + fn ensure_max_ttl(&self, ttl: Duration) -> Result<(), Error> { if ttl > self.config.max_ttl { return Err(Error::TTLTooLong); } @@ -174,7 +172,10 @@ mod tests { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); assert_eq!(config.max_item_size, 500_000); - assert_eq!(config.max_ttl, std::time::Duration::from_secs(60 * 60)); + assert_eq!( + config.max_ttl, + Duration::new(60 * 60, 0).expect("a valid duration") + ); } #[test] @@ -213,7 +214,11 @@ mod tests { #[test] fn test_get_item() { let mut sparkv = SparKV::new(); - let item = KvEntry::new("keyARaw", "value99", std::time::Duration::from_secs(1)); + let item = KvEntry::new( + "keyARaw", + "value99", + Duration::new(1, 0).expect("a valid duration"), + ); sparkv.data.insert(item.key.clone(), item); let get_result = sparkv.get_item("keyARaw"); let unwrapped = get_result.unwrap(); @@ -228,11 +233,15 @@ mod tests { #[test] fn test_get_item_return_none_if_expired() { let mut sparkv = SparKV::new(); - _ = sparkv.set_with_ttl("kkk", "value", std::time::Duration::from_millis(50)); - assert_eq!(sparkv.get("kkk"), Some(String::from("value"))); + _ = sparkv.set_with_ttl( + "key", + "value", + Duration::new(0, 40000).expect("a valid duration"), + ); + assert_eq!(sparkv.get("key"), Some(String::from("value"))); - std::thread::sleep(std::time::Duration::from_millis(60)); - assert_eq!(sparkv.get("kkk"), None); + std::thread::sleep(std::time::Duration::from_nanos(50000)); + assert_eq!(sparkv.get("key"), None); } #[test] @@ -263,8 +272,16 @@ mod tests { fn test_set_with_ttl() { let mut sparkv = SparKV::new(); _ = sparkv.set("longest", "value"); - _ = sparkv.set_with_ttl("longer", "value", std::time::Duration::from_secs(2)); - _ = sparkv.set_with_ttl("shorter", "value", std::time::Duration::from_secs(1)); + _ = sparkv.set_with_ttl( + "longer", + "value", + Duration::new(2, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "shorter", + "value", + Duration::new(1, 0).expect("a valid duration"), + ); assert_eq!(sparkv.get("longer"), Some(String::from("value"))); assert_eq!(sparkv.get("shorter"), Some(String::from("value"))); @@ -281,24 +298,33 @@ mod tests { #[test] fn test_ensure_max_ttl() { let mut config: Config = Config::new(); - config.max_ttl = std::time::Duration::from_secs(3600); - config.default_ttl = std::time::Duration::from_secs(5000); + config.max_ttl = Duration::new(3600, 0).expect("a valid duration"); + config.default_ttl = Duration::new(5000, 0).expect("a valid duration"); let mut sparkv = SparKV::with_config(config); let set_result_long_def = sparkv.set("default is longer than max", "should fail"); assert!(set_result_long_def.is_err()); assert_eq!(set_result_long_def.unwrap_err(), Error::TTLTooLong); - let set_result_ok = - sparkv.set_with_ttl("shorter", "ok", std::time::Duration::from_secs(3599)); + let set_result_ok = sparkv.set_with_ttl( + "shorter", + "ok", + Duration::new(3599, 0).expect("a valid duration"), + ); assert!(set_result_ok.is_ok()); - let set_result_ok_2 = - sparkv.set_with_ttl("exact", "ok", std::time::Duration::from_secs(3600)); + let set_result_ok_2 = sparkv.set_with_ttl( + "exact", + "ok", + Duration::new(3600, 0).expect("a valid duration"), + ); assert!(set_result_ok_2.is_ok()); - let set_result_not_ok = - sparkv.set_with_ttl("not", "not ok", std::time::Duration::from_secs(3601)); + let set_result_not_ok = sparkv.set_with_ttl( + "not", + "not ok", + Duration::new(3601, 0).expect("a valid duration"), + ); assert!(set_result_not_ok.is_err()); assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); } @@ -321,17 +347,22 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = false; let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("not-yet-expired", "v", std::time::Duration::from_secs(90)); - _ = sparkv.set_with_ttl("expiring", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); - std::thread::sleep(std::time::Duration::from_millis(2)); - assert_eq!(sparkv.len(), 3); - - let cleared_count = sparkv.clear_expired(); - assert_eq!(cleared_count, 1); - assert_eq!(sparkv.len(), 2); - - assert_eq!(sparkv.clear_expired(), 0); + _ = sparkv.set_with_ttl( + "not-yet-expired", + "v", + Duration::new(0, 90).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "expiring", + "value", + Duration::new(1, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "not-expired", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); + std::thread::sleep(std::time::Duration::from_nanos(2)) } #[test] @@ -339,10 +370,22 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = false; let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("no-longer", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("no-longer", "v", std::time::Duration::from_secs(90)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); - std::thread::sleep(std::time::Duration::from_millis(2)); + _ = sparkv.set_with_ttl( + "no-longer", + "value", + Duration::new(0, 1).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "no-longer", + "v", + Duration::new(90, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "not-expired", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); + std::thread::sleep(std::time::Duration::from_nanos(2)); assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries assert_eq!(sparkv.len(), 2); @@ -357,15 +400,31 @@ mod tests { let mut config: Config = Config::new(); config.auto_clear_expired = true; // explicitly setting it to true let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl("no-longer", "value", std::time::Duration::from_millis(1)); - _ = sparkv.set_with_ttl("no-longer", "v", std::time::Duration::from_secs(90)); - std::thread::sleep(std::time::Duration::from_millis(2)); - _ = sparkv.set_with_ttl("not-expired", "value", std::time::Duration::from_secs(60)); + _ = sparkv.set_with_ttl( + "no-longer", + "value", + Duration::new(1, 0).expect("a valid duration"), + ); + _ = sparkv.set_with_ttl( + "no-longer", + "v", + Duration::new(90, 0).expect("a valid duration"), + ); + std::thread::sleep(std::time::Duration::from_secs(2)); + _ = sparkv.set_with_ttl( + "not-expired", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear assert_eq!(sparkv.len(), 2); - // auto clear - _ = sparkv.set_with_ttl("new-", "value", std::time::Duration::from_secs(60)); + // auto clear 2 + _ = sparkv.set_with_ttl( + "new-", + "value", + Duration::new(60, 0).expect("a valid duration"), + ); assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries assert_eq!(sparkv.len(), 3); // but not actually deleting } diff --git a/jans-cedarling/test_utils/Cargo.toml b/jans-cedarling/test_utils/Cargo.toml index b47c9faae77..b03100d9fed 100644 --- a/jans-cedarling/test_utils/Cargo.toml +++ b/jans-cedarling/test_utils/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] pretty_assertions = "1" serde_json = { workspace = true } +jsonwebtoken = { workspace = true } +jsonwebkey = { workspace = true, features = ["generate", "jwt-convert"] } +serde = { workspace = true } diff --git a/jans-cedarling/test_utils/src/lib.rs b/jans-cedarling/test_utils/src/lib.rs index 175ba73d4e6..5b1b61320f8 100644 --- a/jans-cedarling/test_utils/src/lib.rs +++ b/jans-cedarling/test_utils/src/lib.rs @@ -6,6 +6,7 @@ */ mod sort_json; +pub mod token_claims; pub use pretty_assertions::*; pub use sort_json::SortedJson; diff --git a/jans-cedarling/cedarling/src/tests/utils/token_claims.rs b/jans-cedarling/test_utils/src/token_claims.rs similarity index 85% rename from jans-cedarling/cedarling/src/tests/utils/token_claims.rs rename to jans-cedarling/test_utils/src/token_claims.rs index 7529926f241..d89c8de7cf0 100644 --- a/jans-cedarling/cedarling/src/tests/utils/token_claims.rs +++ b/jans-cedarling/test_utils/src/token_claims.rs @@ -3,15 +3,16 @@ // // Copyright (c) 2024, Gluu, Inc. -use lazy_static::lazy_static; +//! Package for generating JWT tokens for testing purpose. + +use std::sync::LazyLock; + use {jsonwebkey as jwk, jsonwebtoken as jwt}; // Represent meta information about entity from cedar-policy schema. -lazy_static! { - pub(crate) static ref EncodingKeys: GeneratedKeys = generate_keys(); -} +static ENCODING_KEYS: LazyLock = LazyLock::new(generate_keys); -pub(crate) struct GeneratedKeys { +pub struct GeneratedKeys { pub private_key_id: String, pub private_encoding_key: jwt::EncodingKey, } @@ -19,7 +20,7 @@ pub(crate) struct GeneratedKeys { /// Generates a set of private and public keys using ES256 /// /// Returns a tuple: (Vec<(key_id, private_key)>, jwks) -pub fn generate_keys() -> GeneratedKeys { +fn generate_keys() -> GeneratedKeys { let kid = 1; // Generate a private key let mut jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256()); @@ -50,8 +51,8 @@ pub fn generate_keys() -> GeneratedKeys { /// Generates a token string signed with ES256 pub fn generate_token_using_claims(claims: impl serde::Serialize) -> String { - let key_id = EncodingKeys.private_key_id.clone(); - let encoding_key = &EncodingKeys.private_encoding_key; + let key_id = ENCODING_KEYS.private_key_id.clone(); + let encoding_key = &ENCODING_KEYS.private_encoding_key; // select a key from the keyset // for simplicity, were just choosing the second one diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index 69e873ce172..cd7007ba939 100644 --- a/jans-config-api/docs/jans-config-api-swagger.yaml +++ b/jans-config-api/docs/jans-config-api-swagger.yaml @@ -9337,19 +9337,19 @@ components: type: string selected: type: boolean - whitePagesCanView: + adminCanEdit: + type: boolean + adminCanView: type: boolean userCanView: type: boolean userCanEdit: type: boolean - userCanAccess: - type: boolean adminCanAccess: type: boolean - adminCanEdit: + userCanAccess: type: boolean - adminCanView: + whitePagesCanView: type: boolean baseDn: type: string @@ -10211,6 +10211,8 @@ components: type: boolean lockMessageConfig: $ref: '#/components/schemas/LockMessageConfig' + fapi: + type: boolean allResponseTypesSupported: uniqueItems: true type: array @@ -10220,8 +10222,6 @@ components: - code - token - id_token - fapi: - type: boolean AuthenticationFilter: required: - baseDn diff --git a/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml b/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml index 0859f6b93a1..5384db0df26 100644 --- a/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/fido2-plugin-swagger.yaml @@ -560,12 +560,8 @@ components: type: array items: type: string - superGluuEnabled: - type: boolean sessionIdPersistInCache: type: boolean - oldU2fMigrationEnabled: - type: boolean errorReasonEnabled: type: boolean fido2Configuration: @@ -583,38 +579,53 @@ components: type: string checkU2fAttestations: type: boolean - userAutoEnrollment: + debugUserAutoEnrollment: type: boolean unfinishedRequestExpiration: type: integer format: int32 - authenticationHistoryExpiration: + metadataRefreshInterval: type: integer format: int32 serverMetadataFolder: type: string - requestedCredentialTypes: + enabledFidoAlgorithms: type: array items: type: string - requestedParties: + metadataServers: type: array items: - $ref: '#/components/schemas/RequestedParty' - metadataUrlsProvider: - type: string - skipDownloadMdsEnabled: - type: boolean - skipValidateMdsInAttestationEnabled: + $ref: '#/components/schemas/MetadataServer' + disableMetadataService: type: boolean - assertionOptionsGenerateEndpointEnabled: + hints: + type: array + items: + type: string + enterpriseAttestation: type: boolean + attestationMode: + type: string + rp: + type: array + items: + $ref: '#/components/schemas/RequestedParty' + MetadataServer: + type: object + properties: + url: + type: string + certificateDocumentInum: + type: array + items: + type: string RequestedParty: type: object properties: - name: + id: type: string - domains: + origins: type: array items: type: string diff --git a/jans-config-api/plugins/user-mgt-plugin/src/test/resources/feature/mgt/user/user-patch.json b/jans-config-api/plugins/user-mgt-plugin/src/test/resources/feature/mgt/user/user-patch.json index eef49548448..bb5db8eb5db 100644 --- a/jans-config-api/plugins/user-mgt-plugin/src/test/resources/feature/mgt/user/user-patch.json +++ b/jans-config-api/plugins/user-mgt-plugin/src/test/resources/feature/mgt/user/user-patch.json @@ -8,33 +8,6 @@ ], "value": true, "displayValue": true - }, - { - "name": "secretAnswer", - "multiValued": false, - "values": [ - "james-bond@123" - ], - "value": "james-bond@123", - "displayValue": "james-bond@123" - }, - { - "name": "jansImsValue", - "multiValued": true, - "values": [{ - "value": "123456", - "display": "Home phone", - "type": "home", - "primary": true - }, - { - "value": "9821789", - "display": "Work phone", - "type": "work", - "primary": false - } - - ] } ] } \ No newline at end of file diff --git a/jans-config-api/server/src/test/resources/feature/token/client-token.feature b/jans-config-api/server/src/test/resources/feature/token/client-token.feature index edb945da958..01539195a80 100644 --- a/jans-config-api/server/src/test/resources/feature/token/client-token.feature +++ b/jans-config-api/server/src/test/resources/feature/token/client-token.feature @@ -8,7 +8,7 @@ Background: Scenario: Fetch all client token Given url mainUrl When method GET -Then status 401 +Then status 404 And print response @ignore diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java index 28d0dfebd76..28caa102666 100644 --- a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/AuthorizationChallengeType.java @@ -12,4 +12,7 @@ public interface AuthorizationChallengeType extends BaseExternalType { boolean authorize(Object context); Map getAuthenticationMethodClaims(Object context); + + // prepare authzRequest - AuthzRequest class + void prepareAuthzRequest(Object context); } diff --git a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java index 04476ccb8ee..01f6a0e4866 100644 --- a/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java +++ b/jans-core/script/src/main/java/io/jans/model/custom/script/type/authzchallenge/DummyAuthorizationChallengeType.java @@ -21,6 +21,10 @@ public Map getAuthenticationMethodClaims(Object context) { return new HashMap<>(); } + @Override + public void prepareAuthzRequest(Object context) { + } + @Override public boolean init(Map configurationAttributes) { return false; diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/verifier/CommonVerifiersTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/verifier/CommonVerifiersTest.java index 9b67ab55728..e7acce023ff 100644 --- a/jans-fido2/server/src/test/java/io/jans/fido2/service/verifier/CommonVerifiersTest.java +++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/verifier/CommonVerifiersTest.java @@ -21,6 +21,7 @@ import io.jans.fido2.service.processors.AttestationFormatProcessor; import io.jans.service.net.NetworkService; import jakarta.enterprise.inject.Instance; +import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; import org.bouncycastle.util.encoders.Hex; @@ -33,9 +34,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -116,6 +115,46 @@ void verifyRpDomain_originIsNull_valid() { assertEquals(response, "test.domain"); } + + @Test + void verifyRpDomain_originMatchesValidOrigin_valid() { + String origin = "https://test.bank.com"; + String rpId = "bank.com"; + List requestedParties = new ArrayList<>(); + RequestedParty rp = new RequestedParty(); + rp.setOrigins(Arrays.asList("test.bank.com", "emp.bank.com", "india.bank.com")); + requestedParties.add(rp); + + when(networkService.getHost(origin)).thenReturn("test.bank.com"); + + String response = commonVerifiers.verifyRpDomain(origin, rpId, requestedParties); + + assertNotNull(response); + assertEquals("test.bank.com", response); + } + + @Test + void verifyRpDomain_originDoesNotMatchValidOrigins_invalid() { + String origin = "https://test.bank1.com"; + String rpId = "bank.com"; + List requestedParties = new ArrayList<>(); + RequestedParty rp = new RequestedParty(); + rp.setOrigins(Arrays.asList("test.bank.com", "emp.bank.com", "india.bank.com")); + requestedParties.add(rp); + + when(networkService.getHost(origin)).thenReturn("test.bank1.com"); + + when(errorResponseFactory.badRequestException(any(), anyString())) + .thenThrow(new BadRequestException("The origin " + origin + " is not listed in the allowed origins.")); + + BadRequestException exception = assertThrows(BadRequestException.class, () -> { + commonVerifiers.verifyRpDomain(origin, rpId, requestedParties); + }); + + assertEquals("The origin " + origin + " is not listed in the allowed origins.", exception.getMessage()); + } + + @Test void verifyCounter_oldAndNewCounterZero_valid() { int oldCounter = 0; diff --git a/jans-keycloak-link/server/pom.xml b/jans-keycloak-link/server/pom.xml index 75a754de219..034ca6d63f9 100644 --- a/jans-keycloak-link/server/pom.xml +++ b/jans-keycloak-link/server/pom.xml @@ -33,16 +33,6 @@ io.jans jans-core-service - - - io.jans - jans-core-document-store - - - io.jans - jans-core-message - - diff --git a/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/server/service/ConfigurationService.java b/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/server/service/ConfigurationService.java new file mode 100644 index 00000000000..4f8a766e64a --- /dev/null +++ b/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/server/service/ConfigurationService.java @@ -0,0 +1,97 @@ +/* + * Copyright [2024] [Janssen Project] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jans.keycloak.link.server.service; + +import io.jans.config.GluuConfiguration; +import io.jans.keycloak.link.model.config.StaticConfiguration; +import io.jans.model.SmtpConfiguration; +import io.jans.orm.PersistenceEntryManager; +import io.jans.service.EncryptionService; +import io.jans.util.StringHelper; +import io.jans.util.security.StringEncrypter.EncryptionException; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.slf4j.Logger; + +/** + * + * @author Yuriy Movchan Date: 12/12/2023 + */ +@ApplicationScoped +public class ConfigurationService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private EncryptionService encryptionService; + + public GluuConfiguration getConfiguration() { + String configurationDn = staticConfiguration.getBaseDn().getConfiguration(); + if (StringHelper.isEmpty(configurationDn)) { + return null; + } + + return persistenceEntryManager.find(GluuConfiguration.class, configurationDn); + } + + /** + * Build DN string for configuration + * + * @param inum Inum + * @return DN string for specified configuration or DN for configurations branch if inum is null + * @throws Exception + */ + public String getDnForConfiguration(String inum) { + String baseDn = staticConfiguration.getBaseDn().getConfiguration(); + if (StringHelper.isEmpty(inum)) { + return baseDn; + } + + return String.format("inum=%s,%s", inum, baseDn); + } + + public void decryptSmtpPasswords(SmtpConfiguration smtpConfiguration) { + if (smtpConfiguration == null) { + return; + } + String password = smtpConfiguration.getSmtpAuthenticationAccountPassword(); + if (StringHelper.isNotEmpty(password)) { + try { + smtpConfiguration.setSmtpAuthenticationAccountPasswordDecrypted(encryptionService.decrypt(password)); + } catch (EncryptionException ex) { + log.error("Failed to decrypt SMTP user password", ex); + } + } + password = smtpConfiguration.getKeyStorePassword(); + if (StringHelper.isNotEmpty(password)) { + try { + smtpConfiguration.setKeyStorePasswordDecrypted(encryptionService.decrypt(password)); + } catch (EncryptionException ex) { + log.error("Failed to decrypt Kestore password", ex); + } + } + } + +} + diff --git a/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/service/config/ApplicationFactory.java b/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/service/config/ApplicationFactory.java index 63e8978275d..4804abbd9ae 100644 --- a/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/service/config/ApplicationFactory.java +++ b/jans-keycloak-link/server/src/main/java/io/jans/keycloak/link/service/config/ApplicationFactory.java @@ -6,6 +6,14 @@ package io.jans.keycloak.link.service.config; +import io.jans.config.GluuConfiguration; +import io.jans.keycloak.link.server.service.ConfigurationService; +import io.jans.service.document.store.conf.DocumentStoreConfiguration; +import io.jans.service.document.store.conf.LocalDocumentStoreConfiguration; +import io.jans.service.message.model.config.MessageConfiguration; +import io.jans.service.message.model.config.MessageProviderType; +import io.jans.service.message.model.config.NullMessageConfiguration; +import jakarta.enterprise.inject.Produces; import org.slf4j.Logger; import io.jans.keycloak.link.model.config.AppConfiguration; @@ -35,6 +43,9 @@ public class ApplicationFactory { @Inject private AppConfiguration appConfiguration; + @Inject + private ConfigurationService сonfigurationService; + public static final String PERSISTENCE_ENTRY_MANAGER_FACTORY_NAME = "persistenceEntryManagerFactory"; public static final String PERSISTENCE_ENTRY_MANAGER_NAME = "persistenceEntryManager"; @@ -57,4 +68,44 @@ public PersistenceEntryManagerFactory getPersistenceEntryManagerFactory(Class None: filepath = os.environ.get("CN_CONFIGURATOR_CONFIGURATION_FILE", "/etc/jans/conf/configuration.json") + key_file = os.environ.get("CN_CONFIGURATOR_KEY_FILE", "/etc/jans/conf/configuration.key") - out, err, code = load_schema_from_file(filepath, exclude_secret=True) + out, err, code = load_schema_from_file(filepath, exclude_secret=True, key_file=key_file) if code != 0: logger.warning(f"Unable to load configmaps from file {filepath}; error={err}; local configmaps will be excluded") diff --git a/jans-pycloudlib/jans/pycloudlib/schema/__init__.py b/jans-pycloudlib/jans/pycloudlib/schema/__init__.py index 953f12f4f3e..190ccc72aa9 100644 --- a/jans-pycloudlib/jans/pycloudlib/schema/__init__.py +++ b/jans-pycloudlib/jans/pycloudlib/schema/__init__.py @@ -4,6 +4,7 @@ import logging import re from base64 import b64decode +from contextlib import suppress import pem from fqdn import FQDN @@ -20,6 +21,7 @@ from marshmallow.validate import OneOf from marshmallow.validate import Predicate from marshmallow.validate import Range +from sprig_aes import sprig_decrypt_aes logger = logging.getLogger(__name__) @@ -884,38 +886,72 @@ class Meta: _configmap = Nested(ConfigmapSchema, required=True) -def load_schema_from_file(path, exclude_configmap=False, exclude_secret=False): +def load_schema_from_file(path, exclude_configmap=False, exclude_secret=False, key_file=""): """Loads schema from file.""" - out = {} - err = {} - code = 0 + out, err, code = maybe_encrypted_schema(path, key_file) - try: - with open(path) as f: - docs = json.loads(f.read()) - except (IOError, ValueError) as exc: - err = exc - code = 1 + if code != 0: return out, err, code # dont exclude attributes - exclude_attrs = False + exclude_attrs = [] # exclude configmap from loading mechanism if exclude_configmap: key = "_configmap" exclude_attrs = [key] - docs.pop(key, None) + out.pop(key, None) # exclude secret from loading mechanism if exclude_secret: key = "_secret" exclude_attrs = [key] - docs.pop(key, None) + out.pop(key, None) try: - out = ConfigurationSchema().load(docs, partial=exclude_attrs) + out = ConfigurationSchema().load(out, partial=exclude_attrs) except ValidationError as exc: err = exc.messages code = 1 return out, err, code + + +def load_schema_key(path): + try: + with open(path) as f: + key = f.read().strip() + except FileNotFoundError: + key = "" + return key + + +def maybe_encrypted_schema(path, key_file): + out, err, code = {}, {}, 0 + + try: + # read schema as raw string + with open(path) as f: + raw_txt = f.read() + except FileNotFoundError as exc: + err = { + "error": f"Unable to load schema {path}", + "reason": exc, + } + code = exc.errno + else: + if key := load_schema_key(key_file): + # try to decrypt schema (if applicable) + with suppress(ValueError): + raw_txt = sprig_decrypt_aes(raw_txt, key) + + try: + out = json.loads(raw_txt) + except (json.decoder.JSONDecodeError, UnicodeDecodeError) as exc: + err = { + "error": f"Unable to decode JSON from {path}", + "reason": exc, + } + code = 1 + + # finalized results + return out, err, code diff --git a/jans-pycloudlib/jans/pycloudlib/secret/file_secret.py b/jans-pycloudlib/jans/pycloudlib/secret/file_secret.py index 2d284386699..8386d43c7cb 100644 --- a/jans-pycloudlib/jans/pycloudlib/secret/file_secret.py +++ b/jans-pycloudlib/jans/pycloudlib/secret/file_secret.py @@ -12,8 +12,9 @@ class FileSecret(BaseSecret): def __init__(self) -> None: filepath = os.environ.get("CN_CONFIGURATOR_CONFIGURATION_FILE", "/etc/jans/conf/configuration.json") + key_file = os.environ.get("CN_CONFIGURATOR_KEY_FILE", "/etc/jans/conf/configuration.key") - out, err, code = load_schema_from_file(filepath, exclude_configmap=True) + out, err, code = load_schema_from_file(filepath, exclude_configmap=True, key_file=key_file) if code != 0: logger.warning(f"Unable to load secrets from file {filepath}; error={err}; local secrets will be excluded") diff --git a/jans-pycloudlib/tests/test_schema.py b/jans-pycloudlib/tests/test_schema.py index 99e5769e844..a8c51a78872 100644 --- a/jans-pycloudlib/tests/test_schema.py +++ b/jans-pycloudlib/tests/test_schema.py @@ -159,3 +159,51 @@ def test_random_optional_scopes(value): with pytest.raises(ValidationError): ConfigmapSchema().validate_optional_scopes(value) + + +def test_load_schema_key(tmpdir): + from jans.pycloudlib.schema import load_schema_key + + src = tmpdir.join("configuration.key") + src.write("abcd") + assert load_schema_key(str(src)) == "abcd" + + +def test_maybe_encrypted_schema_file_missing(): + from jans.pycloudlib.schema import maybe_encrypted_schema + + _, err, _ = maybe_encrypted_schema("/path/to/schema/file", "/path/to/schema/key") + assert "error" in err + + +def test_maybe_encrypted_schema(tmpdir): + from jans.pycloudlib.schema import maybe_encrypted_schema + + src = tmpdir.join("configuration.json") + src.write("zLBGM41dAfA2JuIkVHRKa+/WwVo/8oQAdD0LUT3jGfhqp/euYdDhf+kTiKwfb1Sv28zYL12JlO+3oSl6ZlhiTw==") + + src_key = tmpdir.join("configuration.key") + src_key.write("6Jsv61H7fbkeIkRvUpnZ98fu") + + out, _, _ = maybe_encrypted_schema(str(src), str(src_key)) + assert out == {"_configmap": {"hostname": "example.com"}} + + +def test_schema_exclude_configmap(tmpdir): + from jans.pycloudlib.schema import load_schema_from_file + + src = tmpdir.join("configuration.json") + src.write('{"_configmap": {}, "_secret": {"admin_password": "Test1234#"}}') + + out, _, code = load_schema_from_file(str(src), exclude_configmap=True) + assert "_configmap" not in out and code == 0 + + +def test_schema_exclude_secret(tmpdir): + from jans.pycloudlib.schema import load_schema_from_file + + src = tmpdir.join("configuration.json") + src.write('{"_configmap": {"city": "Austin", "country_code": "US", "admin_email": "s@example.com", "hostname": "example.com", "orgName": "Example Inc.", "state": "TX"}, "_secret": {}}') + + out, _, code = load_schema_from_file(str(src), exclude_secret=True) + assert "_secret" not in out and code == 0 diff --git a/mkdocs.yml b/mkdocs.yml index 05159ef1d84..ada56fb3a94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -281,6 +281,7 @@ nav: - Agama Best Practices: janssen-server/developer/agama/agama-best-practices.md - Advanced usages: janssen-server/developer/agama/advanced-usages.md - Engine and bridge configurations: janssen-server/developer/agama/engine-bridge-config.md + - Agama flows in native applications: janssen-server/developer/agama/native-applications.md - FAQ: janssen-server/developer/agama/faq.md - Quick Start Using Agama Lab: janssen-server/developer/agama/quick-start-using-agama-lab.md - External Libraries: janssen-server/developer/external-libraries.md