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 b1b771aa930..28e9ce016b9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -209,7 +209,10 @@ jobs: # 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"; done + 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" 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..f5bc3c5af3e 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 @@ -305,6 +307,8 @@ jobs: overwrite: true build_demo_packages: if: github.repository == 'JanssenProject/jans' + # Needs cedarling wasm as jans-tarp uses the wasm package + needs: build_cedarling_wasm runs-on: ubuntu-latest steps: - name: Harden Runner @@ -324,6 +328,17 @@ jobs: for i in $(ls -d */); do zip -r demo-${i%/}-$VER-source.zip $i && sha256sum demo-${i%/}-$VER-source.zip > demo-${i%/}-$VER-source.zip.sha256sum; done sudo rm demo-jans-tarp-$VER-source.zip demo-jans-tarp-$VER-source.zip.sha256sum cd jans-tarp + # Get the latest cedarling wasm package + TAG=$(echo ${{ github.event.ref }} | cut -d '/' -f 3 | sed 's/^v//') + if [ "${TAG}" == "nightly" ]; then + TAG="0.0.0" + fi + wget https://github.com/${{ github.repository }}/releases/download/"${VER}"/cedarling_wasm_"${TAG}"_pkg.tar.gz -O cedarling_wasm.tar.gz + mkdir -p wasm + tar -xvf cedarling_wasm.tar.gz -C wasm + rm cedarling_wasm.tar.gz + ls wasm + # END Get the latest cedarling wasm package npm install npm run build npm run pack @@ -337,7 +352,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 +371,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 +388,53 @@ 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 + build_cedarling_wasm: + if: github.repository == 'JanssenProject/jans' + runs-on: ubuntu-20.04 + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Import GPG key + id: import_gpg + continue-on-error: true + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + with: + gpg_private_key: ${{ secrets.MOAUTO_GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.MOAUTO_GPG_PRIVATE_KEY_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + - name: Build WASM build + id: sign-cedarling + working-directory: ${{ github.workspace }}/jans-cedarling/bindings/cedarling_wasm + run: | + rustup update stable && rustup default stable + cargo install wasm-pack + wasm-pack build --release --target web + ls pkg + - name: Archive and sign pkg contents + id: archive_pkg + working-directory: ${{ github.workspace }}/jans-cedarling/bindings/cedarling_wasm + run: | + TAG=$(echo ${{ github.event.ref }} | cut -d '/' -f 3 | sed 's/^v//') + VERSION="$(echo ${{ github.event.ref }} | cut -d '/' -f 3)" + if [ "${TAG}" == "nightly" ]; then + VERSION=nightly + TAG="0.0.0" + fi + rm -rf pkg/.gitignore || echo "Failed to remove gitignore" + tar -czvf cedarling_wasm_"${TAG}"_pkg.tar.gz -C pkg . + sha256sum cedarling_wasm_"${TAG}"_pkg.tar.gz > cedarling_wasm_"${TAG}"_pkg.tar.gz.sha256sum + gpg --armor --detach-sign cedarling_wasm_"${TAG}"_pkg.tar.gz || 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 + gh release upload "${VERSION}" *.tar.gz *.sha256sum *.asc diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0142d4bc5b4..bde47dc54af 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 @@ -119,6 +134,7 @@ jobs: strategy: max-parallel: 1 matrix: ${{ fromJson(needs.prep-matrix.outputs.matrix) }} + fail-fast: false steps: - name: Harden Runner @@ -126,18 +142,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 +162,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 +176,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 +187,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 +217,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 +295,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/CHANGELOG.md b/CHANGELOG.md index d101aef742a..6b0959bcc82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +## [1.3.0](https://github.com/JanssenProject/jans/compare/v1.2.0...v1.3.0) (2025-01-20) + + +### Features + +* **agama:** modify RRF and RFAC behavior for non-web clients ([#10547](https://github.com/JanssenProject/jans/issues/10547)) ([58fd359](https://github.com/JanssenProject/jans/commit/58fd3598777a31b5a4f7b7be3fac31a9f2131268)) +* allow integration of Agama flows into the authz challenge enpoint ([#10587](https://github.com/JanssenProject/jans/issues/10587)) ([856f9fe](https://github.com/JanssenProject/jans/commit/856f9fed1d58a6d41503a0459bbe04f52b0bb8e7)) +* cedarling integration with tarp ([#10681](https://github.com/JanssenProject/jans/issues/10681)) ([4f44337](https://github.com/JanssenProject/jans/commit/4f443370153a074bf87834f44ba068266b2b9792)) +* **cloud-native:** secure mounted configuration schema ([#10577](https://github.com/JanssenProject/jans/issues/10577)) ([57c266a](https://github.com/JanssenProject/jans/commit/57c266af928186f45c8d346ce0f7c8c0baba01b1)) +* extract wasm tar in /jans-tarp folder ([#10689](https://github.com/JanssenProject/jans/issues/10689)) ([ca8f453](https://github.com/JanssenProject/jans/commit/ca8f45311747b645ac70458ff2dba4664abf47b7)) +* integrate cedarling with jans-tarp ([#10662](https://github.com/JanssenProject/jans/issues/10662)) ([9f9ae8a](https://github.com/JanssenProject/jans/commit/9f9ae8a9d09f9da9cd6b02e07a591132604ee857)) +* **jans-auth-server:** introduced new 'prepareAuthzRequest' method in authorization challenge script ([#10598](https://github.com/JanssenProject/jans/issues/10598)) ([02c240e](https://github.com/JanssenProject/jans/commit/02c240effba327358a3d2781ddd91b72213d6604)) +* **jans-auth:** log current folder in UserJansExtUidAttributeTest test ([ca79ace](https://github.com/JanssenProject/jans/commit/ca79acec74cef8aeaa9274d3ce3a79657de755af)) +* **jans-auth:** log current folder in UserJansExtUidAttributeTest test ([#10667](https://github.com/JanssenProject/jans/issues/10667)) ([51ce4dc](https://github.com/JanssenProject/jans/commit/51ce4dcfb10c903f81a4fc0310218c4264d7d0cf)) +* **jans-auth:** Remove copyright footer ([#10666](https://github.com/JanssenProject/jans/issues/10666)) ([474661c](https://github.com/JanssenProject/jans/commit/474661cd100704592e7af7736330ed5aec877d96)) +* **jans-auth:** Show valid client name or id in consent form ([#10649](https://github.com/JanssenProject/jans/issues/10649)) ([5a53d53](https://github.com/JanssenProject/jans/commit/5a53d53b50dd0b559254a7008b5db69e08e64f1b)) +* **jans-cedarling:** add to decision log diagnostic info ([#10581](https://github.com/JanssenProject/jans/issues/10581)) ([6f8dc7c](https://github.com/JanssenProject/jans/commit/6f8dc7c08a0b3d810e2336abc7fe370fdd1f7147)) +* **jans-cedarling:** add WASM bindings for Cedarling ([#10542](https://github.com/JanssenProject/jans/issues/10542)) ([ec7c7e1](https://github.com/JanssenProject/jans/commit/ec7c7e186c4b2508a53fe1a7666a4e4023829489)) +* **jans-cedarling:** implement CEDARLING_ID_TOKEN_TRUST_MODE ([#10585](https://github.com/JanssenProject/jans/issues/10585)) ([d76f28c](https://github.com/JanssenProject/jans/commit/d76f28c64109a9f347058be2fa268abdca1d69e9)) +* **jans-cedarling:** Make SparKV use generics, and update MemoryLogger to use those. ([#10593](https://github.com/JanssenProject/jans/issues/10593)) ([25c7a49](https://github.com/JanssenProject/jans/commit/25c7a49c4f41c5750d24950511909db57226dda6)) +* **jans-fido2:** Add test cases for RP domain origin validation and handle multiple origins [#9248](https://github.com/JanssenProject/jans/issues/9248) ([22f0cbe](https://github.com/JanssenProject/jans/commit/22f0cbe4bdc41888c6caa5efa1ab249c6fc47298)) +* **jans-fido2:** Add test cases for RP domain origin. ([#10572](https://github.com/JanssenProject/jans/issues/10572)) ([22f0cbe](https://github.com/JanssenProject/jans/commit/22f0cbe4bdc41888c6caa5efa1ab249c6fc47298)) +* **jans-lock:** lock should collect MAU and MAC based on log entries… ([#10328](https://github.com/JanssenProject/jans/issues/10328)) ([b8a7e1a](https://github.com/JanssenProject/jans/commit/b8a7e1a493a2e7059a8e220eb6ff4305866bfc70)) +* **jans-pycloudlib:** secure mounted configuration schema ([#10551](https://github.com/JanssenProject/jans/issues/10551)) ([2d27184](https://github.com/JanssenProject/jans/commit/2d27184ac81c57596b527143c0a60fec6761cf02)) + + +### Bug Fixes + +* **actions:** immutable github sha instead of github head_ref ([5091b56](https://github.com/JanssenProject/jans/commit/5091b56102be0dd1d683d74703b8352c8cb27693)) +* **agama:** update expected status code ([#10618](https://github.com/JanssenProject/jans/issues/10618)) ([c0dce75](https://github.com/JanssenProject/jans/commit/c0dce7530cf3dd304238b6d93ded693a5c3b81b5)) +* build acct linking agama ([#10575](https://github.com/JanssenProject/jans/issues/10575)) ([85b95ec](https://github.com/JanssenProject/jans/commit/85b95ec91f17ca26964558bb085a5bbc9aad13d0)) +* **cloud-native:** add missing endpoints to aio image ([#10595](https://github.com/JanssenProject/jans/issues/10595)) ([5fb1903](https://github.com/JanssenProject/jans/commit/5fb1903f5f5f49b188b6daf6c194ffb3ecadfd38)) +* **cloud-native:** resolve image builds on slow network ([#10524](https://github.com/JanssenProject/jans/issues/10524)) ([3409098](https://github.com/JanssenProject/jans/commit/3409098777f8696d7a6485e0ae6b226f55cbb6ec)) +* **config-api:** setting agama flow as auth method ([#10539](https://github.com/JanssenProject/jans/issues/10539)) ([3c00152](https://github.com/JanssenProject/jans/commit/3c0015224aec4a2333735478333496baa31ef9b6)) +* **docker-jans-auth:** missing permissions on /app/templates ([#10641](https://github.com/JanssenProject/jans/issues/10641)) ([f1b3ca2](https://github.com/JanssenProject/jans/commit/f1b3ca2294f4eb56bcde3f78a4f6b05f0bb879d7)) +* **docker-jans-persistence-loader:** exclude external tables when creating indexes ([#10522](https://github.com/JanssenProject/jans/issues/10522)) ([9610bc1](https://github.com/JanssenProject/jans/commit/9610bc15908331e8344dfaed16ee8a397bd999d5)) +* **docs:** add documentation for `jans` wrapper command ([#10611](https://github.com/JanssenProject/jans/issues/10611)) ([b65f5e1](https://github.com/JanssenProject/jans/commit/b65f5e109bfc41fa6bc39da8466b4b94c6020788)) +* **docs:** correct the file name for Keycloak link document ([#10680](https://github.com/JanssenProject/jans/issues/10680)) ([35e6ef0](https://github.com/JanssenProject/jans/commit/35e6ef01e8a8c330604249a4fe415d50dea4cf4f)) +* **docs:** minor fixes to the Jans README ([#10604](https://github.com/JanssenProject/jans/issues/10604)) ([41bf8b8](https://github.com/JanssenProject/jans/commit/41bf8b892bc69c665a4e27b265de11eceab0ea6d)) +* **docs:** minor URL fixes ([ba9908d](https://github.com/JanssenProject/jans/commit/ba9908dea395a9a8974e55a4cf9a079749bfcda3)) +* **docs:** minor URL fixes ([#10632](https://github.com/JanssenProject/jans/issues/10632)) ([ba9908d](https://github.com/JanssenProject/jans/commit/ba9908dea395a9a8974e55a4cf9a079749bfcda3)) +* **docs:** remove tent references ([040ff17](https://github.com/JanssenProject/jans/commit/040ff17942019bc10433ce17d819b8d8474f13c8)) +* **docs:** remove tent references from documentation ([#10603](https://github.com/JanssenProject/jans/issues/10603)) ([040ff17](https://github.com/JanssenProject/jans/commit/040ff17942019bc10433ce17d819b8d8474f13c8)) +* implement missing method from interface ([#10646](https://github.com/JanssenProject/jans/issues/10646)) ([2381a09](https://github.com/JanssenProject/jans/commit/2381a09cd7a51f2582c041bcab4941d7c5138696)) +* **jans-auth-server:** access evaluation tests are failing on jenkins ([#10630](https://github.com/JanssenProject/jans/issues/10630)) ([8789289](https://github.com/JanssenProject/jans/commit/87892899b455b009d1493ef47b7ab7ae8dccb69b)) +* **jans-auth-server:** access evaluation tests are failing on jenkins [#10629](https://github.com/JanssenProject/jans/issues/10629) ([37e177c](https://github.com/JanssenProject/jans/commit/37e177c1de0f3efd4dabc14da0bbd6fef3072d62)) +* **jans-auth-server:** access evaluation tests are failing on jenkins [#10629](https://github.com/JanssenProject/jans/issues/10629) ([8789289](https://github.com/JanssenProject/jans/commit/87892899b455b009d1493ef47b7ab7ae8dccb69b)) +* **jans-auth-server:** challenge endpoint returns 400 if authorize throws an unexpected exception ([#10553](https://github.com/JanssenProject/jans/issues/10553)) ([02c3df7](https://github.com/JanssenProject/jans/commit/02c3df77be977248529ccfc23145a37049e12633)) +* **jans-auth-server:** failing test - SelectAccountHttpTest selectAccountTest [#10647](https://github.com/JanssenProject/jans/issues/10647) ([d19e34f](https://github.com/JanssenProject/jans/commit/d19e34f943a34cf7ed2fa5f5ece7e17c97eaa5a2)) +* **jans-auth-server:** NPE during client name rendering [#10663](https://github.com/JanssenProject/jans/issues/10663) ([9dbcb0d](https://github.com/JanssenProject/jans/commit/9dbcb0dead119e2bf780bda9153b84c8ce379266)) +* **jans-auth-server:** test is failing - TokenRestWebServiceHttpTest requestAccessTokenFail [#10637](https://github.com/JanssenProject/jans/issues/10637) ([db38009](https://github.com/JanssenProject/jans/commit/db38009d97a29b57e9b180c8b1e7314fa4edb5f2)) +* **jans-auth-server:** tests corrections ([ef8a07a](https://github.com/JanssenProject/jans/commit/ef8a07aced8eb7eafaac1bda7f36d26a6909bd85)) +* **jans-auth:** log current folder in UserServiceTest test ([#10675](https://github.com/JanssenProject/jans/issues/10675)) ([1468b47](https://github.com/JanssenProject/jans/commit/1468b477b4b7956b731930ddb0381513b47d17a8)) +* **jans-cedarling:** fix Cedarling WASM docs ([#10601](https://github.com/JanssenProject/jans/issues/10601)) ([7690030](https://github.com/JanssenProject/jans/commit/76900307ca0fab78a981c517585499f7b75685e0)) +* **jans-kc-link:** remove default keycloak configs ([#10679](https://github.com/JanssenProject/jans/issues/10679)) ([261c936](https://github.com/JanssenProject/jans/commit/261c936acf732fa249231ff65193de68a47558ca)) +* **jans-keycloak-link:** unstatisfied dependencies ([#10627](https://github.com/JanssenProject/jans/issues/10627)) ([721b8fe](https://github.com/JanssenProject/jans/commit/721b8fe5b68e23988ca298bb06cc405091945f67)) +* **startjanssendemo:** enhance the script ([1ba8e98](https://github.com/JanssenProject/jans/commit/1ba8e9883ec7e1bcf1aa9b57bbe100211edfadb2)) +* **startjanssendemo:** remove unneeded code and packages installation ([#10531](https://github.com/JanssenProject/jans/issues/10531)) ([1ba8e98](https://github.com/JanssenProject/jans/commit/1ba8e9883ec7e1bcf1aa9b57bbe100211edfadb2)) +* update token script (role_based_scopes_update_token) should reje… ([#10536](https://github.com/JanssenProject/jans/issues/10536)) ([3cd5d88](https://github.com/JanssenProject/jans/commit/3cd5d88af2bf4850779b4107d939b97e1e79624b)) +* update token script (role_based_scopes_update_token) should reject the tampered user-info-jwt [#10535](https://github.com/JanssenProject/jans/issues/10535) ([3cd5d88](https://github.com/JanssenProject/jans/commit/3cd5d88af2bf4850779b4107d939b97e1e79624b)) + ## [1.2.0](https://github.com/JanssenProject/jans/compare/v1.1.6...v1.2.0) (2024-12-24) 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/agama/pom.xml b/agama/pom.xml index 3cde1ef02a7..c4481008df8 100644 --- a/agama/pom.xml +++ b/agama/pom.xml @@ -5,7 +5,7 @@ io.jans agama pom - 0.0.0-nightly + 1.3.0 agama parent diff --git a/agama/transpiler/pom.xml b/agama/transpiler/pom.xml index d5e1cb55071..cfdb9b2d472 100644 --- a/agama/transpiler/pom.xml +++ b/agama/transpiler/pom.xml @@ -9,7 +9,7 @@ io.jans agama - 0.0.0-nightly + 1.3.0 diff --git a/automation/startjanssendemo.sh b/automation/startjanssendemo.sh index 5f73b928919..1a784953a5f 100644 --- a/automation/startjanssendemo.sh +++ b/automation/startjanssendemo.sh @@ -22,7 +22,7 @@ if [[ $JANS_PERSISTENCE != "MYSQL" ]] && [[ $JANS_PERSISTENCE != "PGSQL" ]]; the exit 1 fi if [[ -z $JANS_VERSION ]]; then - JANS_VERSION="0.0.0-nightly" + JANS_VERSION="1.3.0" fi LOG_TARGET="FILE" LOG_LEVEL="TRACE" diff --git a/charts/janssen-all-in-one/Chart.yaml b/charts/janssen-all-in-one/Chart.yaml index 6b981a1f0b4..dfe1715715a 100644 --- a/charts/janssen-all-in-one/Chart.yaml +++ b/charts/janssen-all-in-one/Chart.yaml @@ -3,14 +3,14 @@ annotations: artifacthub.io/containsSecurityUpdates: 'true' artifacthub.io/images: | - name: all-in-one - image: ghcr.io/janssenproject/jans/all-in-one:0.0.0-nightly + image: ghcr.io/janssenproject/jans/all-in-one:1.3.0-1 artifacthub.io/license: Apache-2.0 artifacthub.io/prerelease: 'true' catalog.cattle.io/certified: partner catalog.cattle.io/release-name: janssen-all-in-one catalog.cattle.io/display-name: Janssen Cloud Identity and Access Management All-in-One apiVersion: v2 -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" icon: >- https://raw.githubusercontent.com/JanssenProject/jans/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png home: https://jans.io @@ -22,4 +22,4 @@ maintainers: email: support@jans.io description: Janssen Access and Identity Management All-in-One Chart. This chart deploys the selected janssen microservice all in one deployment. name: janssen-all-in-one -version: 0.0.0-nightly +version: 1.3.0 diff --git a/charts/janssen-all-in-one/README.md b/charts/janssen-all-in-one/README.md index b02f1bcf0ef..743b13adf1e 100644 --- a/charts/janssen-all-in-one/README.md +++ b/charts/janssen-all-in-one/README.md @@ -1,6 +1,6 @@ # janssen-all-in-one -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Janssen Access and Identity Management All-in-One Chart. This chart deploys the selected janssen microservice all in one deployment. @@ -30,7 +30,7 @@ Kubernetes: `>=v1.22.0-0` | adminPassword | string | `"Test1234#"` | Admin password to log in to the UI. | | alb.ingress | bool | `false` | switches the service to Nodeport for ALB ingress | | auth-server | object | `{"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","authSigKeys":"RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512","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}` | Parameters used globally across all services helm charts. | -| auth-server-key-rotation | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","enabled":true,"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/certmanager","tag":"0.0.0-nightly"},"initKeysLife":48,"keysLife":48,"keysPushDelay":0,"keysPushStrategy":"NEWER","keysStrategy":"NEWER","lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for regenerating auth-keys per x hours | +| auth-server-key-rotation | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","enabled":true,"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/certmanager","tag":"1.3.0-1"},"initKeysLife":48,"keysLife":48,"keysPushDelay":0,"keysPushStrategy":"NEWER","keysStrategy":"NEWER","lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for regenerating auth-keys per x hours | | auth-server-key-rotation.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | auth-server-key-rotation.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | auth-server-key-rotation.customCommand | list | `[]` | Add custom jobs's command. If passed, it will override the default conditional command. | @@ -41,7 +41,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server-key-rotation.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | auth-server-key-rotation.image.pullSecrets | list | `[]` | Image Pull Secrets | | auth-server-key-rotation.image.repository | string | `"ghcr.io/janssenproject/jans/certmanager"` | Image to use for deploying. | -| auth-server-key-rotation.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| auth-server-key-rotation.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | auth-server-key-rotation.initKeysLife | int | `48` | The initial auth server key rotation keys life in hours | | auth-server-key-rotation.keysLife | int | `48` | Auth server key rotation keys life in hours | | auth-server-key-rotation.keysPushDelay | int | `0` | Delay (in seconds) before pushing private keys to Auth server | @@ -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. | @@ -262,7 +264,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"ghcr.io/janssenproject/jans/all-in-one"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | isFqdnRegistered | bool | `false` | Boolean flag to enable mapping lbIp to fqdn inside pods on clouds that provide static ip for load balancers. On cloud that provide only addresses to the LB this flag will enable a script to actively scan config.configmap.lbAddr and update the hosts file inside the pods automatically. | | istio.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | istio.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | @@ -271,7 +273,7 @@ Kubernetes: `>=v1.22.0-0` | istio.ingress | bool | `false` | Boolean flag that enables using istio gateway for Janssen. This assumes istio ingress is installed and hence the LB is available. | | istio.namespace | string | `"istio-system"` | The namespace istio is deployed in. The is normally istio-system. | | istio.tlsSecretName | string | `"istio-tls-certificate"` | | -| kc-scheduler | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","enabled":false,"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/kc-scheduler","tag":"0.0.0-nightly"},"interval":10,"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for synchronizing Keycloak SAML clients | +| kc-scheduler | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","enabled":false,"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/kc-scheduler","tag":"1.3.0-1"},"interval":10,"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for synchronizing Keycloak SAML clients | | kc-scheduler.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | kc-scheduler.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | kc-scheduler.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -282,7 +284,7 @@ Kubernetes: `>=v1.22.0-0` | kc-scheduler.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | kc-scheduler.image.pullSecrets | list | `[]` | Image Pull Secrets | | kc-scheduler.image.repository | string | `"ghcr.io/janssenproject/jans/kc-scheduler"` | Image to use for deploying. | -| kc-scheduler.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| kc-scheduler.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | kc-scheduler.interval | int | `10` | Interval of running the scheduler (in minutes) | | kc-scheduler.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | | kc-scheduler.resources.limits.cpu | string | `"300m"` | CPU limit. | 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..b50f9bc5262 100644 --- a/charts/janssen-all-in-one/values.yaml +++ b/charts/janssen-all-in-one/values.yaml @@ -300,7 +300,7 @@ auth-server-key-rotation: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/certmanager # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Auth server key rotation keys life in hours @@ -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: @@ -681,7 +685,7 @@ image: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/all-in-one # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -761,7 +765,7 @@ kc-scheduler: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/kc-scheduler # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Resource specs. diff --git a/charts/janssen/Chart.yaml b/charts/janssen/Chart.yaml index 740bc69b176..c15341352c5 100644 --- a/charts/janssen/Chart.yaml +++ b/charts/janssen/Chart.yaml @@ -3,34 +3,34 @@ annotations: artifacthub.io/containsSecurityUpdates: 'true' artifacthub.io/images: | - name: auth-server - image: ghcr.io/janssenproject/jans/auth-server:0.0.0-nightly + image: ghcr.io/janssenproject/jans/auth-server:1.3.0-1 - name: auth-server-key-rotation - image: ghcr.io/janssenproject/jans/certmanager:0.0.0-nightly + image: ghcr.io/janssenproject/jans/certmanager:1.3.0-1 - name: configuration-manager - image: ghcr.io/janssenproject/jans/configurator:0.0.0-nightly + image: ghcr.io/janssenproject/jans/configurator:1.3.0-1 - name: config-api - image: ghcr.io/janssenproject/jans/config-api:0.0.0-nightly + image: ghcr.io/janssenproject/jans/config-api:1.3.0-1 - name: fido2 - image: ghcr.io/janssenproject/jans/fido2:0.0.0-nightly + image: ghcr.io/janssenproject/jans/fido2:1.3.0-1 - name: persistence - image: ghcr.io/janssenproject/jans/persistence-loader:0.0.0-nightly + image: ghcr.io/janssenproject/jans/persistence-loader:1.3.0-1 - name: casa - image: ghcr.io/janssenproject/jans/casa:0.0.0-nightly + image: ghcr.io/janssenproject/jans/casa:1.3.0-1 - name: scim - image: ghcr.io/janssenproject/jans/scim:0.0.0-nightly + image: ghcr.io/janssenproject/jans/scim:1.3.0-1 - name: link - image: ghcr.io/janssenproject/jans/link:0.0.0-nightly + image: ghcr.io/janssenproject/jans/link:1.3.0-1 - name: saml - image: ghcr.io/janssenproject/jans/saml:0.0.0-nightly + image: ghcr.io/janssenproject/jans/saml:1.3.0-1 - name: kc-scheduler - image: ghcr.io/janssenproject/jans/kc-scheduler:0.0.0-nightly + image: ghcr.io/janssenproject/jans/kc-scheduler:1.3.0-1 artifacthub.io/license: Apache-2.0 - artifacthub.io/prerelease: 'true' + artifacthub.io/prerelease: 'false' catalog.cattle.io/certified: partner catalog.cattle.io/release-name: janssen catalog.cattle.io/display-name: Janssen Cloud Identity and Access Management Microservices apiVersion: v2 -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" icon: >- https://raw.githubusercontent.com/JanssenProject/jans/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png home: https://jans.io @@ -42,56 +42,56 @@ maintainers: email: support@jans.io description: Janssen Access and Identity Management Microservices Chart. This chart deploys each janssen microservice as a separate deployment. name: janssen -version: 0.0.0-nightly +version: 1.3.0 dependencies: - name: casa - version: 0.0.0-nightly + version: 1.3.0 condition: global.casa.enabled - name: config condition: global.config.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: config-api condition: global.config-api.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: auth-server condition: global.auth-server.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: fido2 condition: global.fido2.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: scim condition: global.scim.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: nginx-ingress condition: global.nginx-ingress.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: auth-server-key-rotation condition: global.auth-server-key-rotation.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: persistence condition: global.persistence.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: link condition: global.link.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: saml condition: global.saml.enabled - version: 0.0.0-nightly + version: 1.3.0 - name: cn-istio-ingress condition: global.istio.ingress - version: 0.0.0-nightly + version: 1.3.0 - name: kc-scheduler condition: global.kc-scheduler.enabled - version: 0.0.0-nightly + version: 1.3.0 diff --git a/charts/janssen/README.md b/charts/janssen/README.md index 56dace93960..dc279d9d3e8 100644 --- a/charts/janssen/README.md +++ b/charts/janssen/README.md @@ -1,6 +1,6 @@ # janssen -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Janssen Access and Identity Management Microservices Chart. This chart deploys each janssen microservice as a separate deployment. @@ -23,26 +23,26 @@ Kubernetes: `>=v1.22.0-0` | Repository | Name | Version | |------------|------|---------| -| | auth-server | 0.0.0-nightly | -| | auth-server-key-rotation | 0.0.0-nightly | -| | casa | 0.0.0-nightly | -| | cn-istio-ingress | 0.0.0-nightly | -| | config | 0.0.0-nightly | -| | config-api | 0.0.0-nightly | -| | fido2 | 0.0.0-nightly | -| | kc-scheduler | 0.0.0-nightly | -| | link | 0.0.0-nightly | -| | nginx-ingress | 0.0.0-nightly | -| | persistence | 0.0.0-nightly | -| | saml | 0.0.0-nightly | -| | scim | 0.0.0-nightly | +| | auth-server | 1.3.0 | +| | auth-server-key-rotation | 1.3.0 | +| | casa | 1.3.0 | +| | cn-istio-ingress | 1.3.0 | +| | config | 1.3.0 | +| | config-api | 1.3.0 | +| | fido2 | 1.3.0 | +| | kc-scheduler | 1.3.0 | +| | link | 1.3.0 | +| | nginx-ingress | 1.3.0 | +| | persistence | 1.3.0 | +| | saml | 1.3.0 | +| | scim | 1.3.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| -| auth-server | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/auth-server","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"2500m","memory":"2500Mi"},"requests":{"cpu":"2500m","memory":"2500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. | -| auth-server-key-rotation | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/certmanager","tag":"0.0.0-nightly"},"keysLife":48,"keysPushDelay":0,"keysPushStrategy":"NEWER","keysStrategy":"NEWER","lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for regenerating auth-keys per x hours | +| auth-server | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/auth-server","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"2500m","memory":"2500Mi"},"requests":{"cpu":"2500m","memory":"2500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. | +| auth-server-key-rotation | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/certmanager","tag":"1.3.0-1"},"keysLife":48,"keysPushDelay":0,"keysPushStrategy":"NEWER","keysStrategy":"NEWER","lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for regenerating auth-keys per x hours | | auth-server-key-rotation.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | auth-server-key-rotation.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | auth-server-key-rotation.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -52,7 +52,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server-key-rotation.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | auth-server-key-rotation.image.pullSecrets | list | `[]` | Image Pull Secrets | | auth-server-key-rotation.image.repository | string | `"ghcr.io/janssenproject/jans/certmanager"` | Image to use for deploying. | -| auth-server-key-rotation.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| auth-server-key-rotation.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | auth-server-key-rotation.keysLife | int | `48` | Auth server key rotation keys life in hours | | auth-server-key-rotation.keysPushDelay | int | `0` | Delay (in seconds) before pushing private keys to Auth server | | auth-server-key-rotation.keysPushStrategy | string | `"NEWER"` | Set key selection strategy after pushing private keys to Auth server (only takes effect when keysPushDelay value is greater than 0) | @@ -79,7 +79,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | auth-server.image.pullSecrets | list | `[]` | Image Pull Secrets | | auth-server.image.repository | string | `"ghcr.io/janssenproject/jans/auth-server"` | Image to use for deploying. | -| auth-server.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| auth-server.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | auth-server.livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | auth-server.livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | Executes the python3 healthcheck. https://github.com/JanssenProject/docker-jans-auth-server/blob/master/scripts/healthcheck.py | | auth-server.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -96,7 +96,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | auth-server.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | auth-server.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| casa | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/casa","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Janssen Casa ("Casa") is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Janssen Auth Server. | +| casa | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/casa","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Janssen Casa ("Casa") is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Janssen Auth Server. | | casa.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | casa.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | casa.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -109,7 +109,7 @@ Kubernetes: `>=v1.22.0-0` | casa.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | casa.image.pullSecrets | list | `[]` | Image Pull Secrets | | casa.image.repository | string | `"ghcr.io/janssenproject/jans/casa"` | Image to use for deploying. | -| casa.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| casa.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | casa.livenessProbe | object | `{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for casa if needed. | | casa.livenessProbe.httpGet.path | string | `"/jans-casa/health-check"` | http liveness probe endpoint | | casa.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -127,8 +127,8 @@ Kubernetes: `>=v1.22.0-0` | casa.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | casa.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | casa.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| config | object | `{"additionalAnnotations":{},"additionalLabels":{},"adminPassword":"Test1234#","city":"Austin","configmap":{"cnAwsAccessKeyId":"","cnAwsDefaultRegion":"us-west-1","cnAwsProfile":"janssen","cnAwsSecretAccessKey":"","cnAwsSecretsEndpointUrl":"","cnAwsSecretsNamePrefix":"janssen","cnAwsSecretsReplicaRegions":[],"cnCacheType":"NATIVE_PERSISTENCE","cnConfigKubernetesConfigMap":"cn","cnGoogleProjectId":"google-project-to-save-config-and-secrets-to","cnGoogleSecretManagerServiceAccount":"SWFtTm90YVNlcnZpY2VBY2NvdW50Q2hhbmdlTWV0b09uZQo=","cnGoogleSecretNamePrefix":"janssen","cnGoogleSecretVersionId":"latest","cnJettyRequestHeaderSize":8192,"cnMaxRamPercent":"75.0","cnMessageType":"DISABLED","cnOpaUrl":"http://opa.opa.svc.cluster.cluster.local:8181/v1","cnPersistenceHybridMapping":"{}","cnRedisSentinelGroup":"","cnRedisSslTruststore":"","cnRedisType":"STANDALONE","cnRedisUrl":"redis.redis.svc.cluster.local:6379","cnRedisUseSsl":false,"cnScimProtectionMode":"OAUTH","cnSecretKubernetesSecret":"cn","cnSqlDbDialect":"mysql","cnSqlDbHost":"my-release-mysql.default.svc.cluster.local","cnSqlDbName":"jans","cnSqlDbPort":3306,"cnSqlDbSchema":"","cnSqlDbTimezone":"UTC","cnSqlDbUser":"jans","cnSqldbUserPassword":"Test1234#","cnVaultAddr":"http://localhost:8200","cnVaultAppRolePath":"approle","cnVaultKvPath":"secret","cnVaultNamespace":"","cnVaultPrefix":"jans","cnVaultRoleId":"","cnVaultRoleIdFile":"/etc/certs/vault_role_id","cnVaultSecretId":"","cnVaultSecretIdFile":"/etc/certs/vault_secret_id","cnVaultVerify":false,"kcAdminPassword":"Test1234#","kcAdminUsername":"admin","kcDbPassword":"Test1234#","kcDbSchema":"keycloak","kcDbUrlDatabase":"keycloak","kcDbUrlHost":"mysql.kc.svc.cluster.local","kcDbUrlPort":3306,"kcDbUrlProperties":"?useUnicode=true&characterEncoding=UTF-8&character_set_server=utf8mb4","kcDbUsername":"keycloak","kcDbVendor":"mysql","kcLogLevel":"INFO","lbAddr":"","quarkusTransactionEnableRecovery":true},"countryCode":"US","customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","email":"support@jans.io","image":{"pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/configurator","tag":"0.0.0-nightly"},"lifecycle":{},"orgName":"Janssen","redisPassword":"P@assw0rd","resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"salt":"","state":"TX","usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Configuration parameters for setup and initial configuration secret and config layers used by Janssen services. | -| config-api | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/config-api","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"jans-config-api/api/v1/health/ready","port":8074},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Config Api endpoints can be used to configure the auth-server, which is an open-source OpenID Connect Provider (OP) and UMA Authorization Server (AS). | +| config | object | `{"additionalAnnotations":{},"additionalLabels":{},"adminPassword":"Test1234#","city":"Austin","configmap":{"cnAwsAccessKeyId":"","cnAwsDefaultRegion":"us-west-1","cnAwsProfile":"janssen","cnAwsSecretAccessKey":"","cnAwsSecretsEndpointUrl":"","cnAwsSecretsNamePrefix":"janssen","cnAwsSecretsReplicaRegions":[],"cnCacheType":"NATIVE_PERSISTENCE","cnConfigKubernetesConfigMap":"cn","cnGoogleProjectId":"google-project-to-save-config-and-secrets-to","cnGoogleSecretManagerServiceAccount":"SWFtTm90YVNlcnZpY2VBY2NvdW50Q2hhbmdlTWV0b09uZQo=","cnGoogleSecretNamePrefix":"janssen","cnGoogleSecretVersionId":"latest","cnJettyRequestHeaderSize":8192,"cnMaxRamPercent":"75.0","cnMessageType":"DISABLED","cnOpaUrl":"http://opa.opa.svc.cluster.cluster.local:8181/v1","cnPersistenceHybridMapping":"{}","cnRedisSentinelGroup":"","cnRedisSslTruststore":"","cnRedisType":"STANDALONE","cnRedisUrl":"redis.redis.svc.cluster.local:6379","cnRedisUseSsl":false,"cnScimProtectionMode":"OAUTH","cnSecretKubernetesSecret":"cn","cnSqlDbDialect":"mysql","cnSqlDbHost":"my-release-mysql.default.svc.cluster.local","cnSqlDbName":"jans","cnSqlDbPort":3306,"cnSqlDbSchema":"","cnSqlDbTimezone":"UTC","cnSqlDbUser":"jans","cnSqldbUserPassword":"Test1234#","cnVaultAddr":"http://localhost:8200","cnVaultAppRolePath":"approle","cnVaultKvPath":"secret","cnVaultNamespace":"","cnVaultPrefix":"jans","cnVaultRoleId":"","cnVaultRoleIdFile":"/etc/certs/vault_role_id","cnVaultSecretId":"","cnVaultSecretIdFile":"/etc/certs/vault_secret_id","cnVaultVerify":false,"kcAdminPassword":"Test1234#","kcAdminUsername":"admin","kcDbPassword":"Test1234#","kcDbSchema":"keycloak","kcDbUrlDatabase":"keycloak","kcDbUrlHost":"mysql.kc.svc.cluster.local","kcDbUrlPort":3306,"kcDbUrlProperties":"?useUnicode=true&characterEncoding=UTF-8&character_set_server=utf8mb4","kcDbUsername":"keycloak","kcDbVendor":"mysql","kcLogLevel":"INFO","lbAddr":"","quarkusTransactionEnableRecovery":true},"countryCode":"US","customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","email":"support@jans.io","image":{"pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/configurator","tag":"1.3.0-1"},"lifecycle":{},"orgName":"Janssen","redisPassword":"P@assw0rd","resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"salt":"","state":"TX","usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Configuration parameters for setup and initial configuration secret and config layers used by Janssen services. | +| config-api | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/config-api","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"jans-config-api/api/v1/health/ready","port":8074},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Config Api endpoints can be used to configure the auth-server, which is an open-source OpenID Connect Provider (OP) and UMA Authorization Server (AS). | | config-api.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | config-api.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | config-api.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -141,7 +141,7 @@ Kubernetes: `>=v1.22.0-0` | config-api.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | config-api.image.pullSecrets | list | `[]` | Image Pull Secrets | | config-api.image.repository | string | `"ghcr.io/janssenproject/jans/config-api"` | Image to use for deploying. | -| config-api.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| config-api.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | config-api.livenessProbe | object | `{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | config-api.livenessProbe.httpGet | object | `{"path":"/jans-config-api/api/v1/health/live","port":8074}` | http liveness probe endpoint | | config-api.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -188,14 +188,14 @@ Kubernetes: `>=v1.22.0-0` | config.configmap.cnSqlDbUser | string | `"jans"` | SQL database username. | | config.configmap.cnSqldbUserPassword | string | `"Test1234#"` | SQL password injected the secrets . | | config.configmap.cnVaultAddr | string | `"http://localhost:8200"` | Base URL of Vault. | -| config.configmap.cnVaultAppRolePath | string | `"approle"` | Path to Vault AppRole. | +| config.configmap.cnVaultAppRolePath | string | `"approle"` | Path to the Vault AppRole. | | config.configmap.cnVaultKvPath | string | `"secret"` | Path to Vault KV secrets engine. | | config.configmap.cnVaultNamespace | string | `""` | Vault namespace used to access the secrets. | | config.configmap.cnVaultPrefix | string | `"jans"` | Base prefix name used to access secrets. | | config.configmap.cnVaultRoleId | string | `""` | Vault AppRole RoleID. | -| config.configmap.cnVaultRoleIdFile | string | `"/etc/certs/vault_role_id"` | Path to file contains Vault AppRole role ID. | +| config.configmap.cnVaultRoleIdFile | string | `"/etc/certs/vault_role_id"` | Path to the file that contains Vault AppRole role ID. | | config.configmap.cnVaultSecretId | string | `""` | Vault AppRole SecretID. | -| config.configmap.cnVaultSecretIdFile | string | `"/etc/certs/vault_secret_id"` | Path to file contains Vault AppRole secret ID. | +| config.configmap.cnVaultSecretIdFile | string | `"/etc/certs/vault_secret_id"` | Path to the file that contains Vault AppRole secret ID. | | config.configmap.cnVaultVerify | bool | `false` | Verify connection to Vault. | | config.configmap.kcAdminPassword | string | `"Test1234#"` | Keycloak admin UI password | | config.configmap.kcAdminUsername | string | `"admin"` | Keycloak admin UI username | @@ -218,7 +218,7 @@ Kubernetes: `>=v1.22.0-0` | config.email | string | `"support@jans.io"` | Email address of the administrator usually. Used for certificate creation. | | config.image.pullSecrets | list | `[]` | Image Pull Secrets | | config.image.repository | string | `"ghcr.io/janssenproject/jans/configurator"` | Image to use for deploying. | -| config.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| config.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | config.orgName | string | `"Janssen"` | Organization name. Used for certificate creation. | | config.redisPassword | string | `"P@assw0rd"` | Redis admin password if `config.configmap.cnCacheType` is set to `REDIS`. | | config.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | @@ -233,7 +233,7 @@ Kubernetes: `>=v1.22.0-0` | config.usrEnvs.secret | object | `{}` | Add custom secret envs to the service. variable1: value1 | | config.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | config.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| fido2 | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/fido2","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"service":{"name":"http-fido2","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | FIDO 2.0 (FIDO2) is an open authentication standard that enables leveraging common devices to authenticate to online services in both mobile and desktop environments. | +| fido2 | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/fido2","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"service":{"name":"http-fido2","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | FIDO 2.0 (FIDO2) is an open authentication standard that enables leveraging common devices to authenticate to online services in both mobile and desktop environments. | | fido2.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | fido2.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | fido2.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -246,7 +246,7 @@ Kubernetes: `>=v1.22.0-0` | fido2.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | fido2.image.pullSecrets | list | `[]` | Image Pull Secrets | | fido2.image.repository | string | `"ghcr.io/janssenproject/jans/fido2"` | Image to use for deploying. | -| fido2.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| fido2.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | fido2.livenessProbe | object | `{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for the fido2 if needed. | | fido2.livenessProbe.httpGet | object | `{"path":"/jans-fido2/sys/health-check","port":"http-fido2"}` | http liveness probe endpoint | | fido2.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -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 | @@ -340,15 +340,17 @@ Kubernetes: `>=v1.22.0-0` | global.casa.ingress.casaEnabled | bool | `false` | Enable casa endpoints /casa | | global.casa.ingress.casaLabels | object | `{}` | Casa ingress resource labels. key app is taken | | global.cloud.testEnviroment | bool | `false` | Boolean flag if enabled will strip resources requests and limits from all services. | -| global.cnConfiguratorConfigurationFile | string | `"/etc/jans/conf/configuration.json"` | Path to configuration schema file | -| 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.cnConfiguratorConfigurationFile | string | `"/etc/jans/conf/configuration.json"` | Path to the configuration schema file | +| global.cnConfiguratorCustomSchema | object | `{"secretName":""}` | 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. | +| global.cnConfiguratorCustomSchema.secretName | string | `""` | The name of the Kubernetes secret used for storing custom configuration schema. | +| global.cnConfiguratorDumpFile | string | `"/etc/jans/conf/configuration.out.json"` | Path to the 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 the file that contains the key to encrypt/decrypt the 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 | | global.cnPrometheusPort | string | `""` | Port used by Prometheus JMX agent (default to empty string). To enable Prometheus JMX agent, set the value to a number. | -| global.cnSqlPasswordFile | string | `"/etc/jans/conf/sql_password"` | Path to SQL password file | +| global.cnSqlPasswordFile | string | `"/etc/jans/conf/sql_password"` | Path to the SQL password file | | global.config-api.appLoggers | object | `{"configApiLogLevel":"INFO","configApiLogTarget":"STDOUT","enableStdoutLogPrefix":"true","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"}` | App loggers can be configured to define where the logs will be redirected to and the level of each in which it should be displayed. | | global.config-api.appLoggers.configApiLogLevel | string | `"INFO"` | configapi.log level | | global.config-api.appLoggers.configApiLogTarget | string | `"STDOUT"` | configapi.log target | @@ -454,7 +456,7 @@ Kubernetes: `>=v1.22.0-0` | global.usrEnvs | object | `{"normal":{},"secret":{}}` | Add custom normal and secret envs to the service. Envs defined in global.userEnvs will be globally available to all services | | global.usrEnvs.normal | object | `{}` | Add custom normal envs to the service. variable1: value1 | | global.usrEnvs.secret | object | `{}` | Add custom secret envs to the service. variable1: value1 | -| kc-scheduler | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/kc-scheduler","tag":"0.0.0-nightly"},"interval":10,"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for synchronizing Keycloak SAML clients | +| kc-scheduler | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/kc-scheduler","tag":"1.3.0-1"},"interval":10,"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for synchronizing Keycloak SAML clients | | kc-scheduler.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | kc-scheduler.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | kc-scheduler.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -464,7 +466,7 @@ Kubernetes: `>=v1.22.0-0` | kc-scheduler.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | kc-scheduler.image.pullSecrets | list | `[]` | Image Pull Secrets | | kc-scheduler.image.repository | string | `"ghcr.io/janssenproject/jans/kc-scheduler"` | Image to use for deploying. | -| kc-scheduler.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| kc-scheduler.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | kc-scheduler.interval | int | `10` | Interval of running the scheduler (in minutes) | | kc-scheduler.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | | kc-scheduler.resources.limits.cpu | string | `"300m"` | CPU limit. | @@ -476,7 +478,7 @@ Kubernetes: `>=v1.22.0-0` | kc-scheduler.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | kc-scheduler.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | kc-scheduler.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| link | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/link","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Link. | +| link | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/link","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Link. | | link.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | link.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | link.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -489,7 +491,7 @@ Kubernetes: `>=v1.22.0-0` | link.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | link.image.pullSecrets | list | `[]` | Image Pull Secrets | | link.image.repository | string | `"ghcr.io/janssenproject/jans/link"` | Image to use for deploying. | -| link.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| link.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | link.livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | link.livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | http liveness probe endpoint | | link.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -510,7 +512,7 @@ Kubernetes: `>=v1.22.0-0` | nginx-ingress.ingress.additionalAnnotations | object | `{}` | Additional annotations that will be added across all ingress definitions in the format of {cert-manager.io/issuer: "letsencrypt-prod"} Enable client certificate authentication nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" Create the secret containing the trusted ca certificates nginx.ingress.kubernetes.io/auth-tls-secret: "janssen/tls-certificate" Specify the verification depth in the client certificates chain nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1" Specify if certificates are passed to upstream server nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true" | | nginx-ingress.ingress.additionalLabels | object | `{}` | Additional labels that will be added across all ingress definitions in the format of {mylabel: "myapp"} | | nginx-ingress.ingress.tls | list | `[{"hosts":["demoexample.jans.io"],"secretName":"tls-certificate"}]` | Secrets holding HTTPS CA cert and key. | -| persistence | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/persistence-loader","tag":"0.0.0-nightly"},"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Job to generate data and initial config for Janssen Server persistence layer. | +| persistence | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/persistence-loader","tag":"1.3.0-1"},"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Job to generate data and initial config for Janssen Server persistence layer. | | persistence.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | persistence.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | persistence.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -520,7 +522,7 @@ Kubernetes: `>=v1.22.0-0` | persistence.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | persistence.image.pullSecrets | list | `[]` | Image Pull Secrets | | persistence.image.repository | string | `"ghcr.io/janssenproject/jans/persistence-loader"` | Image to use for deploying. | -| persistence.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| persistence.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | persistence.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | | persistence.resources.limits.cpu | string | `"300m"` | CPU limit | | persistence.resources.limits.memory | string | `"300Mi"` | Memory limit. | @@ -531,7 +533,7 @@ Kubernetes: `>=v1.22.0-0` | persistence.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | persistence.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | persistence.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| saml | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/saml","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | SAML. | +| saml | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/saml","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | SAML. | | saml.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | saml.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | saml.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -544,7 +546,7 @@ Kubernetes: `>=v1.22.0-0` | saml.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | saml.image.pullSecrets | list | `[]` | Image Pull Secrets | | saml.image.repository | string | `"ghcr.io/janssenproject/jans/saml"` | Image to use for deploying. | -| saml.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| saml.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | saml.livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | saml.livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | http liveness probe endpoint | | saml.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -561,7 +563,7 @@ Kubernetes: `>=v1.22.0-0` | saml.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | saml.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | saml.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| scim | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/scim","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"service":{"name":"http-scim","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | System for Cross-domain Identity Management (SCIM) version 2.0 | +| scim | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/scim","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"service":{"name":"http-scim","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | System for Cross-domain Identity Management (SCIM) version 2.0 | | scim.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | scim.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | scim.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -574,7 +576,7 @@ Kubernetes: `>=v1.22.0-0` | scim.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | scim.image.pullSecrets | list | `[]` | Image Pull Secrets | | scim.image.repository | string | `"ghcr.io/janssenproject/jans/scim"` | Image to use for deploying. | -| scim.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| scim.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | scim.livenessProbe | object | `{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for SCIM if needed. | | scim.livenessProbe.httpGet.path | string | `"/jans-scim/sys/health-check"` | http liveness probe endpoint | | scim.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | diff --git a/charts/janssen/charts/auth-server-key-rotation/Chart.yaml b/charts/janssen/charts/auth-server-key-rotation/Chart.yaml index 6991669c0af..af6252ae546 100644 --- a/charts/janssen/charts/auth-server-key-rotation/Chart.yaml +++ b/charts/janssen/charts/auth-server-key-rotation/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: auth-server-key-rotation -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Responsible for regenerating auth-keys per x hours type: application @@ -15,4 +15,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" \ No newline at end of file +appVersion: "1.3.0" \ No newline at end of file diff --git a/charts/janssen/charts/auth-server-key-rotation/README.md b/charts/janssen/charts/auth-server-key-rotation/README.md index bc359502b32..8ffadd66eb0 100644 --- a/charts/janssen/charts/auth-server-key-rotation/README.md +++ b/charts/janssen/charts/auth-server-key-rotation/README.md @@ -1,6 +1,6 @@ # auth-server-key-rotation -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Responsible for regenerating auth-keys per x hours @@ -34,7 +34,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/certmanager"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | keysLife | int | `48` | Auth server key rotation keys life in hours | | keysPushDelay | int | `0` | Delay (in seconds) before pushing private keys to Auth server | | keysPushStrategy | string | `"NEWER"` | Set key selection strategy after pushing private keys to Auth server (only takes effect when keysPushDelay value is greater than 0) | diff --git a/charts/janssen/charts/auth-server-key-rotation/values.yaml b/charts/janssen/charts/auth-server-key-rotation/values.yaml index f2c4cf65f8e..d8f13105e97 100644 --- a/charts/janssen/charts/auth-server-key-rotation/values.yaml +++ b/charts/janssen/charts/auth-server-key-rotation/values.yaml @@ -18,7 +18,7 @@ image: # -- Image to use for deploying. repository: janssenproject/certmanager # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Auth server key rotation keys life in hours diff --git a/charts/janssen/charts/auth-server/Chart.yaml b/charts/janssen/charts/auth-server/Chart.yaml index c63adf27402..6e32a73ea3b 100644 --- a/charts/janssen/charts/auth-server/Chart.yaml +++ b/charts/janssen/charts/auth-server/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: auth-server -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/auth-server/README.md b/charts/janssen/charts/auth-server/README.md index 4c790700e5e..31ade4113c8 100644 --- a/charts/janssen/charts/auth-server/README.md +++ b/charts/janssen/charts/auth-server/README.md @@ -1,6 +1,6 @@ # auth-server -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. @@ -37,7 +37,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/auth-server"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | Executes the python3 healthcheck. | diff --git a/charts/janssen/charts/auth-server/values.yaml b/charts/janssen/charts/auth-server/values.yaml index 589e50d4205..7d784a561b4 100644 --- a/charts/janssen/charts/auth-server/values.yaml +++ b/charts/janssen/charts/auth-server/values.yaml @@ -28,7 +28,7 @@ image: # -- Image to use for deploying. repository: janssenproject/auth-server # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. diff --git a/charts/janssen/charts/casa/Chart.yaml b/charts/janssen/charts/casa/Chart.yaml index 88779ffda92..5ec173204dc 100644 --- a/charts/janssen/charts/casa/Chart.yaml +++ b/charts/janssen/charts/casa/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: casa -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.21.0-0" description: Jans Casa ("Casa") is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Jans Server. type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/casa/README.md b/charts/janssen/charts/casa/README.md index 9e37fc5008c..a3b98184b50 100644 --- a/charts/janssen/charts/casa/README.md +++ b/charts/janssen/charts/casa/README.md @@ -1,6 +1,6 @@ # casa -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Jans Casa ("Casa") is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Jans Server. @@ -38,7 +38,7 @@ Kubernetes: `>=v1.21.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/casa"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for casa if needed. | | livenessProbe.httpGet.path | string | `"/jans-casa/health-check"` | http liveness probe endpoint | diff --git a/charts/janssen/charts/casa/values.yaml b/charts/janssen/charts/casa/values.yaml index 382f7c571e2..6e5685d30ab 100644 --- a/charts/janssen/charts/casa/values.yaml +++ b/charts/janssen/charts/casa/values.yaml @@ -27,7 +27,7 @@ image: # -- Image to use for deploying. repository: janssenproject/casa # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. diff --git a/charts/janssen/charts/cn-istio-ingress/Chart.yaml b/charts/janssen/charts/cn-istio-ingress/Chart.yaml index 9dd6cae113d..c81c03ecdf1 100644 --- a/charts/janssen/charts/cn-istio-ingress/Chart.yaml +++ b/charts/janssen/charts/cn-istio-ingress/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: cn-istio-ingress -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.21.0-0" description: Istio Gateway type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/cn-istio-ingress/README.md b/charts/janssen/charts/cn-istio-ingress/README.md index 9cf79ca694d..e7df4ac67f9 100644 --- a/charts/janssen/charts/cn-istio-ingress/README.md +++ b/charts/janssen/charts/cn-istio-ingress/README.md @@ -1,6 +1,6 @@ # cn-istio-ingress -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Istio Gateway diff --git a/charts/janssen/charts/config-api/Chart.yaml b/charts/janssen/charts/config-api/Chart.yaml index c1788343421..c92cc176697 100644 --- a/charts/janssen/charts/config-api/Chart.yaml +++ b/charts/janssen/charts/config-api/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: config-api -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Jans Config Api endpoints can be used to configure jans-auth-server, which is an open-source OpenID Connect Provider (OP) and UMA Authorization Server (AS) type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/config-api/README.md b/charts/janssen/charts/config-api/README.md index 738332940d2..64a70f97d01 100644 --- a/charts/janssen/charts/config-api/README.md +++ b/charts/janssen/charts/config-api/README.md @@ -1,6 +1,6 @@ # config-api -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Jans Config Api endpoints can be used to configure jans-auth-server, which is an open-source OpenID Connect Provider (OP) and UMA Authorization Server (AS) @@ -39,7 +39,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/config-api"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | livenessProbe.httpGet | object | `{"path":"/jans-config-api/api/v1/health/live","port":8074}` | Executes the python3 healthcheck. | diff --git a/charts/janssen/charts/config-api/values.yaml b/charts/janssen/charts/config-api/values.yaml index 1c98156a67c..ebb5beeb857 100644 --- a/charts/janssen/charts/config-api/values.yaml +++ b/charts/janssen/charts/config-api/values.yaml @@ -31,7 +31,7 @@ image: # -- Image to use for deploying. repository: janssenproject/config-api # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. diff --git a/charts/janssen/charts/config/Chart.yaml b/charts/janssen/charts/config/Chart.yaml index 06f47b9d972..15f0615bcdd 100644 --- a/charts/janssen/charts/config/Chart.yaml +++ b/charts/janssen/charts/config/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: config -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Configuration parameters for setup and initial configuration secret and config layers used by Janssen services. type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/config/README.md b/charts/janssen/charts/config/README.md index 891a007874b..c62f8ffb46d 100644 --- a/charts/janssen/charts/config/README.md +++ b/charts/janssen/charts/config/README.md @@ -1,6 +1,6 @@ # config -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Configuration parameters for setup and initial configuration secret and config layers used by Janssen services. @@ -91,7 +91,7 @@ Kubernetes: `>=v1.22.0-0` | fullNameOverride | string | `""` | | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/configurator"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | migration | object | `{"enabled":false,"migrationDataFormat":"ldif","migrationDir":"/ce-migration"}` | CE to CN Migration section | | migration.enabled | bool | `false` | Boolean flag to enable migration from CE | 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/charts/config/values.yaml b/charts/janssen/charts/config/values.yaml index 6efe9c4c1ca..c1ddc9c814e 100644 --- a/charts/janssen/charts/config/values.yaml +++ b/charts/janssen/charts/config/values.yaml @@ -146,7 +146,7 @@ image: # -- Image to use for deploying. repository: janssenproject/configurator # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Organization name. Used for certificate creation. diff --git a/charts/janssen/charts/fido2/Chart.yaml b/charts/janssen/charts/fido2/Chart.yaml index bbb0d286159..e6cb608836a 100644 --- a/charts/janssen/charts/fido2/Chart.yaml +++ b/charts/janssen/charts/fido2/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: fido2 -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: FIDO 2.0 (FIDO2) is an open authentication standard that enables leveraging common devices to authenticate to online services in both mobile and desktop environments. type: application @@ -18,4 +18,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/fido2/README.md b/charts/janssen/charts/fido2/README.md index 965032156a2..856c11c119b 100644 --- a/charts/janssen/charts/fido2/README.md +++ b/charts/janssen/charts/fido2/README.md @@ -1,6 +1,6 @@ # fido2 -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) FIDO 2.0 (FIDO2) is an open authentication standard that enables leveraging common devices to authenticate to online services in both mobile and desktop environments. @@ -38,7 +38,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/fido2"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for the fido2 if needed. | | livenessProbe.httpGet | object | `{"path":"/jans-fido2/sys/health-check","port":"http-fido2"}` | http liveness probe endpoint | diff --git a/charts/janssen/charts/fido2/values.yaml b/charts/janssen/charts/fido2/values.yaml index 75601f95cc2..c21e232d1fa 100644 --- a/charts/janssen/charts/fido2/values.yaml +++ b/charts/janssen/charts/fido2/values.yaml @@ -29,7 +29,7 @@ image: # -- Image to use for deploying. repository: janssenproject/fido2 # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. diff --git a/charts/janssen/charts/kc-scheduler/Chart.yaml b/charts/janssen/charts/kc-scheduler/Chart.yaml index 5865904d54e..28c7279fdca 100644 --- a/charts/janssen/charts/kc-scheduler/Chart.yaml +++ b/charts/janssen/charts/kc-scheduler/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: kc-scheduler -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Responsible for synchronizing Keycloak SAML clients type: application @@ -16,4 +16,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/kc-scheduler/README.md b/charts/janssen/charts/kc-scheduler/README.md index e6cd9072742..b7d87ac9a12 100644 --- a/charts/janssen/charts/kc-scheduler/README.md +++ b/charts/janssen/charts/kc-scheduler/README.md @@ -1,6 +1,6 @@ # kc-scheduler -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Responsible for synchronizing Keycloak SAML clients @@ -33,7 +33,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"ghcr.io/janssenproject/jans/kc-scheduler"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | interval | int | `10` | Interval of running the scheduler (in minutes) | | lifecycle | object | `{}` | | | resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | diff --git a/charts/janssen/charts/kc-scheduler/values.yaml b/charts/janssen/charts/kc-scheduler/values.yaml index 98ec8bf0fbc..401de0aed6d 100644 --- a/charts/janssen/charts/kc-scheduler/values.yaml +++ b/charts/janssen/charts/kc-scheduler/values.yaml @@ -16,7 +16,7 @@ image: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/kc-scheduler # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Resource specs. diff --git a/charts/janssen/charts/link/Chart.yaml b/charts/janssen/charts/link/Chart.yaml index 240add79c74..33d29216521 100644 --- a/charts/janssen/charts/link/Chart.yaml +++ b/charts/janssen/charts/link/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: link -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Jans Link type: application @@ -15,4 +15,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/link/README.md b/charts/janssen/charts/link/README.md index dadd4e3630a..b33d8580f66 100644 --- a/charts/janssen/charts/link/README.md +++ b/charts/janssen/charts/link/README.md @@ -1,6 +1,6 @@ # link -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Jans Link @@ -39,7 +39,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"ghcr.io/janssenproject/jans/link"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the link if needed. | | livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | Executes the python3 healthcheck. | diff --git a/charts/janssen/charts/link/values.yaml b/charts/janssen/charts/link/values.yaml index 98d1542a3d6..07eb249bbfe 100644 --- a/charts/janssen/charts/link/values.yaml +++ b/charts/janssen/charts/link/values.yaml @@ -31,7 +31,7 @@ image: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/link # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. diff --git a/charts/janssen/charts/nginx-ingress/Chart.yaml b/charts/janssen/charts/nginx-ingress/Chart.yaml index 43beb7c60bf..09b42499c51 100644 --- a/charts/janssen/charts/nginx-ingress/Chart.yaml +++ b/charts/janssen/charts/nginx-ingress/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: nginx-ingress -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Nginx ingress definitions chart type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/nginx-ingress/README.md b/charts/janssen/charts/nginx-ingress/README.md index f65059771b7..5ad15595f88 100644 --- a/charts/janssen/charts/nginx-ingress/README.md +++ b/charts/janssen/charts/nginx-ingress/README.md @@ -1,6 +1,6 @@ # nginx-ingress -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Nginx ingress definitions chart diff --git a/charts/janssen/charts/persistence/Chart.yaml b/charts/janssen/charts/persistence/Chart.yaml index 6528003477d..cfcbfc77e81 100644 --- a/charts/janssen/charts/persistence/Chart.yaml +++ b/charts/janssen/charts/persistence/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: persistence -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: Job to generate data and initial config for Janssen Server persistence layer. type: application @@ -15,5 +15,5 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/persistence/README.md b/charts/janssen/charts/persistence/README.md index 85b30cf685e..b1abce75d77 100644 --- a/charts/janssen/charts/persistence/README.md +++ b/charts/janssen/charts/persistence/README.md @@ -1,6 +1,6 @@ # persistence -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Job to generate data and initial config for Janssen Server persistence layer. @@ -34,7 +34,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/persistence"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | imagePullSecrets | list | `[]` | | | lifecycle | object | `{}` | | | nameOverride | string | `""` | | diff --git a/charts/janssen/charts/persistence/values.yaml b/charts/janssen/charts/persistence/values.yaml index 78735cbfa19..9c26a2ec6a2 100644 --- a/charts/janssen/charts/persistence/values.yaml +++ b/charts/janssen/charts/persistence/values.yaml @@ -18,7 +18,7 @@ image: # -- Image to use for deploying. repository: janssenproject/persistence # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Resource specs. diff --git a/charts/janssen/charts/saml/Chart.yaml b/charts/janssen/charts/saml/Chart.yaml index 4b9c55d9b7e..36d147c89e0 100644 --- a/charts/janssen/charts/saml/Chart.yaml +++ b/charts/janssen/charts/saml/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: saml -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.21.0-0" description: Jans SAML type: application @@ -15,4 +15,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/saml/README.md b/charts/janssen/charts/saml/README.md index 0659af4fe85..070e11f5259 100644 --- a/charts/janssen/charts/saml/README.md +++ b/charts/janssen/charts/saml/README.md @@ -1,6 +1,6 @@ # saml -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Jans SAML @@ -37,7 +37,7 @@ Kubernetes: `>=v1.21.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/saml"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for saml if needed. | | livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | Executes the python3 healthcheck. | diff --git a/charts/janssen/charts/saml/values.yaml b/charts/janssen/charts/saml/values.yaml index dd888b2c947..1849f7a9499 100644 --- a/charts/janssen/charts/saml/values.yaml +++ b/charts/janssen/charts/saml/values.yaml @@ -27,7 +27,7 @@ image: # -- Image to use for deploying. repository: janssenproject/saml # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. diff --git a/charts/janssen/charts/scim/Chart.yaml b/charts/janssen/charts/scim/Chart.yaml index 22d24108aae..dc26dd046b1 100644 --- a/charts/janssen/charts/scim/Chart.yaml +++ b/charts/janssen/charts/scim/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: scim -version: 0.0.0-nightly +version: 1.3.0 kubeVersion: ">=v1.22.0-0" description: System for Cross-domain Identity Management (SCIM) version 2.0 type: application @@ -17,4 +17,4 @@ maintainers: email: support@jans.io url: https://github.com/moabu icon: https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_favicon_transparent_50px_50px.png -appVersion: "0.0.0-nightly" +appVersion: "1.3.0" diff --git a/charts/janssen/charts/scim/README.md b/charts/janssen/charts/scim/README.md index 906e70cf1a5..29b544ee89f 100644 --- a/charts/janssen/charts/scim/README.md +++ b/charts/janssen/charts/scim/README.md @@ -1,6 +1,6 @@ # scim -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) System for Cross-domain Identity Management (SCIM) version 2.0 @@ -37,7 +37,7 @@ Kubernetes: `>=v1.22.0-0` | image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | image.pullSecrets | list | `[]` | Image Pull Secrets | | image.repository | string | `"janssenproject/scim"` | Image to use for deploying. | -| image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | | livenessProbe | object | `{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for SCIM if needed. | | livenessProbe.httpGet.path | string | `"/jans-scim/sys/health-check"` | http liveness probe endpoint | diff --git a/charts/janssen/charts/scim/values.yaml b/charts/janssen/charts/scim/values.yaml index d27dbf2824a..386d6569af5 100644 --- a/charts/janssen/charts/scim/values.yaml +++ b/charts/janssen/charts/scim/values.yaml @@ -28,7 +28,7 @@ image: # -- Image to use for deploying. repository: janssenproject/scim # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. 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..e44ff4e3404 100644 --- a/charts/janssen/values.yaml +++ b/charts/janssen/values.yaml @@ -47,7 +47,7 @@ auth-server: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/auth-server # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -126,7 +126,7 @@ auth-server-key-rotation: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/certmanager # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Auth server key rotation keys life in hours @@ -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 @@ -326,7 +326,7 @@ config: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/configurator # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Organization name. Used for certificate creation. @@ -420,7 +420,7 @@ config-api: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/config-api # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -525,7 +525,7 @@ fido2: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/fido2 # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -635,7 +635,7 @@ casa: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/casa # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -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: @@ -1250,7 +1254,7 @@ persistence: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/persistence-loader # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Resource specs. @@ -1336,7 +1340,7 @@ scim: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/scim # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -1446,7 +1450,7 @@ link: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/link # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -1555,7 +1559,7 @@ saml: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/saml # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Service replica number. @@ -1636,7 +1640,7 @@ kc-scheduler: # -- Image to use for deploying. repository: ghcr.io/janssenproject/jans/kc-scheduler # -- Image tag to use for deploying. - tag: 0.0.0-nightly + tag: 1.3.0-1 # -- Image Pull Secrets pullSecrets: [ ] # -- Resource specs. 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..c39a99fa6d4 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 @@ -61,7 +61,7 @@ RUN mkdir -p /root/.bzt/jmeter-taurus \ LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/loadtesting-jmeter" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Jmeter tests" \ org.opencontainers.image.description="Janssen Jmeter and user loader image" diff --git a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_auth_code.yaml b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_auth_code.yaml index 9eec0d7837b..a6042708ed8 100644 --- a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_auth_code.yaml +++ b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_auth_code.yaml @@ -36,7 +36,7 @@ spec: - envFrom: - configMapRef: name: load-test-authz-cm - image: ghcr.io/janssenproject/jans/loadtesting-jmeter:0.0.0-nightly + image: ghcr.io/janssenproject/jans/loadtesting-jmeter:1.3.0-1 imagePullPolicy: Always name: load-test-authz resources: diff --git a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_dcr.yaml b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_dcr.yaml index d1c331290e8..6c61c91f750 100644 --- a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_dcr.yaml +++ b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_dcr.yaml @@ -34,7 +34,7 @@ spec: - envFrom: - configMapRef: name: load-test-dcr-cm - image: ghcr.io/janssenproject/jans/loadtesting-jmeter:0.0.0-nightly + image: ghcr.io/janssenproject/jans/loadtesting-jmeter:1.3.0-1 imagePullPolicy: Always name: load-test-dcr resources: diff --git a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_ropc.yaml b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_ropc.yaml index 56a6504bf2d..7f9f4fc813e 100644 --- a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_ropc.yaml +++ b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-test/load_test_ropc.yaml @@ -36,7 +36,7 @@ spec: - envFrom: - configMapRef: name: load-testing-ropc-cm - image: ghcr.io/janssenproject/jans/loadtesting-jmeter:0.0.0-nightly + image: ghcr.io/janssenproject/jans/loadtesting-jmeter:1.3.0-1 imagePullPolicy: Always name: load-testing-ropc resources: diff --git a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-users/load_users_rdbms_job.yaml b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-users/load_users_rdbms_job.yaml index 02525c0b4de..6add0d4908f 100644 --- a/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-users/load_users_rdbms_job.yaml +++ b/demos/benchmarking/docker-jans-loadtesting-jmeter/yaml/load-users/load_users_rdbms_job.yaml @@ -34,7 +34,7 @@ spec: - envFrom: - configMapRef: name: load-users-mysql-cm - image: ghcr.io/janssenproject/jans/loadtesting-jmeter:0.0.0-nightly + image: ghcr.io/janssenproject/jans/loadtesting-jmeter:1.3.0-1 name: load-users-mysql resources: limits: diff --git a/demos/jans-tarp/.gitignore b/demos/jans-tarp/.gitignore index 22b9f913967..5683ef3b5e6 100644 --- a/demos/jans-tarp/.gitignore +++ b/demos/jans-tarp/.gitignore @@ -1,4 +1,5 @@ node_modules dist release -package-lock.json \ No newline at end of file +package-lock.json +src/wasm \ No newline at end of file diff --git a/demos/jans-tarp/README.md b/demos/jans-tarp/README.md index d4a06130d05..bbdcec4239a 100644 --- a/demos/jans-tarp/README.md +++ b/demos/jans-tarp/README.md @@ -2,9 +2,10 @@ ## Relying Party tool in form of a Browser Extension. -[Demo Video](https://www.loom.com/share/6bfe8c5556a94abea05467e3deead8a2?sid=b65c81d9-c1a1-475c-b89b-c105887d31ad) +[Demo Video](https://www.loom.com/share/b112b9c7214a4920812a2ebe9c36dbf5?sid=7a15d2e5-881e-4002-9b8c-902dd1d80cec) -This extension is for convenient testing of authentication flows on browser. +- This extension is for convenient testing of authentication flows on browser. +- [Cedarling](https://docs.jans.io/head/cedarling/cedarling-overview/) is an embeddable stateful Policy Decision Point, or "PDP". Cedarling is integrated with Jans Tarp to make authorization decision post-authentication. ## Supporting Browser @@ -18,9 +19,10 @@ This extension is for convenient testing of authentication flows on browser. ## Build 1. Change directory to the project directory (`/jans-tarp`). -2. Run `npm install`. -3. Run `npm run build`. It will create Chrome and Firefox build in `/jans-tarp/dist/chrome` and `/jans-tarp/dist/firefox` directories respectively. -4. To pack the build into a zip file run `npm run pack`. This command will pack Chrome and Firefox builds in zip files at `/jans-tarp/release`. +2. Download and extract Cedarling WASM bindings from https://github.com/JanssenProject/jans/releases/download/nightly/cedarling_wasm_{version}_pkg.tar.gz to `/jans-tarp/src/wasm`. +3. Run `npm install`. +4. Run `npm run build`. It will create Chrome and Firefox build in `/jans-tarp/dist/chrome` and `/jans-tarp/dist/firefox` directories respectively. +5. To pack the build into a zip file run `npm run pack`. This command will pack Chrome and Firefox builds in zip files at `/jans-tarp/release`. ## Releases @@ -53,6 +55,28 @@ When you are testing Janssen IdP with self-signed cert then follow below steps b ![self-signed cert risk](./docs/images/untrusted_cert_risk.png) +## Testing using Jans Tarp + +* Setup Jans-Tarp. [Instructions](https://github.com/JanssenProject/jans/tree/main/demos/jans-tarp) +* Configuration to run Agama flow +* Add Your Jans Auth server host , Client expiry date, Scopes and click on `Register` client. It will register new OP Client on your Auth server. + +![image](./docs/images/tarp-client-reg.png) + +* Add `Acr values` and `Scopes ` click on Trigger Auth Code flow. + +![image](./docs/images/authentication-flow-input.png) + +* It will trigger agama flow and show login screen to you. You can use TUI to add testing user. + +![image](./docs/images/agamapw-login.png) + +* After successful auth, it will show you below screen + +![image](./docs/images/successful-tarp-auth-screen.png) + + + ## Testing with Keycloak (installed on localhost) 1. Login to KC admin console diff --git a/demos/jans-tarp/docs/images/agamapw-login.png b/demos/jans-tarp/docs/images/agamapw-login.png new file mode 100644 index 00000000000..32134b26c28 Binary files /dev/null and b/demos/jans-tarp/docs/images/agamapw-login.png differ diff --git a/demos/jans-tarp/docs/images/authentication-flow-input.png b/demos/jans-tarp/docs/images/authentication-flow-input.png new file mode 100644 index 00000000000..aea238c2aed Binary files /dev/null and b/demos/jans-tarp/docs/images/authentication-flow-input.png differ diff --git a/demos/jans-tarp/docs/images/successful-tarp-auth-screen.png b/demos/jans-tarp/docs/images/successful-tarp-auth-screen.png new file mode 100644 index 00000000000..8ddacb3d196 Binary files /dev/null and b/demos/jans-tarp/docs/images/successful-tarp-auth-screen.png differ diff --git a/demos/jans-tarp/docs/images/tarp-client-reg.png b/demos/jans-tarp/docs/images/tarp-client-reg.png new file mode 100644 index 00000000000..9729b53042f Binary files /dev/null and b/demos/jans-tarp/docs/images/tarp-client-reg.png differ diff --git a/demos/jans-tarp/package.json b/demos/jans-tarp/package.json index 392bcd3777a..39a28de664b 100644 --- a/demos/jans-tarp/package.json +++ b/demos/jans-tarp/package.json @@ -1,12 +1,13 @@ { "name": "jans-tarp", - "version": "0.0.0-nightly", + "version": "1.3.0", "description": "Relying Party tool in form of a Chrome Extension.", "main": "index.js", "license": "Apache", "scripts": { "watch": "webpack --watch --progress --config webpack.dev.js", "build": "webpack --mode=production --progress --config webpack.prod.js", + "build-dev": "webpack --mode=development --progress --config webpack.dev.js", "pack": "node pack.js" }, "devDependencies": { @@ -38,6 +39,7 @@ "autoprefixer": "^10.4.7", "axios": "^1.4.0", "dayjs": "^1.11.10", + "json-edit-react": "^1.19.2", "jwt-decode": "^4.0.0", "moment": "^2.29.4", "postcss": "^8.4.14", @@ -47,6 +49,7 @@ "react-select": "^5.7.3", "react-spinner-overlay": "^0.1.33", "styled-components": "^6.1.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "wasm": "file:wasm" } } diff --git a/demos/jans-tarp/src/options/addCedarlingConfig.tsx b/demos/jans-tarp/src/options/addCedarlingConfig.tsx new file mode 100644 index 00000000000..bf63ecfb533 --- /dev/null +++ b/demos/jans-tarp/src/options/addCedarlingConfig.tsx @@ -0,0 +1,200 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import CircularProgress from "@mui/material/CircularProgress"; +import Stack from '@mui/material/Stack'; +import Alert from '@mui/material/Alert'; +import __wbg_init, { init, Cedarling } from "wasm"; +import { v4 as uuidv4 } from 'uuid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { JsonEditor } from 'json-edit-react'; +import axios from 'axios'; + +export default function AddCedarlingConfig({ isOpen, handleDialog, newData }) { + const [open, setOpen] = React.useState(isOpen); + const [bootstrap, setBootstrap] = React.useState(newData); + const [errorMessage, setErrorMessage] = React.useState("") + const [loading, setLoading] = React.useState(false); + const [inputSelection, setInputSelection] = React.useState("json"); + + const ADD_BOOTSTRAP_ERROR = 'Error in adding bootstrap. Check web console for logs.' + + React.useEffect(() => { + if (isOpen) { + handleOpen(); + } else { + handleClose(); + } + }, [isOpen]); + + React.useEffect(() => { + setBootstrap(newData) + }, [newData]); + + const handleClose = () => { + setInputSelection('json') + handleDialog(false) + setOpen(false); + }; + + const handleOpen = () => { + setErrorMessage(''); + setLoading(false); + handleDialog(true) + setOpen(true); + }; + + const validateBootstrap = async (e) => { + let bootstrap = e.target.value; + setErrorMessage(''); + if (inputSelection === 'url') { + let bootstrapUrl = e.target.value; + if (bootstrapUrl === '') { + setErrorMessage('URL is required.'); + return false; + } + const oidcConfigOptions = { + method: 'GET', + url: bootstrapUrl, + }; + const response = await axios(oidcConfigOptions); + bootstrap = response.data; + + } else if (inputSelection === 'json') { + bootstrap = e.target.value; + } + if (isEmpty(bootstrap) || Object.keys(bootstrap).length === 0) { + setErrorMessage('Empty authorization request not allowed.'); + return false; + } + isJsonValid(bootstrap); + }; + + const isJsonValid = async (bootstrap) => { + setErrorMessage(''); + try { + setBootstrap(JSON.parse(JSON.stringify(bootstrap))); + return true; + } catch (err) { + console.error(err) + setErrorMessage(`Invalid input: ${err}`); + return false; + } + }; + + const saveBootstrap = async () => { + try { + setLoading(true); + if (!isJsonValid(bootstrap)) { + return; + } + + await __wbg_init(); + let instance: Cedarling = await init(bootstrap); + + chrome.storage.local.get(["cedarlingConfig"], (result) => { + let bootstrapArr = [] + + let idObj = { id: uuidv4() }; + + bootstrapArr.push({ ...bootstrap, ...idObj }); + chrome.storage.local.set({ cedarlingConfig: bootstrapArr }); + handleClose(); + }); + } catch (err) { + console.error(err) + setErrorMessage(ADD_BOOTSTRAP_ERROR + err) + } + setLoading(false); + } + + const isEmpty = (value) => { + return (value == null || value.length === 0); + } + return ( + + { + event.preventDefault(); + }, + }} + className="form-container" + > + Add Cedarling Configuration + {loading ? ( +
+ +
+ ) : ( + "" + )} + + + Submit below details. + + + {(!!errorMessage || errorMessage !== '') ? + {errorMessage} : '' + } + + { setErrorMessage(''); setInputSelection("json"); }} color="success" />} label="JSON" /> + { setErrorMessage(''); setInputSelection("url") }} />} label="URL" /> + + {inputSelection === 'json' ? + + : ''} + {inputSelection === 'url' ? + { + validateBootstrap(e); + }} + /> : ''} + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/demos/jans-tarp/src/options/authFlowInputs.tsx b/demos/jans-tarp/src/options/authFlowInputs.tsx index 20c88d10840..72f80ce5f81 100644 --- a/demos/jans-tarp/src/options/authFlowInputs.tsx +++ b/demos/jans-tarp/src/options/authFlowInputs.tsx @@ -448,6 +448,7 @@ export default function AuthFlowInputs({ isOpen, handleDialog, client, notifyOnD )} /> + setDisplayToken(!displayToken)}/>} label="Display Access Token and ID Token after authentication" /> diff --git a/demos/jans-tarp/src/options/cedarling.tsx b/demos/jans-tarp/src/options/cedarling.tsx new file mode 100644 index 00000000000..c4d5d1991f8 --- /dev/null +++ b/demos/jans-tarp/src/options/cedarling.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Edit from '@mui/icons-material/Edit'; +import { pink, green } from '@mui/material/colors'; +import Grid from '@mui/material/Grid'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; +import AddIcon from '@mui/icons-material/Add'; +import Container from '@mui/material/Container'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import AddCedarlingConfig from './addCedarlingConfig' +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import DeleteForeverOutlinedIcon from '@mui/icons-material/DeleteForeverOutlined'; +import HelpDrawer from './helpDrawer' +import Alert from '@mui/material/Alert'; +import { JsonEditor } from 'json-edit-react' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + }, +})); + +function Row(props: { row: any, notifyOnDataChange }) { + const { row, notifyOnDataChange } = props; + const [open, setOpen] = React.useState(false); + + const handleDialog = (isOpen) => { + setOpen(isOpen); + notifyOnDataChange(); + }; + + async function resetBootstrap() { + chrome.storage.local.get(["cedarlingConfig"], (result) => { + let cedarlingConfigArr = [] + chrome.storage.local.set({ cedarlingConfig: cedarlingConfigArr }); + }); + notifyOnDataChange(); + } + + return ( + + + *': { borderBottom: 'unset' } }}> + + + + + + + + + + + + + + + + { + setOpen(true); + notifyOnDataChange(); + }} /> + + + + + + + ); +} + +export default function Cedarling({ data, notifyOnDataChange, isOidcClientRegistered }) { + const [modelOpen, setModelOpen] = React.useState(false); + const [drawerOpen, setDrawerOpen] = React.useState(false); + const [oidcClientRegistered, setOidcClientRegistered] = React.useState(false); + + + React.useEffect(() => { + setOidcClientRegistered(isOidcClientRegistered) + }, [isOidcClientRegistered]); + + const handleDialog = (isOpen) => { + setModelOpen(isOpen); + notifyOnDataChange(); + }; + + const handleDrawer = (isOpen) => { + setDrawerOpen(isOpen); + }; + + return ( + + {oidcClientRegistered ? + <> + + + + + {(data === undefined || data?.length == 0) ? + : ''} + + + + + + + Bootstrap Configuration + Action + + + + {(data === undefined || data?.length == 0) ? + No Records to show. : + data.map((row, index) => ()) + } + +
+
+
+ : + At least one OIDC client must be registered in Jans-TARP to add Cedarling configuration. + } +
+ ); +} \ No newline at end of file diff --git a/demos/jans-tarp/src/options/homePage.tsx b/demos/jans-tarp/src/options/homePage.tsx new file mode 100644 index 00000000000..47fb56a99ea --- /dev/null +++ b/demos/jans-tarp/src/options/homePage.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import Container from '@mui/material/Container'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Box from '@mui/material/Box'; +import Password from '@mui/icons-material/Password'; +import LockPerson from '@mui/icons-material/LockPerson'; +import OIDCClients from './oidcClients'; +import Cedarling from './cedarling'; +import Grid from '@mui/material/Grid'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + +export default function HomePage({ data, notifyOnDataChange }) { + + const [value, setValue] = React.useState(0); + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + + return ( + + + + } /> + } /> + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/demos/jans-tarp/src/options/oidcClients.tsx b/demos/jans-tarp/src/options/oidcClients.tsx index d18ea9a298c..af33fea6a05 100644 --- a/demos/jans-tarp/src/options/oidcClients.tsx +++ b/demos/jans-tarp/src/options/oidcClients.tsx @@ -84,7 +84,6 @@ function Row(props: { row: ReturnType, notifyOnDataChange }) let clientArr = [] if (!!result.oidcClients) { clientArr = result.oidcClients; - chrome.storage.local.set({ oidcClients: clientArr.filter(obj => obj.clientId !== row.clientId) }); } }); @@ -93,7 +92,7 @@ function Row(props: { row: ReturnType, notifyOnDataChange }) return ( - + *': { borderBottom: 'unset' } }}> diff --git a/demos/jans-tarp/src/options/options.tsx b/demos/jans-tarp/src/options/options.tsx index cc76c137fc2..ff0e2288658 100644 --- a/demos/jans-tarp/src/options/options.tsx +++ b/demos/jans-tarp/src/options/options.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react' import './options.css' import Header from './header' -import OIDCClients from './oidcClients' +import HomePage from './homePage' import UserDetails from './userDetails' +import { ILooseObject } from './ILooseObject' const Options = () => { @@ -15,17 +16,26 @@ const Options = () => { if (!isEmpty(oidcClientResults) && Object.keys(oidcClientResults).length !== 0) { - chrome.storage.local.get(["loginDetails"], (loginDetailsResult) => { + chrome.storage.local.get(["loginDetails"], async (loginDetailsResult) => { if (!isEmpty(loginDetailsResult) && Object.keys(loginDetailsResult).length !== 0) { setOptionType('loginPage'); setdata(loginDetailsResult); } else { - setOptionType('oidcClientPage'); - setdata(oidcClientResults); + let collectedData = {}; + setOptionType('homePage'); + collectedData = { ...data, ...oidcClientResults }; + + let cedarlingConfig: ILooseObject = await new Promise((resolve, reject) => { chrome.storage.local.get(["cedarlingConfig"], (result) => { resolve(result); }) }); + + if (!isEmpty(cedarlingConfig) && Object.keys(cedarlingConfig).length !== 0) { + collectedData = { ...collectedData, ...cedarlingConfig }; + } + + setdata(collectedData); } }); } else { - setOptionType('oidcClientPage'); + setOptionType('homePage'); setdata({}); } setDataChanged(false); @@ -42,9 +52,9 @@ const Options = () => { function renderPage({ optionType, data }) { switch (optionType) { - case 'oidcClientPage': - return case 'loginPage': diff --git a/demos/jans-tarp/src/options/registerClient.tsx b/demos/jans-tarp/src/options/registerClient.tsx index 79a4770e51b..d908e821a2b 100644 --- a/demos/jans-tarp/src/options/registerClient.tsx +++ b/demos/jans-tarp/src/options/registerClient.tsx @@ -54,7 +54,7 @@ export default function RegisterClient({ isOpen, handleDialog }) { }; const validateIssuer = async (e) => { - + setIssuerError(''); let issuer = e.target.value; if (issuer.length === 0) { @@ -221,7 +221,7 @@ export default function RegisterClient({ isOpen, handleDialog }) { event.preventDefault(); }, }} - className="form-container" + className="form-container" > Register OIDC Client {loading ? ( diff --git a/demos/jans-tarp/src/options/userDetails.tsx b/demos/jans-tarp/src/options/userDetails.tsx index 3aea22ce430..fdf8c158a0e 100644 --- a/demos/jans-tarp/src/options/userDetails.tsx +++ b/demos/jans-tarp/src/options/userDetails.tsx @@ -3,12 +3,114 @@ import { v4 as uuidv4 } from 'uuid'; import './options.css' import './alerts.css'; import { WindmillSpinner } from 'react-spinner-overlay' +import { JsonEditor } from 'json-edit-react' +import TextField from '@mui/material/TextField'; +import InputLabel from '@mui/material/InputLabel'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Button from '@mui/material/Button'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import Typography from '@mui/material/Typography'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -const UserDetails = ({data, notifyOnDataChange}) => { +import __wbg_init, { init, Cedarling, AuthorizeResult } from 'wasm'; + +const UserDetails = ({ data, notifyOnDataChange }) => { const [loading, setLoading] = useState(false); const [showMoreIdToken, setShowMoreIdToken] = useState(false); const [showMoreAT, setShowMoreAT] = useState(false); const [showMoreUI, setShowMoreUI] = useState(false); + const [context, setContext] = React.useState({}); + const [action, setAction] = React.useState(""); + const [accessToken, setAccessToken] = React.useState(false); + const [userInfoToken, setUserInfoToken] = React.useState(false); + const [idToken, setIdToken] = React.useState(false); + const [resource, setResource] = React.useState({}); + const [cedarlingBootstrapPresent, setCedarlingBootstrapPresent] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState("") + const [authzResult, setAuthzResult] = React.useState("") + const [authzLogs, setAuthzLogs] = React.useState("") + + + React.useEffect(() => { + chrome.storage.local.get(["authzRequest"], (authzRequest) => { + if (!isEmpty(authzRequest) && Object.keys(authzRequest).length !== 0) { + setContext(authzRequest.authzRequest.context); + setAction(authzRequest.authzRequest.action); + setResource(authzRequest.authzRequest.resource); + } + }); + chrome.storage.local.get(["cedarlingConfig"], async (cedarlingConfig) => { + setCedarlingBootstrapPresent(false); + if (Object.keys(cedarlingConfig).length !== 0 && !isEmpty(cedarlingConfig?.cedarlingConfig)) { + setCedarlingBootstrapPresent(true); + } + }); + }, []) + + const triggerCedarlingAuthzRequest = async () => { + setAuthzResult(""); + setAuthzLogs(""); + let reqObj = await createCedarlingAuthzRequestObj(); + chrome.storage.local.get(["cedarlingConfig"], async (cedarlingConfig) => { + let instance: Cedarling; + try { + if (Object.keys(cedarlingConfig).length !== 0) { + await __wbg_init(); + instance = await init(!isEmpty(cedarlingConfig?.cedarlingConfig) ? cedarlingConfig?.cedarlingConfig[0] : undefined); + let result: AuthorizeResult = await instance.authorize(reqObj); + let logs = await instance.pop_logs(); + setAuthzResult(result.json_string()) + console.log("result:", result); + logs.push(await instance.pop_logs()); + if (logs.length != 0) { + let pretty_logs = logs.map(log => JSON.stringify(log, null, 2)); + setAuthzLogs(pretty_logs.toString()); + } + + } + } catch (err) { + setAuthzResult(err); + console.log("err:", err); + let logs = await instance.pop_logs(); + if (logs.length != 0) { + let pretty_logs = logs.map(log => JSON.stringify(log, null, 2)); + setAuthzLogs(pretty_logs.toString()); + } + } + + }); + + } + + const createCedarlingAuthzRequestObj = async () => { + let reqObj = { tokens: { access_token: '', id_token: '', userinfo_token: '' }, action: "", resource: {}, context: {} }; + if (accessToken) { + reqObj.tokens.access_token = (!!data ? data?.access_token : ''); + } + + if (idToken) { + reqObj.tokens.id_token = (!!data ? data?.id_token : ''); + } + + if (userInfoToken) { + reqObj.tokens.userinfo_token = (!!data ? data?.userDetails : ''); + } + + //reqObj.tokens.access_token = 'eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiY29kZSI6IjNlMmEyMDEyLTA5OWMtNDY0Zi04OTBiLTQ0ODE2MGMyYWIyNSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhdWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhY3IiOiJzaW1wbGVfcGFzc3dvcmRfYXV0aCIsIng1dCNTMjU2IjoiIiwibmJmIjoxNzMxOTUzMDMwLCJzY29wZSI6WyJyb2xlIiwib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl0sImF1dGhfdGltZSI6MTczMTk1MzAyNywiZXhwIjoxNzMyMTIxNDYwLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6InVaVWgxaERVUW82UEZrQlBud3BHemciLCJ1c2VybmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjMwNiwidXJpIjoiaHR0cHM6Ly9qYW5zLnRlc3QvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.Pt-Y7F-hfde_WP7ZYwyvvSS11rKYQWGZXTzjH_aJKC5VPxzOjAXqI3Igr6gJLsP1aOd9WJvOPchflZYArctopXMWClbX_TxpmADqyCMsz78r4P450TaMKj-WKEa9cL5KtgnFa0fmhZ1ZWolkDTQ_M00Xr4EIvv4zf-92Wu5fOrdjmsIGFot0jt-12WxQlJFfs5qVZ9P-cDjxvQSrO1wbyKfHQ_txkl1GDATXsw5SIpC5wct92vjAVm5CJNuv_PE8dHAY-KfPTxOuDYBuWI5uA2Yjd1WUFyicbJgcmYzUSVt03xZ0kQX9dxKExwU2YnpDorfwebaAPO7G114Bkw208g'; + //reqObj.tokens.id_token = 'eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiYnhhQ1QwWlFYYnY0c2J6alNEck5pQSIsInN1YiI6InF6eG4xU2NyYjlsV3RHeFZlZE1Da3ktUWxfSUxzcFphUUE2Znl1WWt0dzAiLCJhbXIiOltdLCJpc3MiOiJodHRwczovL2FjY291bnQuZ2x1dS5vcmciLCJub25jZSI6IjI1YjJiMTZiLTMyYTItNDJkNi04YThlLWU1ZmE5YWI4ODhjMCIsInNpZCI6IjZkNDQzNzM0LWI3YTItNGVkOC05ZDNhLTE2MDZkMmY5OTI0NCIsImphbnNPcGVuSURDb25uZWN0VmVyc2lvbiI6Im9wZW5pZGNvbm5lY3QtMS4wIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYWNyIjoic2ltcGxlX3Bhc3N3b3JkX2F1dGgiLCJjX2hhc2giOiJWOGg0c085Tnp1TEthd1BPLTNETkxBIiwibmJmIjoxNzMxOTUzMDMwLCJhdXRoX3RpbWUiOjE3MzE5NTMwMjcsImV4cCI6MTczMTk1NjYzMCwiZ3JhbnQiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6ImlqTFpPMW9vUnlXcmdJbjdjSWROeUEiLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4IjozMDcsInVyaSI6Imh0dHBzOi8vamFucy50ZXN0L2phbnMtYXV0aC9yZXN0djEvc3RhdHVzX2xpc3QifX19.Nw7MRaJ5LtDak_LdEjrICgVOxDwd1p1I8WxD7IYw0_mKlIJ-J_78rGPski9p3L5ZNCpXiHtVbnhc4lJdmbh-y6mrD3_EY_AmjK50xpuf6YuUuNVtFENCSkj_irPLkIDG65HeZherWsvH0hUn4FVGv8Sw9fjny9Doi-HGHnKg9Qvphqre1U8hCphCVLQlzXAXmBkbPOC8tDwId5yigBKXP50cdqDcT-bjXf9leIdGgq0jxb57kYaFSElprLN9nUygM4RNCn9mtmo1l4IsdTlvvUb3OMAMQkRLfMkiKBjjeSF3819mYRLb3AUBaFH16ZdHFBzTSB6oA22TYpUqOLihMg'; + //reqObj.tokens.userinfo_token = 'eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiQ2FzYUFkbWluIl0sImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsImdpdmVuX25hbWUiOiJBZG1pbiIsIm1pZGRsZV9uYW1lIjoiQWRtaW4iLCJpbnVtIjoiYTZhNzAzMDEtYWY0OS00OTAxLTk2ODctMGJjZGNmNGUzNGZhIiwiY2xpZW50X2lkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwidXBkYXRlZF9hdCI6MTczMTY5ODEzNSwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsIm5pY2tuYW1lIjoiQWRtaW4iLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJqdGkiOiJPSW4zZzFTUFNEU0tBWUR6RU5Wb3VnIiwiZW1haWwiOiJhZG1pbkBqYW5zLnRlc3QiLCJqYW5zQWRtaW5VSVJvbGUiOlsiYXBpLWFkbWluIl19.CIahQtRpoTkIQx8KttLPIKH7gvGG8OmYCMzz7wch6k792DVYQG1R7q3sS9Ema1rO5Fm_GgjOsR0yTTMKsyhHDLBwkDd3cnMLgsh2AwVFZvxtpafTlUAPfjvMAy9YTtkPcY6rNUhsYLSSOA83kt6pHdIv5nI-G6ybqgg-bLBRpwZDoOV0TulRhmuukdiuugTXHT6Bb-K3ZeYs8CwewztnxoFTSDghSzq7VZIraV8SLTBLx5_xswn9mefamyB2XNN3o6vXuMyf4BEbYSCuJ3pu6YtNgfyWwt9cF8PYe4PVLoXZuJKN-cy4qrtgy43QXPCg96jSQUJqgLb5ZL5_3udm2Q'; + + reqObj.action = action; + reqObj.context = context; + reqObj.resource = resource; + + chrome.storage.local.set({ authzRequest: reqObj }); + return reqObj; + } + async function logout() { setLoading(true); try { @@ -54,6 +156,10 @@ const UserDetails = ({data, notifyOnDataChange}) => { notifyOnDataChange("true"); } + function isEmpty(value) { + return (value == null || value.length === 0); + } + return (
@@ -64,25 +170,155 @@ const UserDetails = ({data, notifyOnDataChange}) => {
{data?.displayToken ? <> + + } + aria-controls="panel1-content" + id="panel1-header" + > + Access Token + + + +
+

{showMoreAT ? (!!data ? data?.access_token : '') : (!!data ? data?.access_token.substring(0, 250).concat(' ...') : '')}

+ setShowMoreAT(!showMoreAT)}>{showMoreAT ? "Show less" : "Show more"} +
+
+
+ + } + aria-controls="panel1-content" + id="panel1-header" + > + Id Token + + + +
+

{showMoreIdToken ? (!!data ? data?.id_token : '') : (!!data ? data?.id_token.substring(0, 250).concat(' ...') : '')}

+ setShowMoreIdToken(!showMoreIdToken)}>{showMoreIdToken ? "Show less" : "Show more"} +
+
+
+ + : ''} + + } + aria-controls="panel1-content" + id="panel1-header" + > + User Details + +
- Access Token -

{showMoreAT ? (!!data ? data?.access_token : '') : (!!data ? data?.access_token.substring(0, 250).concat(' ...') : '')}

- setShowMoreAT(!showMoreAT)}>{showMoreAT ? "Show less" : "Show more"} + User Details +

{showMoreUI ? (!!data ? data?.userDetails : '') : (!!data ? data?.userDetails.substring(0, 250).concat(' ...') : '')}

+ setShowMoreUI(!showMoreUI)}>{showMoreUI ? "Show less" : "Show more"}
-
- Id Token -

{showMoreIdToken ? (!!data ? data?.id_token : '') : (!!data ? data?.id_token.substring(0, 250).concat(' ...') : '')}

- setShowMoreIdToken(!showMoreIdToken)}>{showMoreIdToken ? "Show less" : "Show more"} + + + {cedarlingBootstrapPresent ? + + } + aria-controls="panel1-content" + id="panel1-header" + > + Cedarling Authz Request Form + + +
+ Principal + setAccessToken(!accessToken)} />} label="Access Token" /> + setUserInfoToken(!userInfoToken)} />} label="Userinfo Token" /> + setIdToken(!idToken)} />} label="Id Token" /> + + { + setAction(e.target.value); + }} + /> + Resource + + Context + +
+
- - : ''} -
- User Details -

{showMoreUI ? (!!data ? data?.userDetails : '') : (!!data ? data?.userDetails.substring(0, 250).concat(' ...') : '')}

- setShowMoreUI(!showMoreUI)}>{showMoreUI ? "Show less" : "Show more"} -
+
+
: ''} + {!!authzResult ? + + } + aria-controls="panel1-content" + id="panel1-header" + > + Cedarling Authz Result + + + + + + : ''} + {!!authzLogs ? + + } + aria-controls="panel2-content" + id="panel2-header" + > + Cedarling Authz Logs + + + + + + : ''} +
- +
) }; diff --git a/demos/jans-tarp/src/static/chrome/manifest.json b/demos/jans-tarp/src/static/chrome/manifest.json index 2cb920ca3c3..99ebfbff5be 100644 --- a/demos/jans-tarp/src/static/chrome/manifest.json +++ b/demos/jans-tarp/src/static/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "jans-tarp", - "version": "0.0.0-nightly", + "version": "1.3.0", "version_name": "nightly", "description": "Relying Party tool in form of a Chrome Extension. Please note that the manifest version field should be one to four dot-separated integers identifying the version of this extension. The descriptive version string can be stated in the `version_name` field. For more details see https://developer.chrome.com/docs/extensions/reference/manifest/version.", "icons": { @@ -24,5 +24,8 @@ "*://*/*" ], "options_page": "options.html", - "incognito": "split" + "incognito": "split", + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" + } } \ No newline at end of file diff --git a/demos/jans-tarp/src/static/firefox/manifest.json b/demos/jans-tarp/src/static/firefox/manifest.json index e30988c67a1..5361a2aa467 100644 --- a/demos/jans-tarp/src/static/firefox/manifest.json +++ b/demos/jans-tarp/src/static/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "jans-tarp", - "version": "0.0.0-nightly", + "version": "1.3.0", "description": "Relying Party tool in form of a Firefox Extension.", "icons": { "16": "icon.png", @@ -27,5 +27,8 @@ "gecko": { "id": "jans-tarp@gluu.org" } + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" } } \ No newline at end of file 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..3f7383b4397 100644 --- a/docker-jans-all-in-one/Dockerfile +++ b/docker-jans-all-in-one/Dockerfile @@ -3,7 +3,7 @@ # ============== # original Janssen base version -ARG BASE_VERSION=0.0.0-nightly +ARG BASE_VERSION=1.3.0-1 # the following ARGs set default base images # they can be overriden in build process via --build-arg option @@ -58,7 +58,7 @@ RUN apk update \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 # 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 @@ -178,7 +178,7 @@ RUN mkdir -p /opt/jans/configurator/db \ COPY app /app # CN version as env var (with suffix if any, i.e. SNAPSHOT) -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 # set directory contains installer code that will be added to Python sys.path ENV PYTHONPATH=/app @@ -235,7 +235,7 @@ ENV JETTY_BASE=/opt/jans/jetty \ LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/all-in-one" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen All-in-One" \ org.opencontainers.image.description="" 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..b95f8b559b6 100644 --- a/docker-jans-auth-server/Dockerfile +++ b/docker-jans-auth-server/Dockerfile @@ -50,8 +50,8 @@ RUN /opt/jython/bin/pip uninstall -y pip setuptools # Auth server # =========== -ENV CN_VERSION=0.0.0-nightly -ENV CN_BUILD_DATE='2024-12-20 08:35' +ENV CN_VERSION=1.3.0 +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=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -245,7 +245,7 @@ EXPOSE $CN_AUTH_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/auth-server" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Authorization Server" \ org.opencontainers.image.description="OAuth 2.0 server and client; OpenID Connect Provider (OP) & UMA Authorization Server (AS)" @@ -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..199601cd6e2 100644 --- a/docker-jans-casa/Dockerfile +++ b/docker-jans-casa/Dockerfile @@ -29,7 +29,7 @@ RUN wget -q https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${JETTY_ # Casa # ==== -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 09:16' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/casa/${CN_VERSION}/casa-${CN_VERSION}.war @@ -60,7 +60,7 @@ RUN mkdir -p /usr/share/java \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -206,7 +206,7 @@ EXPOSE $CN_CASA_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/casa" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Casa" \ org.opencontainers.image.description="Self-service portal for people to manage their account security preferences in the Janssen, like 2FA" diff --git a/docker-jans-certmanager/Dockerfile b/docker-jans-certmanager/Dockerfile index d14f8d2f8b8..fefb30cda55 100644 --- a/docker-jans-certmanager/Dockerfile +++ b/docker-jans-certmanager/Dockerfile @@ -14,7 +14,7 @@ RUN apk update \ # =========== # JAR files required to generate OpenID Connect keys -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 08:29' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-client/${CN_VERSION}/jans-auth-client-${CN_VERSION}-jar-with-dependencies.jar @@ -25,7 +25,7 @@ RUN wget -q ${CN_SOURCE_URL} -P /app/javalibs/ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 # 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 @@ -130,7 +130,7 @@ ENV CN_WAIT_MAX_TIME=300 \ LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/certmanager" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Certmanager" \ org.opencontainers.image.description="Manage certs and crypto keys for Janssen Server" diff --git a/docker-jans-certmanager/README.md b/docker-jans-certmanager/README.md index adb12653d6a..f835942ca00 100644 --- a/docker-jans-certmanager/README.md +++ b/docker-jans-certmanager/README.md @@ -186,7 +186,7 @@ spec: spec: containers: - name: auth-key-rotation - image: ghcr.io/janssenproject/jans/certmanager:0.0.0-nightly + image: ghcr.io/janssenproject/jans/certmanager:1.3.0-1 resources: requests: memory: "300Mi" diff --git a/docker-jans-config-api/Dockerfile b/docker-jans-config-api/Dockerfile index ece0aef0c42..b3c075f2ba8 100644 --- a/docker-jans-config-api/Dockerfile +++ b/docker-jans-config-api/Dockerfile @@ -40,7 +40,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # Config API # ========== -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 09:07' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-config-api-server/${CN_VERSION}/jans-config-api-server-${CN_VERSION}.war @@ -70,7 +70,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-config-api/_plugins \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources @@ -223,7 +223,7 @@ EXPOSE $CN_CONFIG_API_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/config-api" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Config API" \ org.opencontainers.image.description="" diff --git a/docker-jans-configurator/Dockerfile b/docker-jans-configurator/Dockerfile index 3b9acd83810..e606a4fcaea 100644 --- a/docker-jans-configurator/Dockerfile +++ b/docker-jans-configurator/Dockerfile @@ -15,7 +15,7 @@ RUN apk update \ # JAR files required to generate OpenID Connect keys -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 08:29' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-client/${CN_VERSION}/jans-auth-client-${CN_VERSION}-jar-with-dependencies.jar @@ -27,7 +27,7 @@ RUN mkdir -p /opt/jans/configurator/javalibs \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG GIT_CLONE_DEPTH=100 RUN git clone --depth ${GIT_CLONE_DEPTH} --filter blob:none --no-checkout https://github.com/janssenproject/jans /tmp/jans \ @@ -125,7 +125,7 @@ ENV CN_WAIT_MAX_TIME=300 \ LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/configurator" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Configuration Manager" \ org.opencontainers.image.description="Manage config and secret" 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..62c4e839e01 100644 --- a/docker-jans-fido2/Dockerfile +++ b/docker-jans-fido2/Dockerfile @@ -41,7 +41,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ===== -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 11:02' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-fido2-server/${CN_VERSION}/jans-fido2-server-${CN_VERSION}.war @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-fido2/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -209,7 +209,7 @@ EXPOSE $CN_FIDO2_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/fido2" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen FIDO2" \ org.opencontainers.image.description="FIDO2 server" @@ -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..5b1135dd0c7 100644 --- a/docker-jans-kc-scheduler/Dockerfile +++ b/docker-jans-kc-scheduler/Dockerfile @@ -13,7 +13,7 @@ RUN apk update \ # KC scheduler # ============ -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 09:15' ENV SCHEDULER_HOME=/opt/kc-scheduler @@ -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=040ff17942019bc10433ce17d819b8d8474f13c8 # 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 @@ -139,7 +139,7 @@ EXPOSE $CN_SAML_HTTP_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/kc-scheduler" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen KC scheduler" \ org.opencontainers.image.description="" @@ -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..3cd9c64fd20 100644 --- a/docker-jans-keycloak-link/Dockerfile +++ b/docker-jans-keycloak-link/Dockerfile @@ -41,7 +41,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ======= -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 09:05' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-keycloak-link-server/${CN_VERSION}/jans-keycloak-link-server-${CN_VERSION}.war @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-keycloak-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -202,7 +202,7 @@ EXPOSE $CN_KEYCLOAK_LINK_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/keycloak-link" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Keycloak Link" \ org.opencontainers.image.description="" @@ -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..f0fa8b23719 100644 --- a/docker-jans-link/Dockerfile +++ b/docker-jans-link/Dockerfile @@ -41,7 +41,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ==== -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 08:56' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-link-server/${CN_VERSION}/jans-link-server-${CN_VERSION}.war @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -202,7 +202,7 @@ EXPOSE $CN_LINK_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/link" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Link" \ org.opencontainers.image.description="" @@ -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..25ab0018e30 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=040ff17942019bc10433ce17d819b8d8474f13c8 # cleanup RUN rm -rf /tmp/jans @@ -92,7 +92,7 @@ ENV CN_HOSTNAME="demoexample.jans.io" \ LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/monolith" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Monolith Image" \ org.opencontainers.image.description="Janssen Authorization server" diff --git a/docker-jans-monolith/clean.sh b/docker-jans-monolith/clean.sh index 96b650b521a..ff9bbd47109 100644 --- a/docker-jans-monolith/clean.sh +++ b/docker-jans-monolith/clean.sh @@ -23,7 +23,7 @@ if [ -z "$INSTALLED_JANSSEN_NAME" ]; then fi if [ -z "$JANSSEN_VERSION" ]; then - JANSSEN_VERSION="0.0.0-nightly" + JANSSEN_VERSION="1.3.0-1" fi if [ -z "$DATABASE_VOLUME_NAME" ]; then diff --git a/docker-jans-monolith/down.sh b/docker-jans-monolith/down.sh index d1af54d8a61..2b2b921507d 100644 --- a/docker-jans-monolith/down.sh +++ b/docker-jans-monolith/down.sh @@ -23,7 +23,7 @@ if [ -z "$INSTALLED_JANSSEN_NAME" ]; then fi if [ -z "$JANSSEN_VERSION" ]; then - JANSSEN_VERSION="0.0.0-nightly" + JANSSEN_VERSION="1.3.0-1" fi if [ -z "$JANSSEN_SERVICE_NAME" ]; then diff --git a/docker-jans-monolith/jans-mysql-compose.yml b/docker-jans-monolith/jans-mysql-compose.yml index 07a4d639d67..95708ffbc0e 100644 --- a/docker-jans-monolith/jans-mysql-compose.yml +++ b/docker-jans-monolith/jans-mysql-compose.yml @@ -16,7 +16,7 @@ services: - MYSQL_PASSWORD=1t5Fin3#security - MYSQL_ROOT_PASSWORD=1t5Fin3#security jans: - image: ${JANSSEN_IMAGE:-ghcr.io/janssenproject/jans/monolith:0.0.0-nightly} + image: ${JANSSEN_IMAGE:-ghcr.io/janssenproject/jans/monolith:1.3.0-1} restart: always ports: - "443:443" diff --git a/docker-jans-monolith/jans-postgres-compose.yml b/docker-jans-monolith/jans-postgres-compose.yml index c7707f0a342..891a496cada 100644 --- a/docker-jans-monolith/jans-postgres-compose.yml +++ b/docker-jans-monolith/jans-postgres-compose.yml @@ -14,7 +14,7 @@ services: POSTGRES_PASSWORD: 1t5Fin3#security POSTGRES_DB: jans jans: - image: ${JANSSEN_IMAGE:-ghcr.io/janssenproject/jans/monolith:0.0.0-nightly} + image: ${JANSSEN_IMAGE:-ghcr.io/janssenproject/jans/monolith:1.3.0-1} restart: always ports: - "443:443" diff --git a/docker-jans-monolith/up.sh b/docker-jans-monolith/up.sh index 7dd0eb8cff8..e5a6170a651 100644 --- a/docker-jans-monolith/up.sh +++ b/docker-jans-monolith/up.sh @@ -23,7 +23,7 @@ if [ -z "$INSTALLED_JANSSEN_NAME" ]; then fi if [ -z "$JANSSEN_VERSION" ]; then - JANSSEN_VERSION="0.0.0-nightly" + JANSSEN_VERSION="1.3.0-1" fi if [ -z "$DATABASE_VOLUME_NAME" ]; then diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index bb662da7577..675ea717070 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=040ff17942019bc10433ce17d819b8d8474f13c8 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 @@ -159,7 +159,7 @@ ENV CN_CACHE_TYPE=NATIVE_PERSISTENCE \ LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/persistence-loader" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen Authorization Server Persistence loader" \ org.opencontainers.image.description="Generate initial data for persistence layer" @@ -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..ce66e05ed61 100644 --- a/docker-jans-saml/Dockerfile +++ b/docker-jans-saml/Dockerfile @@ -23,7 +23,7 @@ RUN mkdir -p /opt/keycloak/logs \ # KC integration # ============== -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 09:15' RUN wget -q https://jenkins.jans.io/maven/io/jans/kc-jans-spi/${CN_VERSION}/kc-jans-spi-${CN_VERSION}.jar -P /opt/keycloak/providers \ @@ -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=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) @@ -173,7 +173,7 @@ EXPOSE $CN_SAML_HTTP_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/saml" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen SAML" \ org.opencontainers.image.description="" @@ -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..9ff848d9f88 100644 --- a/docker-jans-scim/Dockerfile +++ b/docker-jans-scim/Dockerfile @@ -40,7 +40,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # SCIM # ==== -ENV CN_VERSION=0.0.0-nightly +ENV CN_VERSION=1.3.0 ENV CN_BUILD_DATE='2024-12-20 10:33' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-scim-server/${CN_VERSION}/jans-scim-server-${CN_VERSION}.war @@ -60,7 +60,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-scim/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=aa1b2edaa8d7e3413bd57a7bd7cc86206086768b +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCIM_RESOURCE_DIR=jans-scim/server/src/main/resources @@ -204,7 +204,7 @@ EXPOSE $CN_SCIM_JETTY_PORT LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/scim" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="Janssen SCIM" \ org.opencontainers.image.description="SCIM server" @@ -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/agama-catalog/jans/inboundID/project/project.json b/docs/agama-catalog/jans/inboundID/project/project.json index 2ff995479db..f06e2f76819 100644 --- a/docs/agama-catalog/jans/inboundID/project/project.json +++ b/docs/agama-catalog/jans/inboundID/project/project.json @@ -2,7 +2,7 @@ "projectName": "agama-inbound-oauth", "author": "jgomer2001", "type": "Community", - "version": "0.0.0-nightly", + "version": "1.3.0", "description": "A project useful to delegate authorization to external services like social sites", "noDirectLaunch": [ "io.jans.inbound.Apple", "io.jans.inbound.GenericProvider", "io.jans.inbound.oauth2.AuthzCode", "io.jans.inbound.oauth2.AuthzCodeWithUserInfo" ], "configs": { 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/assets/jans_health.png b/docs/assets/jans_health.png new file mode 100644 index 00000000000..86553acaa75 Binary files /dev/null and b/docs/assets/jans_health.png differ diff --git a/docs/assets/jans_info.png b/docs/assets/jans_info.png new file mode 100644 index 00000000000..9fea89fa659 Binary files /dev/null and b/docs/assets/jans_info.png differ diff --git a/docs/assets/jans_logs.png b/docs/assets/jans_logs.png new file mode 100644 index 00000000000..ed8818f2443 Binary files /dev/null and b/docs/assets/jans_logs.png differ diff --git a/docs/assets/jans_restart.png b/docs/assets/jans_restart.png new file mode 100644 index 00000000000..b4c8a7cea97 Binary files /dev/null and b/docs/assets/jans_restart.png differ diff --git a/docs/assets/jans_start.png b/docs/assets/jans_start.png new file mode 100644 index 00000000000..e85c33c5307 Binary files /dev/null and b/docs/assets/jans_start.png differ diff --git a/docs/assets/jans_status.png b/docs/assets/jans_status.png new file mode 100644 index 00000000000..42485240e54 Binary files /dev/null and b/docs/assets/jans_status.png differ diff --git a/docs/assets/jans_stop.png b/docs/assets/jans_stop.png new file mode 100644 index 00000000000..b840865ff68 Binary files /dev/null and b/docs/assets/jans_stop.png differ diff --git a/docs/assets/jans_version.png b/docs/assets/jans_version.png new file mode 100644 index 00000000000..305b0848251 Binary files /dev/null and b/docs/assets/jans_version.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 0f5f03fe0f3..70445076942 100644 --- a/docs/cedarling/cedarling-authz.md +++ b/docs/cedarling/cedarling-authz.md @@ -55,7 +55,9 @@ Action, Resource and Context is sent by the application in the authorization req this is a sample request from a hypothetical application: ```js -input = { +const bootstrap_config = {...}; +const cedarling = await init(bootstrap_config); +let input = { "tokens": { "access_token": "eyJhbGc....", "id_token": "eyJjbGc...", @@ -76,19 +78,19 @@ input = { } } -decision_result = authz(input) +decision_result = await cedarling(input) ``` ## Automatically Adding Entity References to the Context -Cedarling simplifies context creation by automatically including certain entities. This means you don't need to manually pass their references when using them in your policies. The following entities are automatically added to the context, along with their naming conventions in `lower_snake_case` format: +Cedarling simplifies context creation by automatically including certain entities. This means you don't need to manually pass their references when using them in your policies. The following entities are automatically added to the context. -- **Workload Entity**: `workload` -- **User Entity**: `user` -- **Resource Entity**: `resource` -- **Access Token Entity**: `access_token` -- **ID Token Entity**: `id_token` -- **Userinfo Token Entity**: `userinfo_token` +- Workload Entity +- User Entity +- Resource Entity +- Access Token Entity +- ID Token Entity +- Userinfo Token Entity ### Example Policy 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-policy-store.md b/docs/cedarling/cedarling-policy-store.md index 55bdf64de8c..504f9cf69ca 100644 --- a/docs/cedarling/cedarling-policy-store.md +++ b/docs/cedarling/cedarling-policy-store.md @@ -178,7 +178,11 @@ This record contains the information needed to validate tokens from this issuer: - **description** : (*String*) A brief description of the trusted issuer, providing context for administrators. - **openid_configuration_endpoint** : (*String*) The HTTPS URL for the OpenID Connect configuration endpoint (usually found at `/.well-known/openid-configuration`). - **identity_source** : (*Object*, *optional*) Metadata related to the tokens issued by this issuer. -- **`access_tokens`, `id_tokens`, `userinfo_tokens`, and `tx_tokens`**: See: [Token Metadata Schema](#token-metadata-schema). + +**Notes**: + +- The `access_tokens`, `id_tokens`, `userinfo_tokens`, and `tx_tokens` fields will follow the [Token Metadata Schema](#token-metadata-schema). +- The `access_tokens` will contain a `trusted` and `principal_identifier` field in addition to the fields from the `Token Metadata Schema`. ### Token Metadata Schema @@ -186,8 +190,6 @@ The Token Entity Metadata Schema defines how tokens are mapped, parsed, and tran ```json { - "trusted": bool, - "principal_identifier": "str", "user_id": "", "role_mapping": "", "claim_mapping": { diff --git a/docs/cedarling/python/sidecar.md b/docs/cedarling/cedarling-sidecar.md similarity index 51% rename from docs/cedarling/python/sidecar.md rename to docs/cedarling/cedarling-sidecar.md index 9febea1c8a4..6a3647a0fc6 100644 --- a/docs/cedarling/python/sidecar.md +++ b/docs/cedarling/cedarling-sidecar.md @@ -1,7 +1,6 @@ --- tags: - cedarling - - python - sidecar --- @@ -11,13 +10,29 @@ The sidecar is a containerized Flask project that uses the `cedarling_python` bi ## Docker setup -- Ensure that you have installed [docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/install/). -- Clone the [Janssen](https://github.com/JanssenProject/jans) repository -- Navigate to `jans/jans-cedarling/flask-sidecar` -- Edit the provided `secrets/bootstrap.json` file to your specifications. The configuration keys are described [here](https://github.com/JanssenProject/jans/blob/ffe9f493e4a5c6b05f2adeeb8a6eba7eb83b103e/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi#L9). -- Run `docker compose up` - - For cloud deployments, please use the provided Dockerfile and pass your bootstrap configuration via the environment variable `CEDARLING_BOOTSTRAP_CONFIG_FILE`. -- The sidecar runs on port 5000. OpenAPI documentation is available at `http://0.0.0.0:5000/swagger-ui` +- Ensure that you have installed [docker](https://docs.docker.com/engine/install/) +- Create a file called `bootstrap.json`. You may use this [sample](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/flask-sidecar/secrets/bootstrap.json) file. +- Modify the bootstrap file to your specifications. In particular you need to provide a link to your policy store in `CEDARLING_POLICY_STORE_URI`. The configuration keys are described [here](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi#L10). +- Pull the docker image: + ``` + docker pull ghcr.io/janssenproject/jans/cedarling-flask-sidecar:1.3.0-1 + ``` +- Run the docker image, replacing `` with the absolute path to your bootstrap file: + + ```bash + docker run -d \ + -e APP_MODE='development' \ + -e CEDARLING_BOOTSTRAP_CONFIG_FILE=/bootstrap.json \ + -e SIDECAR_DEBUG_RESPONSE=False \ + --mount type=bind,src=,dst=/bootstrap.json \ + -p 5000:5000\ + ghcr.io/janssenproject/jans/cedarling-flask-sidecar:1.3.0-1 + ``` + + - `SIDECAR_DEBUG_RESPONSE` is an option that will cause the sidecar to return extra diagnostic information for each query if set to `True`. This may be useful to check which policies are being used to reach a decision. + - Take note of the output of the command. This is the container ID of the sidecar. +- The sidecar runs in the background on port 5000. OpenAPI documentation is available at `http://0.0.0.0:5000/swagger-ui` +- To stop the sidecar, run `docker container stop ` ## Usage @@ -25,11 +40,16 @@ The sidecar has one endpoint: `/cedarling/evaluation`. Example request to the evaluation endpoint: -``` +```json { "subject": { - "type": "string", - "id": "string" + "type": "JWT", + "id": "cedarling", + "properties": { + "access_token": "", + "id_token": "", + "userinfo_token": "" + } }, "resource": { "type": "Jans::Application", @@ -48,9 +68,6 @@ Example request to the evaluation endpoint: "name": "Jans::Action::\"Read\"" }, "context": { - "access_token": "...", - "id_token": "...", - "userinfo_token": "...", "device_health": [ "Healthy" ], @@ -69,13 +86,13 @@ Example request to the evaluation endpoint: } ``` -Cedarling requires OpenID Userinfo, Access, and ID tokens to construct the principal entity, as described [here](../cedarling-authz.md). As per AuthZen specification, these values are sent in the `context` field of the payload. Conversely, the `subject` field is currently not used by cedarling. These 3 tokens are subsequently removed from the context object before it is passed to cedarling. +Cedarling requires OpenID Userinfo, Access, and ID tokens to construct the principal entity, as described [here](./cedarling-authz.md). These values are sent in the subject field's properties. Upon creating the principal, action, resource, and context entities, cedarling will evaluate these entities against the policies defined in the policy store. Then it will return a true/false decision. If the decision is false, the sidecar will analyze cedarling diagnostics and provide additional information for the admin. Example of `true` case: -``` +```json { "decision": true } @@ -83,7 +100,7 @@ Example of `true` case: Example of `false` case: -``` +```json { "context": { "reason_admin": { @@ -99,4 +116,4 @@ Example of `false` case: } ``` -In this example both the person and workload evaluations were `DENY`, so the decision was false. Additional information is returned in the `context` field as per AuthZen specification. +In this example both the person and workload evaluations were `DENY`, so the decision was false. Additional information is returned in the `context` field. 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/cedarling/python/README.md b/docs/cedarling/python/README.md index 134fd3d4510..630a29de18c 100644 --- a/docs/cedarling/python/README.md +++ b/docs/cedarling/python/README.md @@ -33,14 +33,18 @@ The recommended way to include cedarling in a Python project is to compile it to ``` - The wheel will be available in `target/wheels/` -## Including in projects +- If you are developing a simple project in a `venv` setup, you can run `maturin develop --release` and maturin will install the wheel into the currently activated virtual environment. After that, you may run your code directly from the command line. -If you are developing a simple project in a `venv` setup, in the previous section you can run `maturin develop --release` and maturin will install the wheel into the currently activated virtual environment. After that, you may run your code directly from the command line. +## Including in projects In case of more complicated projects with a dependency manager such as [poetry](https://python-poetry.org/), you can either install the wheel via the command line: ``` poetry add path/to/wheel.whl ``` +Or install it into the virtual environment managed by poetry: +``` +poetry run pip install path/to/wheel.whl +``` or include it as a static dependency in the [dependencies](https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups) section of your `pyproject.toml`: ``` ... diff --git a/docs/cedarling/python/usage.md b/docs/cedarling/python/usage.md index 9b531d569e0..c9b21738e1c 100644 --- a/docs/cedarling/python/usage.md +++ b/docs/cedarling/python/usage.md @@ -16,12 +16,8 @@ In this example, we will show an example Python script that calls the `cedarling ``` (venv) $ python example.py -Policy store location not provided, use 'CEDARLING_LOCAL_POLICY_STORE' environment variable -Used default policy store path: example_files/policy-store.json - -{"id":"0193414e-9672-786a-986c-57f48d41c4e4","time":1731967489,"log_type":"System","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"configuration parsed successfully"} -{"id":"0193414e-9672-786a-986c-57f5379086c3","time":1731967489,"log_type":"System","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"Cedarling Authz initialized successfully","application_id":"TestApp"} -{"id":"0193414e-9676-7d8a-b55b-3f0097355851","time":1731967489,"log_type":"Decision","pdp_id":"c0ec33ff-9482-4bdc-83f6-2925a41a3280","msg":"Result of authorize.","application_id":"TestApp","action":"Jans::Action::\"Read\"","resource":"Jans::Application::\"some_id\"","context":{"user_agent":"Linux","operating_system":"Linux","network_type":"Local","network":"127.0.0.1","geolocation":["America"],"fraud_indicators":["Allowed"],"device_health":["Healthy"],"current_time":1731967489},"person_principal":"Jans::User::\"qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0\"","person_diagnostics":{"reason":["840da5d85403f35ea76519ed1a18a33989f855bf1cf8"],"errors":[]},"person_decision":"ALLOW","workload_principal":"Jans::Workload::\"d7f71bea-c38d-4caf-a1ba-e43c74a11a62\"","workload_diagnostics":{"reason":["444da5d85403f35ea76519ed1a18a33989f855bf1cf8"],"errors":[]},"workload_decision":"ALLOW","authorized":true} +{"request_id":"019474da-12ee-7315-bb12-35f46a9bc2b2","timestamp":"2025-01-17T09:20:36.334Z","log_kind":"System","pdp_id":"4cf7864b-50d4-4492-8cd1-3ddb424e2711","level":"INFO","msg":"Cedarling Authz initialized successfully","application_id":"My App","cedar_lang_version":"4.1.0","cedar_sdk_version":"4.2.2"} +{"request_id":"019474da-12f3-74f1-8f3e-da7624806135","timestamp":"2025-01-17T09:20:36.339Z","log_kind":"Decision","pdp_id":"4cf7864b-50d4-4492-8cd1-3ddb424e2711","policystore_id":"gICAgcHJpbmNpcGFsIGlz","policystore_version":"undefined","principal":"User & Workload","User":{"email":{"domain":"jans.test","uid":"admin"},"sub":"qzxn1Scrb9lWtGxVedMCky-Ql_ILspZaQA6fyuYktw0"},"Workload":{"client_id":"d7f71bea-c38d-4caf-a1ba-e43c74a11a62"},"diagnostics":{"reason":[{"id":"840da5d85403f35ea76519ed1a18a33989f855bf1cf8","description":"simple policy example for principal user"}],"errors":[]},"action":"Jans::Action::\"Read\"","resource":"Jans::Application::\"some_id\"","decision":"ALLOW","tokens":{"id_token":{"jti":"ijLZO1ooRyWrgIn7cIdNyA"},"Userinfo":{"jti":"OIn3g1SPSDSKAYDzENVoug"},"access":{"jti":"uZUh1hDUQo6PFkBPnwpGzg"}},"decision_time_ms":3} Result of workload authorization: ALLOW Policy ID used: 444da5d85403f35ea76519ed1a18a33989f855bf1cf8 @@ -35,7 +31,8 @@ Errors during authorization: 0 ## Explanation -Cedarling creates principal entities from the access, ID and userinfo tokens. The action, resource and context entities are declared in code. These four entities together form the `PARC` format that cedarling evaluates against policies provided in the policy store. The principal entities can be either User, Workload or Role. After forming the entities, cedarling evaluates them against the policies provided in the policy store. If entity is explicitly permitted by a policy, the result of the evaluation is `ALLOW`, otherwise it is `DENY`. +Cedarling creates principal entities from either the access, ID and userinfo tokens, or a combination of the three depending on bootstrap configurations. For example, to create a Workload entity only one token is sufficient. But to create a user entity at least ID or Userinfo tokens are needed. This is defined by `CEDARLING_USER_AUTHZ` and `CEDARLING_WORKLOAD_AUTHZ` in the [bootstrap configuration](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi#L10). Cedarling will make a best attempt to create entities based on tokens provided; if it is unable to do so it will raise an `EntitiesError` exception. +The action, resource and context entities are declared in code. These four entities together form the `PARC` format that cedarling evaluates against policies provided in the policy store. The principal entities can be either User, Workload or Role. After forming the entities, cedarling evaluates them against the policies provided in the policy store. If entity is explicitly permitted by a policy, the result of the evaluation is `ALLOW`, otherwise it is `DENY`. In this case there are two policies in the store, one for User entities and one for Workload entities: @@ -91,9 +88,7 @@ context = { } request = Request( - access_token, - id_token, - userinfo_token, + tokens=Tokens(access_token, id_token, userinfo_token), action=action, resource=resource, context=context) @@ -101,7 +96,7 @@ authorize_result = instance.authorize(request) assert authorize_result.is_allowed() ``` -Cedarling will return `is_allowed()` as `True` only if both the User and Workload entity evaluations are `ALLOW`. +Cedarling will return `is_allowed()` as `True` only if the authorization queries set in the bootstrap return `True`. In case of the example, both `CEDARLING_USER_AUTHZ` and `CEDARLING_WORKLOAD_AUTHZ` were set to `enabled`, so cedarling will only return True if both user and workload evaluations are true. ## Exposed functions diff --git a/docs/contribute/testing.md b/docs/contribute/testing.md index 3fc5672b60a..debd5321cb4 100644 --- a/docs/contribute/testing.md +++ b/docs/contribute/testing.md @@ -109,7 +109,7 @@ As part of pre-release QA check, we run a set of [manual sanity checks](#sanity- - Review functioning of `.well-known` endpoints for OpenId, Fido, UMA, SCIM modules - Test device authentication flow using TUI -- Test password authentication flow using Janssen Server Tent +- Test password authentication flow using Jans Tarp - Test Agama project deployment and functioning ### Post-release QA checklist 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/config-guide/scim-config/user-config.md b/docs/janssen-server/config-guide/scim-config/user-config.md index 131a7bba914..b38a3f7f819 100644 --- a/docs/janssen-server/config-guide/scim-config/user-config.md +++ b/docs/janssen-server/config-guide/scim-config/user-config.md @@ -454,6 +454,25 @@ To modify any user properties, find the user from search box and hit `Enter` to ![update-user](../../../assets/jans-tui-update-user.png) +### Change User Password +No chance to recover user password, but you can change. +To change password of a user navigate/or search user and press key `p` when the target user is higlighted. +In the figure below, passowrd of user **sakamura** is being changed. + +![Change User Password](../../../assets/tui-user-change-password.png) + +Once you write new password (it will be displayed while you type), go to button `< Save >` and press Enter. + +### Manage User FIDO Devices +To view and manage users registered FIDO devices, first navigate/or search user and press key `f` on the keyboard. +If user has any registered FIDO device, a popup will appears as in image below: + +![User FIDO Devices](../../../assets/tui-ser-fido-device-list.png) + +You can veiw details of a device by pressing Enter. To delete a device press key `d`, you will be +prompted for confirmation. + + ### Delete User To delete user, bring the control on the specific user row and press `delete` or `d` key from keyboard. It will show a pop-up for confirmation as below: 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/install/vm-install/rhel.md b/docs/janssen-server/install/vm-install/rhel.md index 8c4709aef3b..730d2c26989 100644 --- a/docs/janssen-server/install/vm-install/rhel.md +++ b/docs/janssen-server/install/vm-install/rhel.md @@ -41,7 +41,7 @@ sudo rpm -import automation-jans-public-gpg.asc [Releases](https://github.com/JanssenProject/jans/releases) ``` -wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version-stable.el8.x86_64.rpm -P ~/ +wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version.el8.x86_64.rpm -P ~/ ``` - Verify integrity of the downloaded package using published `sha256sum`. @@ -49,7 +49,7 @@ wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-v Download `sha256sum` file for the package ```shell - wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version-stable.el8.x86_64.rpm.sha256sum -P ~/ + wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version.el8.x86_64.rpm.sha256sum -P ~/ ``` Check the hash if it is matching. diff --git a/docs/janssen-server/install/vm-install/suse.md b/docs/janssen-server/install/vm-install/suse.md index d5768e4ae78..4f08242ea54 100644 --- a/docs/janssen-server/install/vm-install/suse.md +++ b/docs/janssen-server/install/vm-install/suse.md @@ -53,7 +53,7 @@ sudo rpm -import automation-jans-public-gpg.asc [Releases](https://github.com/JanssenProject/jans/releases) ```shell -wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version-stable.suse15.x86_64.rpm +wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version.suse15.x86_64.rpm ``` - Verify integrity of the downloaded package using published `sha256sum`. @@ -61,7 +61,7 @@ wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-v Download `sha256sum` file for the package ```shell -wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version-stable.suse15.x86_64.rpm.sha256sum +wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans-replace-janssen-version.suse15.x86_64.rpm.sha256sum ``` Check the hash if it is matching. You may need to change your working directory diff --git a/docs/janssen-server/install/vm-install/ubuntu.md b/docs/janssen-server/install/vm-install/ubuntu.md index 1fb6963db3c..1db461aa491 100644 --- a/docs/janssen-server/install/vm-install/ubuntu.md +++ b/docs/janssen-server/install/vm-install/ubuntu.md @@ -38,7 +38,7 @@ sudo gpg --import automation-jans-public-gpg.asc; [Releases](https://github.com/JanssenProject/jans/releases) ``` -wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version-stable.ubuntu22.04_amd64.deb -P /tmp +wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version.ubuntu22.04_amd64.deb -P /tmp ``` - Verify integrity of the downloaded package by verifying published `sha256sum`. @@ -46,7 +46,7 @@ wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-v Download `sha256sum` file for the package ```shell - wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version-stable.ubuntu22.04_amd64.deb.sha256sum -P /tmp + wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version.ubuntu22.04_amd64.deb.sha256sum -P /tmp ``` Check the hash if it is matching. @@ -74,7 +74,7 @@ sudo apt install ./jans_replace-janssen-version.ubuntu22.04_amd64.deb [Releases](https://github.com/JanssenProject/jans/releases) ``` -wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version-stable.ubuntu20.04_amd64.deb -P /tmp +wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version.ubuntu20.04_amd64.deb -P /tmp ``` - Verify integrity of the downloaded package by verifying published `sha256sum`. @@ -82,7 +82,7 @@ wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-v Download `sha256sum` file for the package ```shell - wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version-stable.ubuntu20.04_amd64.deb.sha256sum -P /tmp + wget https://github.com/JanssenProject/jans/releases/download/vreplace-janssen-version/jans_replace-janssen-version.ubuntu20.04_amd64.deb.sha256sum -P /tmp ``` Check the hash if it is matching. diff --git a/docs/janssen-server/link/jans-kc-link.md b/docs/janssen-server/link/jans-kc-link.md deleted file mode 100644 index be86886c5d9..00000000000 --- a/docs/janssen-server/link/jans-kc-link.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -tags: - - administration - - link - - keycloak ---- - -# Jans Keycloak Link - -The Jans Keycloak Link is a [Jans Link](README.md) module that provides -synchronization services to update the Janssen User Store from an external -Keycloak instance. - -Jans Keycloak Link accesses Keycloak data via Keycloak API. A new -client needs to be created on Keycloak in order to authorize Jans Keycloak Link -for API access. The client can be configured to use one of the two -authentication mechanisms: - -- [Client Credentials Grant](#using-client-credentials-grant) -- [Resource Owner Password Credentials Grant](#using-resource-owner-password-credentials-grant) - -## Using Client Credentials Grant - -### Create Client on Keycloak - -- Create a new OpenId Connect client from Keycloak administration console -- Configure this client as having `confidential` access type by enabling `client - authentication` -- Enable `Service Accounts Enabled` flag, which enables client credentials grant - ![](../../assets/jans-kc-link-client-2.png) -- Go to the tab `Service accounts roles`, assign role `admin` to the client using - `Assign role` button - ![](../../assets/jans-kc-link-client-4.png) -- Keep a note of the client ID and client secret. This detail will be required - to be added to the Janssen server - -### Configure Jans Keycloak Link Module - -On the Janssen server, Jans Keycloak Link module configuration needs to be -updated to be able to connect with Keycloak server. - -- Using [TUI](../config-guide/config-tools/jans-tui/README.md), update the - Jans KC Link module configuration. Navigate to - `Jans KC Link` -> `Keycloak Configuration`, and configure following - parameters: - - `Server URL`: Keycloak Server URL - - `Realm`: Keycloak Realm - - `Client ID`: ID of the newly created client on Keycloak - - `Client Secret`: Client secret of the Keycloak client - - `Grant Type`: Set this as _client_credentials_ - - ![](../../assets/tui-kc-link-kc-config-client-cred.png) -- [Test](#test-the-integration) the integration - -## Using Resource Owner Password Credentials Grant - -!!! Note -Use of this grant type is generally discouraged and [removed from OAuth -2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#name-differences-from-oauth-20). - -### Configure Client on Keycloak - -- Create a new OpenId Connect client from Keycloak administration console -- Configure this client as having `direct access grant` - ![](../../assets/jans-kc-link-client-3.png) -- Create a user in the Keycloak server. The user should have permission to - access Keycloak API in the Keycloak. For the instructions in this document, - We will use the default Keycloak user which is `admin`. - -### Configure Jans Keycloak Link Module - -On the Janssen server, Jans Keycloak Link module configuration needs to be -updated to be able to connect with Keycloak server. - -- Using [TUI](../config-guide/config-tools/jans-tui/README.md), update the - Jans KC Link module configuration. Navigate to - `Jans KC Link` -> `Keycloak Configuration`, and configure following - parameters: - - `Server URL`: Keycloak Server URL - - `Realm`: Keycloak Realm - - `Client ID`: ID of the newly created client on Keycloak - - `Grant Type`: Set this as _password_ - - `Username`: Set this as _admin_ - - `Password`: Password of _admin_ user - - ![](../../assets/tui-kc-link-kc-config-ropc.png) -- [Test](#test-the-integration) the integration - -## Test The Integration - -To check if the integration is working, you can create a user on Keycloak server. -This user should reflect in Janssen Server after the polling interval has passed. - -![](../../assets/jans-kc-link-user-create.png) - -Use [TUI](../config-guide/config-tools/jans-tui/README.md) to see the list of -available users in Janssen Server. - -![](../../assets/jans-kc-link-user-in-jans.png) - -## Want to contribute? - -If you have content you'd like to contribute to this page in the meantime, -you can get started with -our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). diff --git a/docs/janssen-server/link/jans-keycloak-link.md b/docs/janssen-server/link/jans-keycloak-link.md index 528f5ae2c94..be86886c5d9 100644 --- a/docs/janssen-server/link/jans-keycloak-link.md +++ b/docs/janssen-server/link/jans-keycloak-link.md @@ -5,15 +5,101 @@ tags: - keycloak --- +# Jans Keycloak Link -## This content is in progress +The Jans Keycloak Link is a [Jans Link](README.md) module that provides +synchronization services to update the Janssen User Store from an external +Keycloak instance. -The Janssen Project documentation is currently in development. Topic pages are being created in order of broadest relevance, and this page is coming in the near future. +Jans Keycloak Link accesses Keycloak data via Keycloak API. A new +client needs to be created on Keycloak in order to authorize Jans Keycloak Link +for API access. The client can be configured to use one of the two +authentication mechanisms: -## Have questions in the meantime? +- [Client Credentials Grant](#using-client-credentials-grant) +- [Resource Owner Password Credentials Grant](#using-resource-owner-password-credentials-grant) -While this documentation is in progress, you can ask questions through [GitHub Discussions](https://github.com/JanssenProject/jans/discussions) or the [community chat on Gitter](https://gitter.im/JanssenProject/Lobby). Any questions you have will help determine what information our documentation should cover. +## Using Client Credentials Grant + +### Create Client on Keycloak + +- Create a new OpenId Connect client from Keycloak administration console +- Configure this client as having `confidential` access type by enabling `client + authentication` +- Enable `Service Accounts Enabled` flag, which enables client credentials grant + ![](../../assets/jans-kc-link-client-2.png) +- Go to the tab `Service accounts roles`, assign role `admin` to the client using + `Assign role` button + ![](../../assets/jans-kc-link-client-4.png) +- Keep a note of the client ID and client secret. This detail will be required + to be added to the Janssen server + +### Configure Jans Keycloak Link Module + +On the Janssen server, Jans Keycloak Link module configuration needs to be +updated to be able to connect with Keycloak server. + +- Using [TUI](../config-guide/config-tools/jans-tui/README.md), update the + Jans KC Link module configuration. Navigate to + `Jans KC Link` -> `Keycloak Configuration`, and configure following + parameters: + - `Server URL`: Keycloak Server URL + - `Realm`: Keycloak Realm + - `Client ID`: ID of the newly created client on Keycloak + - `Client Secret`: Client secret of the Keycloak client + - `Grant Type`: Set this as _client_credentials_ + + ![](../../assets/tui-kc-link-kc-config-client-cred.png) +- [Test](#test-the-integration) the integration + +## Using Resource Owner Password Credentials Grant + +!!! Note +Use of this grant type is generally discouraged and [removed from OAuth +2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#name-differences-from-oauth-20). + +### Configure Client on Keycloak + +- Create a new OpenId Connect client from Keycloak administration console +- Configure this client as having `direct access grant` + ![](../../assets/jans-kc-link-client-3.png) +- Create a user in the Keycloak server. The user should have permission to + access Keycloak API in the Keycloak. For the instructions in this document, + We will use the default Keycloak user which is `admin`. + +### Configure Jans Keycloak Link Module + +On the Janssen server, Jans Keycloak Link module configuration needs to be +updated to be able to connect with Keycloak server. + +- Using [TUI](../config-guide/config-tools/jans-tui/README.md), update the + Jans KC Link module configuration. Navigate to + `Jans KC Link` -> `Keycloak Configuration`, and configure following + parameters: + - `Server URL`: Keycloak Server URL + - `Realm`: Keycloak Realm + - `Client ID`: ID of the newly created client on Keycloak + - `Grant Type`: Set this as _password_ + - `Username`: Set this as _admin_ + - `Password`: Password of _admin_ user + + ![](../../assets/tui-kc-link-kc-config-ropc.png) +- [Test](#test-the-integration) the integration + +## Test The Integration + +To check if the integration is working, you can create a user on Keycloak server. +This user should reflect in Janssen Server after the polling interval has passed. + +![](../../assets/jans-kc-link-user-create.png) + +Use [TUI](../config-guide/config-tools/jans-tui/README.md) to see the list of +available users in Janssen Server. + +![](../../assets/jans-kc-link-user-in-jans.png) ## Want to contribute? -If you have content you'd like to contribute to this page in the meantime, you can get started with our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). \ No newline at end of file +If you have content you'd like to contribute to this page in the meantime, +you can get started with +our [Contribution guide](https://docs.jans.io/head/CONTRIBUTING/). 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/janssen-server/reference/kubernetes/docker-jans-certmanager.md b/docs/janssen-server/reference/kubernetes/docker-jans-certmanager.md index adb12653d6a..f835942ca00 100644 --- a/docs/janssen-server/reference/kubernetes/docker-jans-certmanager.md +++ b/docs/janssen-server/reference/kubernetes/docker-jans-certmanager.md @@ -186,7 +186,7 @@ spec: spec: containers: - name: auth-key-rotation - image: ghcr.io/janssenproject/jans/certmanager:0.0.0-nightly + image: ghcr.io/janssenproject/jans/certmanager:1.3.0-1 resources: requests: memory: "300Mi" diff --git a/docs/janssen-server/reference/kubernetes/helm-chart.md b/docs/janssen-server/reference/kubernetes/helm-chart.md index 44a994a6b75..45a843062a3 100644 --- a/docs/janssen-server/reference/kubernetes/helm-chart.md +++ b/docs/janssen-server/reference/kubernetes/helm-chart.md @@ -6,7 +6,7 @@ tags: --- # janssen -![Version: 0.0.0-nightly](https://img.shields.io/badge/Version-0.0.0--nightly-informational?style=flat-square) ![AppVersion: 0.0.0-nightly](https://img.shields.io/badge/AppVersion-0.0.0--nightly-informational?style=flat-square) +![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) Janssen Access and Identity Management Microservices Chart. This chart deploys each janssen microservice as a separate deployment. @@ -29,26 +29,26 @@ Kubernetes: `>=v1.22.0-0` | Repository | Name | Version | |------------|------|---------| -| | auth-server | 0.0.0-nightly | -| | auth-server-key-rotation | 0.0.0-nightly | -| | casa | 0.0.0-nightly | -| | cn-istio-ingress | 0.0.0-nightly | -| | config | 0.0.0-nightly | -| | config-api | 0.0.0-nightly | -| | fido2 | 0.0.0-nightly | -| | kc-scheduler | 0.0.0-nightly | -| | link | 0.0.0-nightly | -| | nginx-ingress | 0.0.0-nightly | -| | persistence | 0.0.0-nightly | -| | saml | 0.0.0-nightly | -| | scim | 0.0.0-nightly | +| | auth-server | 1.3.0 | +| | auth-server-key-rotation | 1.3.0 | +| | casa | 1.3.0 | +| | cn-istio-ingress | 1.3.0 | +| | config | 1.3.0 | +| | config-api | 1.3.0 | +| | fido2 | 1.3.0 | +| | kc-scheduler | 1.3.0 | +| | link | 1.3.0 | +| | nginx-ingress | 1.3.0 | +| | persistence | 1.3.0 | +| | saml | 1.3.0 | +| | scim | 1.3.0 | ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| -| auth-server | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/auth-server","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"2500m","memory":"2500Mi"},"requests":{"cpu":"2500m","memory":"2500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. | -| auth-server-key-rotation | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/certmanager","tag":"0.0.0-nightly"},"keysLife":48,"keysPushDelay":0,"keysPushStrategy":"NEWER","keysStrategy":"NEWER","lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for regenerating auth-keys per x hours | +| auth-server | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/auth-server","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"2500m","memory":"2500Mi"},"requests":{"cpu":"2500m","memory":"2500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. | +| auth-server-key-rotation | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/certmanager","tag":"1.3.0-1"},"keysLife":48,"keysPushDelay":0,"keysPushStrategy":"NEWER","keysStrategy":"NEWER","lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for regenerating auth-keys per x hours | | auth-server-key-rotation.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | auth-server-key-rotation.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | auth-server-key-rotation.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -58,7 +58,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server-key-rotation.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | auth-server-key-rotation.image.pullSecrets | list | `[]` | Image Pull Secrets | | auth-server-key-rotation.image.repository | string | `"ghcr.io/janssenproject/jans/certmanager"` | Image to use for deploying. | -| auth-server-key-rotation.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| auth-server-key-rotation.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | auth-server-key-rotation.keysLife | int | `48` | Auth server key rotation keys life in hours | | auth-server-key-rotation.keysPushDelay | int | `0` | Delay (in seconds) before pushing private keys to Auth server | | auth-server-key-rotation.keysPushStrategy | string | `"NEWER"` | Set key selection strategy after pushing private keys to Auth server (only takes effect when keysPushDelay value is greater than 0) | @@ -85,7 +85,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | auth-server.image.pullSecrets | list | `[]` | Image Pull Secrets | | auth-server.image.repository | string | `"ghcr.io/janssenproject/jans/auth-server"` | Image to use for deploying. | -| auth-server.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| auth-server.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | auth-server.livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | auth-server.livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | Executes the python3 healthcheck. https://github.com/JanssenProject/docker-jans-auth-server/blob/master/scripts/healthcheck.py | | auth-server.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -102,7 +102,7 @@ Kubernetes: `>=v1.22.0-0` | auth-server.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | auth-server.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | auth-server.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| casa | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/casa","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Janssen Casa ("Casa") is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Janssen Auth Server. | +| casa | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/casa","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Janssen Casa ("Casa") is a self-service web portal for end-users to manage authentication and authorization preferences for their account in a Janssen Auth Server. | | casa.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | casa.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | casa.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -115,7 +115,7 @@ Kubernetes: `>=v1.22.0-0` | casa.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | casa.image.pullSecrets | list | `[]` | Image Pull Secrets | | casa.image.repository | string | `"ghcr.io/janssenproject/jans/casa"` | Image to use for deploying. | -| casa.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| casa.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | casa.livenessProbe | object | `{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for casa if needed. | | casa.livenessProbe.httpGet.path | string | `"/jans-casa/health-check"` | http liveness probe endpoint | | casa.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -133,8 +133,8 @@ Kubernetes: `>=v1.22.0-0` | casa.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | casa.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | casa.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| config | object | `{"additionalAnnotations":{},"additionalLabels":{},"adminPassword":"Test1234#","city":"Austin","configmap":{"cnAwsAccessKeyId":"","cnAwsDefaultRegion":"us-west-1","cnAwsProfile":"janssen","cnAwsSecretAccessKey":"","cnAwsSecretsEndpointUrl":"","cnAwsSecretsNamePrefix":"janssen","cnAwsSecretsReplicaRegions":[],"cnCacheType":"NATIVE_PERSISTENCE","cnConfigKubernetesConfigMap":"cn","cnGoogleProjectId":"google-project-to-save-config-and-secrets-to","cnGoogleSecretManagerServiceAccount":"SWFtTm90YVNlcnZpY2VBY2NvdW50Q2hhbmdlTWV0b09uZQo=","cnGoogleSecretNamePrefix":"janssen","cnGoogleSecretVersionId":"latest","cnJettyRequestHeaderSize":8192,"cnMaxRamPercent":"75.0","cnMessageType":"DISABLED","cnOpaUrl":"http://opa.opa.svc.cluster.cluster.local:8181/v1","cnPersistenceHybridMapping":"{}","cnRedisSentinelGroup":"","cnRedisSslTruststore":"","cnRedisType":"STANDALONE","cnRedisUrl":"redis.redis.svc.cluster.local:6379","cnRedisUseSsl":false,"cnScimProtectionMode":"OAUTH","cnSecretKubernetesSecret":"cn","cnSqlDbDialect":"mysql","cnSqlDbHost":"my-release-mysql.default.svc.cluster.local","cnSqlDbName":"jans","cnSqlDbPort":3306,"cnSqlDbSchema":"","cnSqlDbTimezone":"UTC","cnSqlDbUser":"jans","cnSqldbUserPassword":"Test1234#","cnVaultAddr":"http://localhost:8200","cnVaultAppRolePath":"approle","cnVaultKvPath":"secret","cnVaultNamespace":"","cnVaultPrefix":"jans","cnVaultRoleId":"","cnVaultRoleIdFile":"/etc/certs/vault_role_id","cnVaultSecretId":"","cnVaultSecretIdFile":"/etc/certs/vault_secret_id","cnVaultVerify":false,"kcAdminPassword":"Test1234#","kcAdminUsername":"admin","kcDbPassword":"Test1234#","kcDbSchema":"keycloak","kcDbUrlDatabase":"keycloak","kcDbUrlHost":"mysql.kc.svc.cluster.local","kcDbUrlPort":3306,"kcDbUrlProperties":"?useUnicode=true&characterEncoding=UTF-8&character_set_server=utf8mb4","kcDbUsername":"keycloak","kcDbVendor":"mysql","kcLogLevel":"INFO","lbAddr":"","quarkusTransactionEnableRecovery":true},"countryCode":"US","customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","email":"support@jans.io","image":{"pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/configurator","tag":"0.0.0-nightly"},"lifecycle":{},"orgName":"Janssen","redisPassword":"P@assw0rd","resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"salt":"","state":"TX","usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Configuration parameters for setup and initial configuration secret and config layers used by Janssen services. | -| config-api | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/config-api","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"jans-config-api/api/v1/health/ready","port":8074},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Config Api endpoints can be used to configure the auth-server, which is an open-source OpenID Connect Provider (OP) and UMA Authorization Server (AS). | +| config | object | `{"additionalAnnotations":{},"additionalLabels":{},"adminPassword":"Test1234#","city":"Austin","configmap":{"cnAwsAccessKeyId":"","cnAwsDefaultRegion":"us-west-1","cnAwsProfile":"janssen","cnAwsSecretAccessKey":"","cnAwsSecretsEndpointUrl":"","cnAwsSecretsNamePrefix":"janssen","cnAwsSecretsReplicaRegions":[],"cnCacheType":"NATIVE_PERSISTENCE","cnConfigKubernetesConfigMap":"cn","cnGoogleProjectId":"google-project-to-save-config-and-secrets-to","cnGoogleSecretManagerServiceAccount":"SWFtTm90YVNlcnZpY2VBY2NvdW50Q2hhbmdlTWV0b09uZQo=","cnGoogleSecretNamePrefix":"janssen","cnGoogleSecretVersionId":"latest","cnJettyRequestHeaderSize":8192,"cnMaxRamPercent":"75.0","cnMessageType":"DISABLED","cnOpaUrl":"http://opa.opa.svc.cluster.cluster.local:8181/v1","cnPersistenceHybridMapping":"{}","cnRedisSentinelGroup":"","cnRedisSslTruststore":"","cnRedisType":"STANDALONE","cnRedisUrl":"redis.redis.svc.cluster.local:6379","cnRedisUseSsl":false,"cnScimProtectionMode":"OAUTH","cnSecretKubernetesSecret":"cn","cnSqlDbDialect":"mysql","cnSqlDbHost":"my-release-mysql.default.svc.cluster.local","cnSqlDbName":"jans","cnSqlDbPort":3306,"cnSqlDbSchema":"","cnSqlDbTimezone":"UTC","cnSqlDbUser":"jans","cnSqldbUserPassword":"Test1234#","cnVaultAddr":"http://localhost:8200","cnVaultAppRolePath":"approle","cnVaultKvPath":"secret","cnVaultNamespace":"","cnVaultPrefix":"jans","cnVaultRoleId":"","cnVaultRoleIdFile":"/etc/certs/vault_role_id","cnVaultSecretId":"","cnVaultSecretIdFile":"/etc/certs/vault_secret_id","cnVaultVerify":false,"kcAdminPassword":"Test1234#","kcAdminUsername":"admin","kcDbPassword":"Test1234#","kcDbSchema":"keycloak","kcDbUrlDatabase":"keycloak","kcDbUrlHost":"mysql.kc.svc.cluster.local","kcDbUrlPort":3306,"kcDbUrlProperties":"?useUnicode=true&characterEncoding=UTF-8&character_set_server=utf8mb4","kcDbUsername":"keycloak","kcDbVendor":"mysql","kcLogLevel":"INFO","lbAddr":"","quarkusTransactionEnableRecovery":true},"countryCode":"US","customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","email":"support@jans.io","image":{"pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/configurator","tag":"1.3.0-1"},"lifecycle":{},"orgName":"Janssen","redisPassword":"P@assw0rd","resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"salt":"","state":"TX","usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Configuration parameters for setup and initial configuration secret and config layers used by Janssen services. | +| config-api | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/config-api","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"jans-config-api/api/v1/health/ready","port":8074},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Config Api endpoints can be used to configure the auth-server, which is an open-source OpenID Connect Provider (OP) and UMA Authorization Server (AS). | | config-api.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | config-api.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | config-api.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -147,7 +147,7 @@ Kubernetes: `>=v1.22.0-0` | config-api.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | config-api.image.pullSecrets | list | `[]` | Image Pull Secrets | | config-api.image.repository | string | `"ghcr.io/janssenproject/jans/config-api"` | Image to use for deploying. | -| config-api.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| config-api.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | config-api.livenessProbe | object | `{"httpGet":{"path":"/jans-config-api/api/v1/health/live","port":8074},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | config-api.livenessProbe.httpGet | object | `{"path":"/jans-config-api/api/v1/health/live","port":8074}` | http liveness probe endpoint | | config-api.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -194,14 +194,14 @@ Kubernetes: `>=v1.22.0-0` | config.configmap.cnSqlDbUser | string | `"jans"` | SQL database username. | | config.configmap.cnSqldbUserPassword | string | `"Test1234#"` | SQL password injected the secrets . | | config.configmap.cnVaultAddr | string | `"http://localhost:8200"` | Base URL of Vault. | -| config.configmap.cnVaultAppRolePath | string | `"approle"` | Path to Vault AppRole. | +| config.configmap.cnVaultAppRolePath | string | `"approle"` | Path to the Vault AppRole. | | config.configmap.cnVaultKvPath | string | `"secret"` | Path to Vault KV secrets engine. | | config.configmap.cnVaultNamespace | string | `""` | Vault namespace used to access the secrets. | | config.configmap.cnVaultPrefix | string | `"jans"` | Base prefix name used to access secrets. | | config.configmap.cnVaultRoleId | string | `""` | Vault AppRole RoleID. | -| config.configmap.cnVaultRoleIdFile | string | `"/etc/certs/vault_role_id"` | Path to file contains Vault AppRole role ID. | +| config.configmap.cnVaultRoleIdFile | string | `"/etc/certs/vault_role_id"` | Path to the file that contains Vault AppRole role ID. | | config.configmap.cnVaultSecretId | string | `""` | Vault AppRole SecretID. | -| config.configmap.cnVaultSecretIdFile | string | `"/etc/certs/vault_secret_id"` | Path to file contains Vault AppRole secret ID. | +| config.configmap.cnVaultSecretIdFile | string | `"/etc/certs/vault_secret_id"` | Path to the file that contains Vault AppRole secret ID. | | config.configmap.cnVaultVerify | bool | `false` | Verify connection to Vault. | | config.configmap.kcAdminPassword | string | `"Test1234#"` | Keycloak admin UI password | | config.configmap.kcAdminUsername | string | `"admin"` | Keycloak admin UI username | @@ -224,7 +224,7 @@ Kubernetes: `>=v1.22.0-0` | config.email | string | `"support@jans.io"` | Email address of the administrator usually. Used for certificate creation. | | config.image.pullSecrets | list | `[]` | Image Pull Secrets | | config.image.repository | string | `"ghcr.io/janssenproject/jans/configurator"` | Image to use for deploying. | -| config.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| config.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | config.orgName | string | `"Janssen"` | Organization name. Used for certificate creation. | | config.redisPassword | string | `"P@assw0rd"` | Redis admin password if `config.configmap.cnCacheType` is set to `REDIS`. | | config.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | @@ -239,7 +239,7 @@ Kubernetes: `>=v1.22.0-0` | config.usrEnvs.secret | object | `{}` | Add custom secret envs to the service. variable1: value1 | | config.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | config.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| fido2 | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/fido2","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"service":{"name":"http-fido2","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | FIDO 2.0 (FIDO2) is an open authentication standard that enables leveraging common devices to authenticate to online services in both mobile and desktop environments. | +| fido2 | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/fido2","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}},"service":{"name":"http-fido2","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | FIDO 2.0 (FIDO2) is an open authentication standard that enables leveraging common devices to authenticate to online services in both mobile and desktop environments. | | fido2.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | fido2.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | fido2.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -252,7 +252,7 @@ Kubernetes: `>=v1.22.0-0` | fido2.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | fido2.image.pullSecrets | list | `[]` | Image Pull Secrets | | fido2.image.repository | string | `"ghcr.io/janssenproject/jans/fido2"` | Image to use for deploying. | -| fido2.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| fido2.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | fido2.livenessProbe | object | `{"httpGet":{"path":"/jans-fido2/sys/health-check","port":"http-fido2"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for the fido2 if needed. | | fido2.livenessProbe.httpGet | object | `{"path":"/jans-fido2/sys/health-check","port":"http-fido2"}` | http liveness probe endpoint | | fido2.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -271,7 +271,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":{},"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":{}}},"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":{"linkEnabled":true},"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 | @@ -294,10 +294,13 @@ Kubernetes: `>=v1.22.0-0` | global.auth-server.authSigKeys | string | `"RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512"` | space-separated key algorithm for signing (default to `RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512`) | | global.auth-server.cnCustomJavaOptions | string | `""` | passing custom java options to auth-server. Notice you do not need to pass in any loggers options as they are introduced below in appLoggers. DO NOT PASS JAVA_OPTIONS in envs. | | global.auth-server.enabled | bool | `true` | Boolean flag to enable/disable auth-server chart. You should never set this to false. | -| global.auth-server.ingress | object | `{"authServerAdditionalAnnotations":{},"authServerEnabled":true,"authServerLabels":{},"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":{}}` | Enable endpoints in either istio or nginx ingress depending on users choice | +| global.auth-server.ingress | object | `{"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":{}}` | Enable endpoints in either istio or nginx ingress depending on users choice | | global.auth-server.ingress.authServerAdditionalAnnotations | object | `{}` | Auth server ingress resource additional annotations. | | global.auth-server.ingress.authServerEnabled | bool | `true` | Enable Auth server endpoints /jans-auth | | global.auth-server.ingress.authServerLabels | object | `{}` | Auth server ingress resource labels. key app is taken | +| global.auth-server.ingress.authzenAdditionalAnnotations | object | `{}` | authzen config ingress resource additional annotations. | +| global.auth-server.ingress.authzenConfigEnabled | bool | `true` | Enable endpoint /.well-known/authzen-configuration | +| global.auth-server.ingress.authzenConfigLabels | object | `{}` | authzen config ingress resource labels. key app is taken | | global.auth-server.ingress.deviceCodeAdditionalAnnotations | object | `{}` | device-code ingress resource additional annotations. | | global.auth-server.ingress.deviceCodeEnabled | bool | `true` | Enable endpoint /device-code | | global.auth-server.ingress.deviceCodeLabels | object | `{}` | device-code ingress resource labels. key app is taken | @@ -343,15 +346,17 @@ Kubernetes: `>=v1.22.0-0` | global.casa.ingress.casaEnabled | bool | `false` | Enable casa endpoints /casa | | global.casa.ingress.casaLabels | object | `{}` | Casa ingress resource labels. key app is taken | | global.cloud.testEnviroment | bool | `false` | Boolean flag if enabled will strip resources requests and limits from all services. | -| global.cnConfiguratorConfigurationFile | string | `"/etc/jans/conf/configuration.json"` | Path to configuration schema file | -| 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.cnConfiguratorConfigurationFile | string | `"/etc/jans/conf/configuration.json"` | Path to the configuration schema file | +| global.cnConfiguratorCustomSchema | object | `{"secretName":""}` | 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. | +| global.cnConfiguratorCustomSchema.secretName | string | `""` | The name of the Kubernetes secret used for storing custom configuration schema. | +| global.cnConfiguratorDumpFile | string | `"/etc/jans/conf/configuration.out.json"` | Path to the 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 the file that contains the key to encrypt/decrypt the 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 | | global.cnPrometheusPort | string | `""` | Port used by Prometheus JMX agent (default to empty string). To enable Prometheus JMX agent, set the value to a number. | -| global.cnSqlPasswordFile | string | `"/etc/jans/conf/sql_password"` | Path to SQL password file | +| global.cnSqlPasswordFile | string | `"/etc/jans/conf/sql_password"` | Path to the SQL password file | | global.config-api.appLoggers | object | `{"configApiLogLevel":"INFO","configApiLogTarget":"STDOUT","enableStdoutLogPrefix":"true","persistenceDurationLogLevel":"INFO","persistenceDurationLogTarget":"FILE","persistenceLogLevel":"INFO","persistenceLogTarget":"FILE","scriptLogLevel":"INFO","scriptLogTarget":"FILE"}` | App loggers can be configured to define where the logs will be redirected to and the level of each in which it should be displayed. | | global.config-api.appLoggers.configApiLogLevel | string | `"INFO"` | configapi.log level | | global.config-api.appLoggers.configApiLogTarget | string | `"STDOUT"` | configapi.log target | @@ -385,13 +390,16 @@ Kubernetes: `>=v1.22.0-0` | global.fido2.cnCustomJavaOptions | string | `""` | passing custom java options to fido2. Notice you do not need to pass in any loggers options as they are introduced below in appLoggers. DO NOT PASS JAVA_OPTIONS in envs. | | global.fido2.enabled | bool | `true` | Boolean flag to enable/disable the fido2 chart. | | global.fido2.fido2ServiceName | string | `"fido2"` | Name of the fido2 service. Please keep it as default. | -| global.fido2.ingress | object | `{"fido2AdditionalAnnotations":{},"fido2ConfigAdditionalAnnotations":{},"fido2ConfigEnabled":false,"fido2ConfigLabels":{},"fido2Enabled":false,"fido2Labels":{}}` | Enable endpoints in either istio or nginx ingress depending on users choice | +| global.fido2.ingress | object | `{"fido2AdditionalAnnotations":{},"fido2ConfigAdditionalAnnotations":{},"fido2ConfigEnabled":false,"fido2ConfigLabels":{},"fido2Enabled":false,"fido2Labels":{},"fido2WebauthnAdditionalAnnotations":{},"fido2WebauthnEnabled":false,"fido2WebauthnLabels":{}}` | Enable endpoints in either istio or nginx ingress depending on users choice | | global.fido2.ingress.fido2AdditionalAnnotations | object | `{}` | fido2 ingress resource additional annotations. | | global.fido2.ingress.fido2ConfigAdditionalAnnotations | object | `{}` | fido2 config ingress resource additional annotations. | | global.fido2.ingress.fido2ConfigEnabled | bool | `false` | Enable endpoint /.well-known/fido2-configuration | | global.fido2.ingress.fido2ConfigLabels | object | `{}` | fido2 config ingress resource labels. key app is taken | | global.fido2.ingress.fido2Enabled | bool | `false` | Enable endpoint /jans-fido2 | | global.fido2.ingress.fido2Labels | object | `{}` | fido2 ingress resource labels. key app is taken | +| global.fido2.ingress.fido2WebauthnAdditionalAnnotations | object | `{}` | fido2 webauthn ingress resource additional annotations. | +| global.fido2.ingress.fido2WebauthnEnabled | bool | `false` | Enable endpoint /.well-known/webauthn | +| global.fido2.ingress.fido2WebauthnLabels | object | `{}` | fido2 webauthn ingress resource labels. key app is taken | | global.fqdn | string | `"demoexample.jans.io"` | Fully qualified domain name to be used for Janssen installation. This address will be used to reach Janssen services. | | global.gcePdStorageType | string | `"pd-standard"` | GCE storage kind if using Google disks | | global.isFqdnRegistered | bool | `false` | Boolean flag to enable mapping global.lbIp to global.fqdn inside pods on clouds that provide static ip for load balancers. On cloud that provide only addresses to the LB this flag will enable a script to actively scan config.configmap.lbAddr and update the hosts file inside the pods automatically. | @@ -416,7 +424,9 @@ Kubernetes: `>=v1.22.0-0` | global.link.appLoggers.scriptLogTarget | string | `"FILE"` | cacherefresh_script.log target | | global.link.cnCustomJavaOptions | string | `""` | passing custom java options to link. Notice you do not need to pass in any loggers options as they are introduced below in appLoggers. DO NOT PASS JAVA_OPTIONS in envs. | | global.link.enabled | bool | `false` | Boolean flag to enable/disable the link chart. | -| global.link.ingress | object | `{"linkEnabled":true}` | Enable endpoints in either istio or nginx ingress depending on users choice | +| global.link.ingress | object | `{"linkAdditionalAnnotations":{},"linkEnabled":true,"linkLabels":{}}` | Enable endpoints in either istio or nginx ingress depending on users choice | +| global.link.ingress.linkAdditionalAnnotations | object | `{}` | link ingress resource additional annotations. | +| global.link.ingress.linkLabels | object | `{}` | link ingress resource labels. key app is taken | | global.link.linkServiceName | string | `"link"` | Name of the link service. Please keep it as default. | | global.nginx-ingress.enabled | bool | `true` | Boolean flag to enable/disable the nginx-ingress definitions chart. | | global.persistence.enabled | bool | `true` | Boolean flag to enable/disable the persistence chart. | @@ -444,7 +454,7 @@ Kubernetes: `>=v1.22.0-0` | global.scim.ingress.scimConfigEnabled | bool | `false` | Enable endpoint /.well-known/scim-configuration | | global.scim.ingress.scimConfigLabels | object | `{}` | SCIM config ingress resource labels. key app is taken | | global.scim.ingress.scimEnabled | bool | `false` | Enable SCIM endpoints /jans-scim | -| global.scim.ingress.scimLabels | object | `{}` | SCIM config ingress resource labels. key app is taken | +| global.scim.ingress.scimLabels | object | `{}` | SCIM ingress resource labels. key app is taken | | global.scim.scimServiceName | string | `"scim"` | Name of the scim service. Please keep it as default. | | global.serviceAccountName | string | `"default"` | service account used by Kubernetes resources | | global.storageClass | object | `{"allowVolumeExpansion":true,"allowedTopologies":[],"mountOptions":["debug"],"parameters":{},"provisioner":"microk8s.io/hostpath","reclaimPolicy":"Retain","volumeBindingMode":"WaitForFirstConsumer"}` | StorageClass section. This is not currently used by the openbanking distribution. You may specify custom parameters as needed. | @@ -452,7 +462,7 @@ Kubernetes: `>=v1.22.0-0` | global.usrEnvs | object | `{"normal":{},"secret":{}}` | Add custom normal and secret envs to the service. Envs defined in global.userEnvs will be globally available to all services | | global.usrEnvs.normal | object | `{}` | Add custom normal envs to the service. variable1: value1 | | global.usrEnvs.secret | object | `{}` | Add custom secret envs to the service. variable1: value1 | -| kc-scheduler | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/kc-scheduler","tag":"0.0.0-nightly"},"interval":10,"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for synchronizing Keycloak SAML clients | +| kc-scheduler | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/kc-scheduler","tag":"1.3.0-1"},"interval":10,"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Responsible for synchronizing Keycloak SAML clients | | kc-scheduler.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | kc-scheduler.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | kc-scheduler.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -462,7 +472,7 @@ Kubernetes: `>=v1.22.0-0` | kc-scheduler.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | kc-scheduler.image.pullSecrets | list | `[]` | Image Pull Secrets | | kc-scheduler.image.repository | string | `"ghcr.io/janssenproject/jans/kc-scheduler"` | Image to use for deploying. | -| kc-scheduler.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| kc-scheduler.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | kc-scheduler.interval | int | `10` | Interval of running the scheduler (in minutes) | | kc-scheduler.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | | kc-scheduler.resources.limits.cpu | string | `"300m"` | CPU limit. | @@ -474,7 +484,7 @@ Kubernetes: `>=v1.22.0-0` | kc-scheduler.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | kc-scheduler.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | kc-scheduler.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| link | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/link","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Link. | +| link | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/link","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Link. | | link.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | link.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | link.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -487,7 +497,7 @@ Kubernetes: `>=v1.22.0-0` | link.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | link.image.pullSecrets | list | `[]` | Image Pull Secrets | | link.image.repository | string | `"ghcr.io/janssenproject/jans/link"` | Image to use for deploying. | -| link.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| link.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | link.livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | link.livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | http liveness probe endpoint | | link.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -508,7 +518,7 @@ Kubernetes: `>=v1.22.0-0` | nginx-ingress.ingress.additionalAnnotations | object | `{}` | Additional annotations that will be added across all ingress definitions in the format of {cert-manager.io/issuer: "letsencrypt-prod"} Enable client certificate authentication nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" Create the secret containing the trusted ca certificates nginx.ingress.kubernetes.io/auth-tls-secret: "janssen/tls-certificate" Specify the verification depth in the client certificates chain nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1" Specify if certificates are passed to upstream server nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true" | | nginx-ingress.ingress.additionalLabels | object | `{}` | Additional labels that will be added across all ingress definitions in the format of {mylabel: "myapp"} | | nginx-ingress.ingress.tls | list | `[{"hosts":["demoexample.jans.io"],"secretName":"tls-certificate"}]` | Secrets holding HTTPS CA cert and key. | -| persistence | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/persistence-loader","tag":"0.0.0-nightly"},"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Job to generate data and initial config for Janssen Server persistence layer. | +| persistence | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/persistence-loader","tag":"1.3.0-1"},"lifecycle":{},"resources":{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | Job to generate data and initial config for Janssen Server persistence layer. | | persistence.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | persistence.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | persistence.customCommand | list | `[]` | Add custom job's command. If passed, it will override the default conditional command. | @@ -518,7 +528,7 @@ Kubernetes: `>=v1.22.0-0` | persistence.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | persistence.image.pullSecrets | list | `[]` | Image Pull Secrets | | persistence.image.repository | string | `"ghcr.io/janssenproject/jans/persistence-loader"` | Image to use for deploying. | -| persistence.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| persistence.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | persistence.resources | object | `{"limits":{"cpu":"300m","memory":"300Mi"},"requests":{"cpu":"300m","memory":"300Mi"}}` | Resource specs. | | persistence.resources.limits.cpu | string | `"300m"` | CPU limit | | persistence.resources.limits.memory | string | `"300Mi"` | Memory limit. | @@ -529,7 +539,7 @@ Kubernetes: `>=v1.22.0-0` | persistence.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | persistence.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | persistence.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| saml | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/saml","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | SAML. | +| saml | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/saml","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"500m","memory":"1200Mi"},"requests":{"cpu":"500m","memory":"1200Mi"}},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | SAML. | | saml.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | saml.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | saml.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -542,7 +552,7 @@ Kubernetes: `>=v1.22.0-0` | saml.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | saml.image.pullSecrets | list | `[]` | Image Pull Secrets | | saml.image.repository | string | `"ghcr.io/janssenproject/jans/saml"` | Image to use for deploying. | -| saml.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| saml.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | saml.livenessProbe | object | `{"exec":{"command":["python3","/app/scripts/healthcheck.py"]},"failureThreshold":10,"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for the auth server if needed. | | saml.livenessProbe.exec | object | `{"command":["python3","/app/scripts/healthcheck.py"]}` | http liveness probe endpoint | | saml.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -559,7 +569,7 @@ Kubernetes: `>=v1.22.0-0` | saml.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | saml.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | saml.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | -| scim | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/scim","tag":"0.0.0-nightly"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"service":{"name":"http-scim","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | System for Cross-domain Identity Management (SCIM) version 2.0 | +| scim | object | `{"additionalAnnotations":{},"additionalLabels":{},"customCommand":[],"customScripts":[],"dnsConfig":{},"dnsPolicy":"","hpa":{"behavior":{},"enabled":true,"maxReplicas":10,"metrics":[],"minReplicas":1,"targetCPUUtilizationPercentage":50},"image":{"pullPolicy":"IfNotPresent","pullSecrets":[],"repository":"ghcr.io/janssenproject/jans/scim","tag":"1.3.0-1"},"lifecycle":{},"livenessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5},"pdb":{"enabled":true,"maxUnavailable":"90%"},"readinessProbe":{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5},"replicas":1,"resources":{"limits":{"cpu":"1000m","memory":"1200Mi"},"requests":{"cpu":"1000m","memory":"1200Mi"}},"service":{"name":"http-scim","port":8080},"topologySpreadConstraints":{},"usrEnvs":{"normal":{},"secret":{}},"volumeMounts":[],"volumes":[]}` | System for Cross-domain Identity Management (SCIM) version 2.0 | | scim.additionalAnnotations | object | `{}` | Additional annotations that will be added across the gateway in the format of {cert-manager.io/issuer: "letsencrypt-prod"} | | scim.additionalLabels | object | `{}` | Additional labels that will be added across the gateway in the format of {mylabel: "myapp"} | | scim.customCommand | list | `[]` | Add custom pod's command. If passed, it will override the default conditional command. | @@ -572,7 +582,7 @@ Kubernetes: `>=v1.22.0-0` | scim.image.pullPolicy | string | `"IfNotPresent"` | Image pullPolicy to use for deploying. | | scim.image.pullSecrets | list | `[]` | Image Pull Secrets | | scim.image.repository | string | `"ghcr.io/janssenproject/jans/scim"` | Image to use for deploying. | -| scim.image.tag | string | `"0.0.0-nightly"` | Image tag to use for deploying. | +| scim.image.tag | string | `"1.3.0-1"` | Image tag to use for deploying. | | scim.livenessProbe | object | `{"httpGet":{"path":"/jans-scim/sys/health-check","port":8080},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the liveness healthcheck for SCIM if needed. | | scim.livenessProbe.httpGet.path | string | `"/jans-scim/sys/health-check"` | http liveness probe endpoint | | scim.pdb | object | `{"enabled":true,"maxUnavailable":"90%"}` | Configure the PodDisruptionBudget | @@ -591,6 +601,3 @@ Kubernetes: `>=v1.22.0-0` | scim.usrEnvs.secret | object | `{}` | Add custom secret envs to the service variable1: value1 | | scim.volumeMounts | list | `[]` | Configure any additional volumesMounts that need to be attached to the containers | | scim.volumes | list | `[]` | Configure any additional volumes that need to be attached to the pod | - ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/docs/janssen-server/reference/learning-reference.md b/docs/janssen-server/reference/learning-reference.md index 28cf9f0e483..5d73a9d824a 100644 --- a/docs/janssen-server/reference/learning-reference.md +++ b/docs/janssen-server/reference/learning-reference.md @@ -20,7 +20,6 @@ help us keep it updated by raising a PR. - [An intro to Janssen IDP Terraform Provider - Part1](https://medium.com/@moabu/an-intro-to-janssen-idp-terraform-provider-part-1-cff350526f17) - [An intro to Janssen IDP Terraform Provider - Part2](https://medium.com/@moabu/an-intro-to-janssen-idp-terraform-provider-part-2-basic-example-d88c48607293) -- [Jans-tent to test Single-Sign-On with your Authorization Server](https://medium.com/@imshakil/jans-tent-to-test-single-sign-on-with-your-authorization-server-d153c873c5d1) - [Janssen + Mod Auth openidc module to Test OpenID Connect Single Sign-on (SSO) and Single Single Logout (SLO)](https://medium.com/@imshakil/janssen-mod-auth-openidc-module-to-test-openid-connect-single-sign-on-sso-and-single-single-48fcd7b894b7) - [Enhancing Secure Mobile Authentication with OAuth, Dynamic Client Registration, and DPoP](https://medium.com/@arnab.bdutta/dcr-attestation-and-dpop-for-first-party-native-applications-3e86a837196e) diff --git a/docs/janssen-server/usermgmt/usermgmt-scim.md b/docs/janssen-server/usermgmt/usermgmt-scim.md index e3afa2275ec..618dee88c42 100644 --- a/docs/janssen-server/usermgmt/usermgmt-scim.md +++ b/docs/janssen-server/usermgmt/usermgmt-scim.md @@ -64,386 +64,50 @@ Please see [here](https://docs.jans.io/v1.0.14/admin/scim/logs/) besides To know more about OAuth protection mode please visit [here](https://docs.jans.io/v1.0.14/admin/scim/oauth-protection/). The SCIM API endpoints are by default protected by (Bearer) OAuth 2.0 tokens. Depending on the operation, these tokens must have certain scopes for the operations to be authorized. We need a client to get Bearer token. ### Get SCIM Client -Let's obtain the credentials of this client first. In TUI, navigate to `Auth Server > Clients`. In the search field type SCIM (uppercase). Highlight the row that matches a client named "SCIM Client" and press Enter. To see in `JSON` formate please press `d`. -From the "Basic" section, grab the "client id" and "client secret". This secret is encrypted, to decrypt it, in a terminal run `/opt/jans/bin/encode.py -D ENCRYPTED-SECRET-HERE`. + +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#get-scim-client) for this topic + ### Get Access token -Let's get a token, -``` -curl -k -u 'CLIENT_ID:DECRYPTED_CLIENT_SECRET' -k -d grant_type=client_credentials -d scope='https://jans.io/scim/users.read https://jans.io/scim/users write' https:///jans-auth/restv1/token > /tmp/token.json -``` -In response `token.json` we will get `access_token` -``` -{ -"access_token":"11a76589-7955-4247-9ca5-f3ad7884305...", -"scope":"https://jans.io/scim/users.read", -"token_type":"Bearer", -"expires_in":299 -} -``` +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#get-access-token) for this topic ### Retrive existing User -To get an existing user - -``` -curl -k -G -H 'Authorization: Bearer ACCESS_TOKEN' --data-urlencode 'filter=displayName co "Admin"' https:///jans-scim/restv1/v2/Users > /tmp/user.json -``` -In response `user.json` we will get -``` -{ - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:ListResponse" - ], - "totalResults": 1, - "startIndex": 1, - "itemsPerPage": 1, - "Resources": [ - { - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "5fdbb720-a1fd-477f-af92-b7c054f02c98", - "meta": { - "resourceType": "User", - "created": "2023-06-12T14:54:09.531Z", - "location": "https://raju.jans13.me/jans-scim/restv1/v2/Users/5fdbb720-a1fd-477f-af92-b7c054f02c98" - }, - "userName": "admin", - "name": { - "familyName": "...", - "givenName": "...", - "middleName": "...", - "formatted": "..." - }, - "displayName": "Admin", - "active": true, - "emails": [ - { - "value": "example@gluu.org", - "primary": false - } - ], - "groups": [ - { - "value": "60B7", - "display": "Jannsen Manager Group", - "type": "direct", - "$ref": "https://raju.jans13.me/jans-scim/restv1/v2/Groups/60B7" - } - ] - } - ] -} -``` - +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#retrive-existing-user) for this topic ## Creating Resource ### Create an User -Let's start creating a dummy user. A client sends a POST request containing a "User" to the "/Users" endpoint. -``` -POST /Users HTTP/1.1 -Host: example.com -Accept: application/scim+json -Content-Type: application/scim+json -Authorization: Bearer h480djs93hd8.. -Content-Length: ... - -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "userName": "bjensen", - "externalId": "bjensen", - "name": { - "formatted": "Ms. Barbara J Jensen III", - "familyName": "Jensen", - "givenName": "Barbara" - } -} -``` -Open a text editor and copy paste the json body, name as `input.json`. -Hit on your terminal with bellow command. -``` -curl -k -H 'Authorization: Bearer ACCESS_TOKEN' -H 'Content-Type: application/scim+json' -d @input.json -o output.json https:///jans-scim/restv1/v2/Users -``` -response looks like -``` -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "e3009115-b890-4d8b-bd63-bbfef34aa583", - "externalId": "bjensen", - "meta": { - "resourceType": "User", - "created": "2023-06-26T19:43:32.945Z", - "lastModified": "2023-06-26T19:43:32.945Z", - "location": "https://raju.jans13.me/jans-scim/restv1/v2/Users/e3009115-b890-4d8b-bd63-bbfef34aa583" - }, - "userName": "bjensen", - "name": { - "familyName": "Jensen", - "givenName": "Barbara", - "formatted": "Ms. Barbara J Jensen III" - } -} -``` - -This new user has been given an `id`. If possible, inspect your `ou=people` branch and find the entry whose `inum` matches the `id` given. An easier option would be to via **Jans TUI** and go to `Users` and search "bjensen" to see the recently created user. -### Updating a User(PUT) +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#create-an-user) for this topic -Overwrite your `input.json` with the following. Replace content in angle brackets accordingly: - -``` -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "e3009115-b890-4d8b-bd63-bbfef34aa583", - "userName": "bjensen", - "externalId": "bjensen", - "name": { - "formatted": "Ms. Barbara J Jensen III", - "familyName": "Jensen", - "givenName": "Barbara" - }, - "displayName": "Jensen Barbara", - "emails": [ - { - "value": "jensen@example.com", - "type": "work", - "primary": true - } - ] -} -``` - -PUT with curl: - -``` -curl -k -X PUT -H 'Authorization: Bearer ACCESS_TOKEN' -H 'Content-Type: application/scim+json' -d @input.json -o output.json https:///jans-scim/restv1/v2/Users/ -``` - -Response `(output.json)` will show the same contents of a full retrieval. - -Please verify changes were applied whether by inspecting LDAP or issuing a GET. If you have followed the steps properly, you should notice a new e-mail added and the change in `displayName` attribute +### Updating a User(PUT) +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#updating-a-userput) for this topic ### Updating a User (PATCH) -With patching, you can be very precise about the modifications you want to apply. Patching syntax follows JSON Patch spec (RFC 6902) closely. While it's not a must to read the RFC to learn how patch works, see section 3.5.2 of SCIM protocol (RFC 7644) to get the grasp. - -If you prefer reading code, [patch test cases](https://github.com/JanssenProject/jans/tree/main/jans-scim/client/src/test/java/io/jans/scim2/client/patch) found in the Java scim-client project are worth to look at. - -The following is a simple example that illustrates the kind of modifications developers can achieve via `PATCH`. Overwrite your `input.json` with the following: - -``` -{ - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:PatchOp" - ], - "Operations": [ - { - "op": "replace", - "value": { - "name": { - "givenName": "Joey" - } - } - }, - { - "op": "replace", - "path": "emails[type eq \"work\" or primary eq false].value", - "value": "jensen@example.com" - }, - { - "op": "add", - "value": { - "name": { - "middleName": "Jhon" - } - } - }, - { - "op": "add", - "value": { - "emails": [ - { - "primary": true, - "value": "my@own.mail" - } - ], - "phoneNumbers": [ - { - "type": "home", - "value": "5 123 8901" - }, - { - "value": "5 123 8902" - } - ] - } - }, - { - "op": "remove", - "path": "name.middleName" - }, - { - "op": "remove", - "path": "phoneNumbers[value ew \"01\"].type" - } - ] -} -``` - -A collection of modification are provided under "Operations". They are processed in order of appearance. Also, every operation has a type; patching supports add, remove and replace. - -The first operations states the following: replace the value of `givenName` subattribute (that belongs to complex attribute `name`) with the string "Joey". - -Operations are easier to understand when using a "path". The second operation replaces the value subattribute inside the complex multi-valued attribute emails. Inside the square brackets, we find a filter expression, so the replacement does not apply to all emails in the list but only to those matching the criterion. - -So the second operation can be read as "set the value of value subattribute to string `jensen@example.com` where the type subattribute of the `email` equals to string "work" or if primary attribute is false". - -The third operation is similar to the first. It sets the value of a subattribute which was unassigned (null). You could have used "replace" operation in this case and results would have been identical. - -The fourth operation is more interesting. It adds to the current list of emails a new one. It supplies a couple of subattributes for the email to include: primary and value. Additionally, we set the value of (previously unassigned) phoneNumbers multi-valued attribute passing a list of elements. - -In the fifth operation, we remove the `middleName` attribute that was set in operation three. Note how we make explicit the path of data to nullify: "name.middleName". - -The sixth operation allows us to remove a specific subattribute of `phoneNumbers`. The aim is to nullify the "type" of the item whose phone number value ends with "01". The remove operation can also be used to remove a complete item from a list, or empty the whole list by providing a suitable value for "path". - -Now let's see it in action: - - -``` -curl -k -X PATCH -H 'Authorization: Bearer ACCESS_TOKEN' -H 'Content-Type: application/scim+json' -d @input.json -o output.json https:///jans-scim/restv1/v2/Users/ -``` - -So far our resource look like this - -``` -{ - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "id": "e3009115-b890-4d8b-bd63-bbfef34aa583", - "externalId": "bjensen", - "meta": { - "resourceType": "User", - "created": "2023-06-26T19:43:32.945Z", - "lastModified": "2023-06-26T22:34:27.465Z", - "location": "https://raju.jans13.me/jans-scim/restv1/v2/Users/e3009115-b890-4d8b-bd63-bbfef34aa583" - }, - "userName": "bjensen", - "name": { - "familyName": "Jensen", - "givenName": "Joey", - "formatted": "Ms. Barbara J Jensen III" - }, - "displayName": "Jensen Barbara", - "active": false, - "emails": [ - { - "value": "my@own.mail", - "primary": true - }, - { - "value": "jensen@example.com", - "type": "work", - "primary": false - } - ], - "phoneNumbers": [ - { - "value": "5 123 8901" - }, - { - "value": "5 123 8902" - } - ] -} -``` - -Note the primary subattribute accompanying `email` "my@own.mail" is false but when inserted we provided `true`. This is because the SCIM specification states that after modifications are applied to resources **(PUT or PATCH)**, there cannot be more than one item in a multi-valued attribute with primary value set as `true`. - -To see more sample `JSON` payloads, check the `.json` files used by the scim-client test cases referenced above. - -### Deleting Users -For deleting, the `DELETE `method of `HTTP` is used. +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#updating-a-user-patch) for this topic -No input file is used in this case. A delete request could be the following: -``` -curl -k -X DELETE -H 'Authorization: Bearer ACCESS_TOKEN' https:///jans-scim/restv1/v2/Users/ -``` - -Use the inum of our dummy user, **Jensen Barbara**. - -Check your LDAP or via Jans TUI to see that **Bjensen** is gone. +### Deleting Users +You can refer to [here](../../janssen-server/config-guide/scim-config/user-config.md#deleting-users) for this topic ## How is SCIM data stored? -SCIM [schema spec](https://datatracker.ietf.org/doc/html/rfc7643) does not use LDAP attribute names but a different naming convention for resource attributes (note this is not the case of custom attributes where the SCIM name used is that of the LDAP attribute). - -It is possible to determine if a given LDAP attribute is being mapped to a SCIM attribute. For that you need to check in Jans TUI `Auth-Server >> Attributes` and click on any attributes. Check `Include in SCIM Extension:` is `true` or `false`. Whenever you try to map any LDAP attribute to a SCIM attribute keep it's value `true`. - +You can refer to [here](../../janssen-server/scim/monitoring.md#how-is-scim-data-stored) for this topic ## FIDO Devices -A FIDO device represents a user credential stored in the Jans Server database that is compliant with the [FIDO](https://fidoalliance.org/) standard. These devices are used as a second factor in a setting of strong authentication. - -FIDO devices were superseded by [FIDO 2](#fido-2-devices) devices in Jans Server. +You can refer to [here](../../janssen-server/fido/monitoring.md#fido-devices) for this topic. ## FIDO 2 devices -FIDO 2 devices are credentials that adhere to the more current Fido 2.0 initiative (WebAuthn + CTAP). Examples of FIDO 2 devices are USB security keys and Super Gluu devices. - -The SCIM endpoints for FIDO 2 allow application developers to query, update and delete already existing devices. Addition of devices do not take place through the service since this process requires direct end-user interaction, ie. device enrolling. - -The schema attributes for a device of this kind can be found by hitting the URL `https:///jans-scim/restv1/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Fido2Device` - -To distinguish between regular FIDO2 and SuperGluu devices, note only SuperGluu entries have the attribute `deviceData` populated (i.e. not null) - -### Example: Querying Enrolled Devices - -Say we are interested in having a list of Super Gluu devices users have enrolled and whose operating system is iOS. We may issue a query like this: - -``` -curl -k -G -H 'Authorization: Bearer ACCESS_TOKEN' --data-urlencode -'filter=deviceData co "ios"' -d count=10 https:///jans-scim/restv1/v2/Fido2Devices -``` - -The response will be like: - -``` -{ - "totalResults": ..., - "itemsPerPage": ..., - "startIndex": 1, - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:ListResponse" - ], - "Resources": [ - { - "id": "...", - "meta": {...}, - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Fido2Device"], - "userId": "...", - ... - "deviceData": "{...}", - "displayName": ..., - }, - ... - ] -} -``` +You can refer to [here](../../janssen-server/fido/monitoring.md#fido2-devices) for this topic. ## Potential performance issues with Group endpoints diff --git a/docs/janssen-server/vm-ops/jans-command.md b/docs/janssen-server/vm-ops/jans-command.md new file mode 100644 index 00000000000..dc3607af5f0 --- /dev/null +++ b/docs/janssen-server/vm-ops/jans-command.md @@ -0,0 +1,167 @@ +--- +tags: +- administration +- vm +- operations +- jans wrapper command +--- + + +# Jans Command Overview + +The `jans` command is a top-level wrapper script for managing the Janssen Server. +This guide provides an overview of its usage and available commands. List of +available commands may change as more commands are added. To see the current +list of commands available in your installation, run following command at the +Janssen Server: + +```bash title="Command" +jans +``` + +## Available Commands + +### Version and build information + +Displays the version and build information of the currently installed +Janssen Server. + +```bash title="Command" +jans version +``` +![](../../assets/jans_version.png) + + +### CLI + +Invokes the Janssen Command-Line Interface. + +```bash title="Command" +jans cli +``` + +### TUI + +Launches the text-based user interface for Janssen. + +```bash title="Command" +jans tui +``` + +### Logs + +Shows the log file paths for various Janssen Server modules. + +```bash title="Command" +jans logs +``` +![](../../assets/jans_logs.png) + + +### Status + +Displays the status of Janssen Server module services. + +```bash title="Command" +jans status +``` +![](../../assets/jans_status.png) + + +### Start + +Starts services for the Janssen Server. + +```bash title="Command" + jans start +``` +![image](../../assets/jans_start.png) + +Start a specific service. + +```bash title="sample command" +jans start -service=jans-config-api +``` + +```title="Sample Output" +Executing sudo systemctl start jans-config-api +``` + + +### Stop + +Stops services for the Janssen Server. +```bash title="Command" +jans stop +``` + +![](../../assets/jans_stop.png) + +Stop a specific service. + +```bash title="Command" +jans stop -service=jans-config-api +``` +```title="Sample Output" +Executing sudo systemctl stop jans-config-api +``` + + + + +### Restart + +Restart services for the Janssen Server. + +```bash title="Command" +jans restart +``` +![](../../assets/jans_restart.png) + +Restart a specific service. + +```bash title="Sample Command" +jans restart -service=jans-config-api +``` + +```title="Sample Output" +Executing sudo systemctl restart jans-config-api +``` + + + +### Health + +Retrieves health status from the Janssen services health-check endpoint. + +```bash title="Command" +jans health +``` +![image](../../assets/jans_health.png) + +Health check for specific service. + +```bash title="Command" +jans health -service= +``` + +```title="sample Output" +Checking health status for jans-config-api + Executing curl -s http://localhost:8074/jans-config-api/api/v1/health/live + Command output: {"name":"jans-config-api liveness","status":"UP"} +``` + + + +### Info + +Lists important URLs such as `.well-known` endpoints. + +```bash title="Command" +jans info +``` +![](../../assets/jans_info.png) + + + + diff --git a/docs/script-catalog/authorization_challenge/AgamaChallenge.java b/docs/script-catalog/authorization_challenge/AgamaChallenge.java new file mode 100644 index 00000000000..6c272dff776 --- /dev/null +++ b/docs/script-catalog/authorization_challenge/AgamaChallenge.java @@ -0,0 +1,338 @@ +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.as.server.authorize.ws.rs.AuthzRequest; +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"); + + AuthzRequest authRequest = context.getAuthzRequest(); + + if (!authRequest.isUseAuthorizationChallengeSession()) + return makeMissingParamError(context, "Please set 'use_auth_session=true' in your request"); + + ServletRequest servletRequest = context.getHttpRequest(); + AuthorizationChallengeSession deviceSessionObject = authRequest.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(); + } + + @Override + public void prepareAuthzRequest(Object scriptContext) { + + ExternalScriptContext context = (ExternalScriptContext) scriptContext; + AuthzRequest authRequest = context.getAuthzRequest(); + + AuthorizationChallengeSession sessionObject = authRequest.getAuthorizationChallengeSessionObject(); + if (sessionObject != null) { + Map sessionAttributes = sessionObject.getAttributes().getAttributes(); + + // set scope from session into request object + String scopeFromSession = sessionAttributes.get("scope"); + if (StringUtils.isNotBlank(scopeFromSession) && StringUtils.isBlank(authRequest.getScope())) { + authRequest.setScope(scopeFromSession); + } + } + } + +} 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/consent_gathering/sample-script/ConsentGatheringSample.py b/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py index 40576be19b1..f0a48a0a49d 100644 --- a/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py +++ b/docs/script-catalog/consent_gathering/sample-script/ConsentGatheringSample.py @@ -30,7 +30,7 @@ def destroy(self, configurationAttributes): return True def getApiVersion(self): - return 1 + return 11 # Main consent-gather method. Must return True (if gathering performed successfully) or False (if fail). # All user entered values can be access via Map context.getPageAttributes() 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..668d2deca51 100644 --- a/jans-auth-server/agama/engine/pom.xml +++ b/jans-auth-server/agama/engine/pom.xml @@ -9,7 +9,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 ../../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/agama/inboundID/pom.xml b/jans-auth-server/agama/inboundID/pom.xml index 95dd3a28b2d..86c6792934a 100644 --- a/jans-auth-server/agama/inboundID/pom.xml +++ b/jans-auth-server/agama/inboundID/pom.xml @@ -18,7 +18,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 ../../pom.xml diff --git a/jans-auth-server/agama/model/pom.xml b/jans-auth-server/agama/model/pom.xml index 2c9a19fe788..650bbcda8b1 100644 --- a/jans-auth-server/agama/model/pom.xml +++ b/jans-auth-server/agama/model/pom.xml @@ -9,7 +9,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 ../../pom.xml diff --git a/jans-auth-server/client/pom.xml b/jans-auth-server/client/pom.xml index e97a272ee7f..46e3fc9a2a1 100644 --- a/jans-auth-server/client/pom.xml +++ b/jans-auth-server/client/pom.xml @@ -8,7 +8,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SelectAccountHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SelectAccountHttpTest.java index 5f1788fb3d0..7e93a231e14 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SelectAccountHttpTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SelectAccountHttpTest.java @@ -20,7 +20,6 @@ import io.jans.as.model.jwt.JwtHeaderName; import org.apache.logging.log4j.util.Strings; import org.json.JSONArray; -import org.openqa.selenium.htmlunit.HtmlUnitDriver; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.Parameters; @@ -42,7 +41,7 @@ public class SelectAccountHttpTest extends BaseTest { @BeforeTest public void setUp() { - driver = new HtmlUnitDriver(true); + startSelenium(); pageConfig = newPageConfig(driver); } diff --git a/jans-auth-server/common/pom.xml b/jans-auth-server/common/pom.xml index e6445e1c69a..eb9267f0a56 100644 --- a/jans-auth-server/common/pom.xml +++ b/jans-auth-server/common/pom.xml @@ -8,7 +8,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-auth-server/model/pom.xml b/jans-auth-server/model/pom.xml index 8a740c09428..f0f2ec88416 100644 --- a/jans-auth-server/model/pom.xml +++ b/jans-auth-server/model/pom.xml @@ -8,7 +8,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-auth-server/persistence-model/pom.xml b/jans-auth-server/persistence-model/pom.xml index 598f9b7bb59..9e1fe618953 100644 --- a/jans-auth-server/persistence-model/pom.xml +++ b/jans-auth-server/persistence-model/pom.xml @@ -4,7 +4,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 jans-auth-persistence-model Persistence model diff --git a/jans-auth-server/pom.xml b/jans-auth-server/pom.xml index 05999ad6882..5850e5200c8 100644 --- a/jans-auth-server/pom.xml +++ b/jans-auth-server/pom.xml @@ -5,7 +5,7 @@ io.jans jans-auth-server-parent pom - 0.0.0-nightly + 1.3.0 Jans authentication server parent diff --git a/jans-auth-server/server-fips/pom.xml b/jans-auth-server/server-fips/pom.xml index 207baf86397..e066e58e98a 100644 --- a/jans-auth-server/server-fips/pom.xml +++ b/jans-auth-server/server-fips/pom.xml @@ -9,7 +9,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 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/pom.xml b/jans-auth-server/server/pom.xml index 6c08b7e9434..a679cd37894 100644 --- a/jans-auth-server/server/pom.xml +++ b/jans-auth-server/server/pom.xml @@ -8,7 +8,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 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 2cc8ece3184..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,7 +160,7 @@ 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) { @@ -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/authorize/ws/rs/AuthorizeAction.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java index a5a96ce2de4..9433e274ce3 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java @@ -86,6 +86,7 @@ @Named public class AuthorizeAction { + public static final String UNKNOWN = "Unknown"; @Inject private Logger log; @@ -985,10 +986,28 @@ public String getClientDisplayName() { log.trace("client {}", clientId); if (StringUtils.isBlank(clientId)) { - return "Unknown"; + return UNKNOWN; } final Client client = clientService.getClient(clientId); + return getCheckedClientDisplayName(client); + } + + public String getClientDisplayName(final Client client) { + log.trace("client {}", client); + + if (client == null) { + return UNKNOWN; + } + + return getCheckedClientDisplayName(client); + } + + private String getCheckedClientDisplayName(final Client client) { + if (client == null) { + return UNKNOWN; + } + if (StringUtils.isNotBlank(client.getClientName())) { return client.getClientName(); } @@ -997,8 +1016,8 @@ public String getClientDisplayName() { return client.getClientId(); } - return "Unknown"; - } + return UNKNOWN; + } public String getAuthReqId() { return authReqId; 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 36700006f0e..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; @@ -134,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-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml index e8b03eafc30..57a4ac12f27 100644 --- a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml +++ b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml @@ -46,7 +46,7 @@ + value="#{authorizeAction.getClientDisplayName(client)}" />

@@ -56,8 +56,8 @@
  • - +
    @@ -132,10 +132,6 @@
  • -
    - #{msgs['common.copyright']} Gluu. - #{msgs['common.allRightsReserved']} -
    diff --git a/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml b/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml index a3dfe21e879..c17d933890d 100644 --- a/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml +++ b/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml @@ -7,6 +7,7 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" template="/WEB-INF/incl/layout/authorize-extended-template.xhtml"> + diff --git a/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml b/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml index 47c99994584..6fb7f4fd943 100644 --- a/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml +++ b/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml @@ -7,6 +7,7 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" template="/WEB-INF/incl/layout/authorize-extended-template.xhtml"> + diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java index aeb3cdb8d41..800133aa225 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java @@ -10,6 +10,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.io.File; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -86,7 +88,9 @@ public static void init() { @Produces @ApplicationScoped public StringEncrypter getStringEncrypter() throws EncryptionException { - FileConfiguration cryptoConfiguration = new FileConfiguration(".\\target\\conf\\salt"); + String saltFilePath = Paths.get(Paths.get("").toAbsolutePath().toString(), "target/conf/salt").toAbsolutePath().toString(); + FileConfiguration cryptoConfiguration = new FileConfiguration(saltFilePath); + String encodeSalt = cryptoConfiguration.getString("encodeSalt"); return StringEncrypter.instance(encodeSalt); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java index f437694ee64..e29bbd3476f 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Properties; @@ -87,7 +88,8 @@ public static void init() { @Produces @ApplicationScoped public StringEncrypter getStringEncrypter() throws EncryptionException { - FileConfiguration cryptoConfiguration = new FileConfiguration(".\\target\\conf\\salt"); + String saltFilePath = Paths.get(Paths.get("").toAbsolutePath().toString(), "target/conf/salt").toAbsolutePath().toString(); + FileConfiguration cryptoConfiguration = new FileConfiguration(saltFilePath); String encodeSalt = cryptoConfiguration.getString("encodeSalt"); return StringEncrypter.instance(encodeSalt); @@ -295,7 +297,7 @@ public void findUserAfterAddAuthenticator() { assertNotNull(user.getAuthenticator()); assertNotNull(user.getAuthenticator().getAuthenticators()); assertEquals(user.getAuthenticator().getAuthenticators().size(), 1); - assertEquals(userAuthenticatorService.getUserAuthenticatorsByType(user, "type1"), Arrays.asList(new UserAuthenticator("id2", "type2"))); + assertEquals(userAuthenticatorService.getUserAuthenticatorsByType(user, "type2"), Arrays.asList(new UserAuthenticator("id2", "type2"))); } diff --git a/jans-auth-server/static/pom.xml b/jans-auth-server/static/pom.xml index 204957fcbda..1bff845966b 100644 --- a/jans-auth-server/static/pom.xml +++ b/jans-auth-server/static/pom.xml @@ -3,7 +3,7 @@ io.jans jans-auth-static - 0.0.0-nightly + 1.3.0 jar jans-auth-static @@ -11,7 +11,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-auth-server/test-model/pom.xml b/jans-auth-server/test-model/pom.xml index 28bea7579e7..bbe19fe18ca 100644 --- a/jans-auth-server/test-model/pom.xml +++ b/jans-auth-server/test-model/pom.xml @@ -8,7 +8,7 @@ io.jans jans-auth-server-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-bom/pom.xml b/jans-bom/pom.xml index 2015b451200..3eb4552bbbe 100644 --- a/jans-bom/pom.xml +++ b/jans-bom/pom.xml @@ -5,7 +5,7 @@ jans-bom pom jans-bom - 0.0.0-nightly + 1.3.0 @@ -424,7 +424,7 @@ diff --git a/jans-casa/agama/pom.xml b/jans-casa/agama/pom.xml index 309a5ac4b9d..cb8c42b8b7d 100644 --- a/jans-casa/agama/pom.xml +++ b/jans-casa/agama/pom.xml @@ -8,7 +8,7 @@ io.jans casa-base - 0.0.0-nightly + 1.3.0 diff --git a/jans-casa/agama/project/project.json b/jans-casa/agama/project/project.json index 2a50b1be366..8d7a5a6cd2c 100644 --- a/jans-casa/agama/project/project.json +++ b/jans-casa/agama/project/project.json @@ -1,6 +1,6 @@ { "projectName": "casa", - "version": "0.0.0-nightly", + "version": "1.3.0", "author": "jgomer2001", "description": "2FA flow featuring pluggable authentication methods", "noDirectLaunch": [ "io.jans.casa.authn.fido2", "io.jans.casa.authn.otp", "io.jans.casa.authn.super_gluu", "io.jans.casa.authn.twilio_sms" ], diff --git a/jans-casa/app-fips/pom.xml b/jans-casa/app-fips/pom.xml index 588db1b0976..2f2c60c7613 100644 --- a/jans-casa/app-fips/pom.xml +++ b/jans-casa/app-fips/pom.xml @@ -9,7 +9,7 @@ io.jans casa-base - 0.0.0-nightly + 1.3.0 diff --git a/jans-casa/app/pom.xml b/jans-casa/app/pom.xml index 012af6d76e1..565664e3d66 100644 --- a/jans-casa/app/pom.xml +++ b/jans-casa/app/pom.xml @@ -9,7 +9,7 @@ io.jans casa-base - 0.0.0-nightly + 1.3.0 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&&0 io.jans casa-base - 0.0.0-nightly + 1.3.0 diff --git a/jans-casa/plugins/acct-linking/agama/pom.xml b/jans-casa/plugins/acct-linking/agama/pom.xml index e81554651e1..2f0fd07ce02 100644 --- a/jans-casa/plugins/acct-linking/agama/pom.xml +++ b/jans-casa/plugins/acct-linking/agama/pom.xml @@ -5,8 +5,14 @@ io.jans.casa.plugins acct-linking-agama - 0.0.0-nightly - + 1.3.0 + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + diff --git a/jans-casa/plugins/acct-linking/agama/project/project.json b/jans-casa/plugins/acct-linking/agama/project/project.json index b7deef2a864..dc91a5d0a54 100644 --- a/jans-casa/plugins/acct-linking/agama/project/project.json +++ b/jans-casa/plugins/acct-linking/agama/project/project.json @@ -2,7 +2,7 @@ "projectName": "casa-account-linking", "author": "jgomer2001", "type": "Community", - "version": "0.0.0-nightly", + "version": "1.3.0", "description": "A helper project for Jans Casa accounts linking plugin", "noDirectLaunch": [ ], "configs": { diff --git a/jans-casa/plugins/acct-linking/pom.xml b/jans-casa/plugins/acct-linking/pom.xml index 8b6f291e163..2ff4c855586 100644 --- a/jans-casa/plugins/acct-linking/pom.xml +++ b/jans-casa/plugins/acct-linking/pom.xml @@ -5,7 +5,7 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/bioid/pom.xml b/jans-casa/plugins/bioid/pom.xml index 4f935a34dd4..4e773564a06 100644 --- a/jans-casa/plugins/bioid/pom.xml +++ b/jans-casa/plugins/bioid/pom.xml @@ -5,7 +5,7 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/client-authorizations/pom.xml b/jans-casa/plugins/client-authorizations/pom.xml index d11e9fad704..4913590a5d2 100644 --- a/jans-casa/plugins/client-authorizations/pom.xml +++ b/jans-casa/plugins/client-authorizations/pom.xml @@ -5,7 +5,7 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/custom-branding/pom.xml b/jans-casa/plugins/custom-branding/pom.xml index 2eea7fddb24..4ac024cf54b 100644 --- a/jans-casa/plugins/custom-branding/pom.xml +++ b/jans-casa/plugins/custom-branding/pom.xml @@ -5,7 +5,7 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/email_2fa/agama/pom.xml b/jans-casa/plugins/email_2fa/agama/pom.xml index 8c7d6e4e5a7..8d14456bff5 100644 --- a/jans-casa/plugins/email_2fa/agama/pom.xml +++ b/jans-casa/plugins/email_2fa/agama/pom.xml @@ -5,7 +5,15 @@ io.jans.casa.plugins email_2fa-agama - 0.0.0-nightly + 1.3.0 + + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + diff --git a/jans-casa/plugins/email_2fa/agama/project/project.json b/jans-casa/plugins/email_2fa/agama/project/project.json index e2e89e37495..ba4adb4ff58 100644 --- a/jans-casa/plugins/email_2fa/agama/project/project.json +++ b/jans-casa/plugins/email_2fa/agama/project/project.json @@ -2,7 +2,7 @@ "projectName": "email_2fa", "author": "jgomer2001", "type": "Community", - "version": "0.0.0-nightly", + "version": "1.3.0", "noDirectLaunch": [], "configs": { "io.jans.casa.authn.emailotp": { diff --git a/jans-casa/plugins/email_2fa/pom.xml b/jans-casa/plugins/email_2fa/pom.xml index ed8dd41867f..fef44276637 100644 --- a/jans-casa/plugins/email_2fa/pom.xml +++ b/jans-casa/plugins/email_2fa/pom.xml @@ -5,9 +5,17 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + + 11 11 diff --git a/jans-casa/plugins/samples/authentication-script-properties/pom.xml b/jans-casa/plugins/samples/authentication-script-properties/pom.xml index 16a18e36080..b1d5df3d0a0 100644 --- a/jans-casa/plugins/samples/authentication-script-properties/pom.xml +++ b/jans-casa/plugins/samples/authentication-script-properties/pom.xml @@ -5,7 +5,7 @@ co.acme ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/samples/helloworld/pom.xml b/jans-casa/plugins/samples/helloworld/pom.xml index 1067490b09d..e8ccdd9121b 100644 --- a/jans-casa/plugins/samples/helloworld/pom.xml +++ b/jans-casa/plugins/samples/helloworld/pom.xml @@ -5,7 +5,7 @@ co.acme ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/samples/sample-cred/pom.xml b/jans-casa/plugins/samples/sample-cred/pom.xml index dc5074b505e..bff7ff550fb 100644 --- a/jans-casa/plugins/samples/sample-cred/pom.xml +++ b/jans-casa/plugins/samples/sample-cred/pom.xml @@ -5,7 +5,7 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/plugins/strong-authn-settings/pom.xml b/jans-casa/plugins/strong-authn-settings/pom.xml index 8eb464c3886..4a9ad548538 100644 --- a/jans-casa/plugins/strong-authn-settings/pom.xml +++ b/jans-casa/plugins/strong-authn-settings/pom.xml @@ -5,7 +5,7 @@ io.jans.casa.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar diff --git a/jans-casa/pom.xml b/jans-casa/pom.xml index 9388b923188..61dd1c904c5 100644 --- a/jans-casa/pom.xml +++ b/jans-casa/pom.xml @@ -5,7 +5,7 @@ io.jans casa-base - 0.0.0-nightly + 1.3.0 pom diff --git a/jans-casa/shared/pom.xml b/jans-casa/shared/pom.xml index cb26a712863..d144e04c938 100644 --- a/jans-casa/shared/pom.xml +++ b/jans-casa/shared/pom.xml @@ -9,7 +9,7 @@ io.jans casa-base - 0.0.0-nightly + 1.3.0 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..cf198a68059 100644 --- a/jans-cedarling/bindings/cedarling_python/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_python/Cargo.toml @@ -1,16 +1,18 @@ [package] name = "cedarling_python" -version = "0.0.0" +version = "1.3.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/PYTHON_TYPES.md b/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md index f0875a6f06d..2124691bbec 100644 --- a/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md +++ b/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md @@ -201,36 +201,20 @@ ___ Error encountered while parsing Action to EntityUid ___ -# authorize_errors.AddEntitiesIntoContextError -Error encountered while adding entities into context -___ - # authorize_errors.AuthorizeError Exception raised by authorize_errors ___ -# authorize_errors.CreateAccessTokenEntityError -Error encountered while creating access_token entity -___ - -# authorize_errors.CreateContextError -Error encountered while validating context according to the schema -___ - -# authorize_errors.CreateIdTokenEntityError -Error encountered while creating id token entities -___ - -# authorize_errors.CreateUserEntityError -Error encountered while creating User entity +# authorize_errors.BuildContextError +Error encountered while building the request context ___ -# authorize_errors.CreateUserinfoTokenEntityError -Error encountered while creating Userinfo_token entity +# authorize_errors.BuildEntityError +Error encountered while running on strict id token trust mode ___ -# authorize_errors.CreateWorkloadEntityError -Error encountered while creating workload entity +# authorize_errors.CreateContextError +Error encountered while validating context according to the schema ___ # authorize_errors.EntitiesError @@ -241,16 +225,12 @@ ___ Error encountered while parsing all entities to json for logging ___ -# authorize_errors.ProcessTokens -Error encountered while processing JWT token data +# authorize_errors.IdTokenTrustModeError +Error encountered while running on strict id token trust mode ___ -# authorize_errors.ResourceEntityError -Error encountered while creating resource entity -___ - -# authorize_errors.RoleEntityError -Error encountered while creating role entity +# authorize_errors.ProcessTokens +Error encountered while processing JWT token data ___ # authorize_errors.UserRequestValidationError diff --git a/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.json b/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.json new file mode 100644 index 00000000000..6463ea37f78 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_python/example_files/sample_bootstrap_props.json @@ -0,0 +1,49 @@ +{ + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_POLICY_STORE_URI": null, + "CEDARLING_POLICY_STORE_ID": "gICAgcHJpbmNpcGFsIGlz", + "CEDARLING_LOG_TYPE": "std_out", + "CEDARLING_LOG_LEVEL": "INFO", + "CEDARLING_LOG_TTL": null, + "CEDARLING_DECISION_LOG_USER_CLAIMS": [ + "sub", + "email" + ], + "CEDARLING_DECISION_LOG_WORKLOAD_CLAIMS": [ + "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": "./example_files/policy-store.json", + "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": 0, + "CEDARLING_AUDIT_HEALTH_INTERVAL": 0, + "CEDARLING_AUDIT_TELEMETRY_INTERVAL": 0, + "CEDARLING_LISTEN_SSE": "disabled" +} 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/errors.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs index 89ad49ba9a9..06534720bf5 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs @@ -30,54 +30,6 @@ create_exception!( "Error encountered while processing JWT token data" ); -create_exception!( - authorize_errors, - CreateIdTokenEntityError, - AuthorizeError, - "Error encountered while creating id token entities" -); - -create_exception!( - authorize_errors, - CreateUserinfoTokenEntityError, - AuthorizeError, - "Error encountered while creating Userinfo_token entity" -); -create_exception!( - authorize_errors, - CreateAccessTokenEntityError, - AuthorizeError, - "Error encountered while creating access_token entity" -); - -create_exception!( - authorize_errors, - CreateUserEntityError, - AuthorizeError, - "Error encountered while creating User entity" -); - -create_exception!( - authorize_errors, - CreateWorkloadEntityError, - AuthorizeError, - "Error encountered while creating workload entity" -); - -create_exception!( - authorize_errors, - ResourceEntityError, - AuthorizeError, - "Error encountered while creating resource entity" -); - -create_exception!( - authorize_errors, - RoleEntityError, - AuthorizeError, - "Error encountered while creating role entity" -); - create_exception!( authorize_errors, ActionError, @@ -122,9 +74,23 @@ create_exception!( create_exception!( authorize_errors, - AddEntitiesIntoContextError, + BuildContextError, + AuthorizeError, + "Error encountered while building the request context" +); + +create_exception!( + authorize_errors, + IdTokenTrustModeError, + AuthorizeError, + "Error encountered while running on strict id token trust mode" +); + +create_exception!( + authorize_errors, + BuildEntityError, AuthorizeError, - "Error encountered while adding entities into context" + "Error encountered while running on strict id token trust mode" ); #[pyclass] @@ -166,20 +132,15 @@ macro_rules! errors_functions { // For each possible case of `AuthorizeError`, we have created a corresponding Python exception that inherits from `cedarling::AuthorizeError`. errors_functions! { ProcessTokens => ProcessTokens, - CreateIdTokenEntity => CreateIdTokenEntityError, - CreateUserinfoTokenEntity => CreateUserinfoTokenEntityError, - CreateAccessTokenEntity => CreateAccessTokenEntityError, - CreateUserEntity => CreateUserEntityError, - CreateWorkloadEntity => CreateWorkloadEntityError, - ResourceEntity => ResourceEntityError, - RoleEntity => RoleEntityError, Action => ActionError, CreateContext => CreateContextError, WorkloadRequestValidation => WorkloadRequestValidationError, UserRequestValidation => UserRequestValidationError, - BuildContext => AddEntitiesIntoContextError, Entities => EntitiesError, - EntitiesToJson => EntitiesToJsonError + EntitiesToJson => EntitiesToJsonError, + BuildContext => BuildContextError, + IdTokenTrustMode => IdTokenTrustModeError, + BuildEntity => BuildEntityError } pub fn authorize_errors_module(m: &Bound<'_, PyModule>) -> PyResult<()> { 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_python/tests/test_authorize.py b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py index 92926ccfccd..86597323ff0 100644 --- a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py +++ b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py @@ -186,8 +186,8 @@ def test_resource_entity_error(): ''' try: raise_authorize_error(load_bootstrap_config()) - except authorize_errors.ResourceEntityError as e: - assert str(e) == "could not create resource entity: could not get attribute value from payload: type mismatch for key 'org_id'. expected: 'String', but found: 'number'" + except authorize_errors.BuildEntityError as e: + assert str(e) == "failed to build resource entity: failed to build `org_id` attribute: failed to build restricted expression: type mismatch for key 'org_id'. expected: 'string', but found: 'number'" def test_authorize_error(): @@ -199,4 +199,4 @@ def test_authorize_error(): try: raise_authorize_error(load_bootstrap_config()) except authorize_errors.AuthorizeError as e: - assert str(e) == "could not create resource entity: could not get attribute value from payload: type mismatch for key 'org_id'. expected: 'String', but found: 'number'" + assert str(e) == "failed to build resource entity: failed to build `org_id` attribute: failed to build restricted expression: type mismatch for key 'org_id'. expected: 'string', but found: 'number'" diff --git a/jans-cedarling/bindings/cedarling_wasm/Cargo.toml b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml new file mode 100644 index 00000000000..fdbefd360e9 --- /dev/null +++ b/jans-cedarling/bindings/cedarling_wasm/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "cedarling_wasm" +version = "1.3.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..c2315cd650f 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -1,21 +1,27 @@ [package] name = "cedarling" -version = "0.0.0-nightly" +version = "1.3.0" 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 } uuid7 = { version = "1.1.0", features = ["serde", "uuid"] } -cedar-policy = "4.2" +cedar-policy = { version = "4.2", features = ["partial-eval"] } 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..36d74baa93f 100644 --- a/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_with_jwt_validation.rs @@ -3,19 +3,19 @@ // // Copyright (c) 2024, Gluu, Inc. -use std::collections::{HashMap, HashSet}; - use cedarling::{ - AuthorizationConfig, BootstrapConfig, Cedarling, IdTokenTrustMode, JwtConfig, LogConfig, - LogLevel, LogTypeConfig, PolicyStoreConfig, PolicyStoreSource, Request, ResourceData, - TokenValidationConfig, Tokens, WorkloadBoolOp, + AuthorizationConfig, BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogLevel, LogTypeConfig, + PolicyStoreConfig, PolicyStoreSource, Request, ResourceData, TokenValidationConfig, Tokens, + WorkloadBoolOp, }; use jsonwebtoken::Algorithm; +use std::collections::{HashMap, HashSet}; 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. @@ -23,7 +23,6 @@ fn main() -> Result<(), Box> { jwks: None, jwt_sig_validation: true, jwt_status_validation: false, - id_token_trust_mode: IdTokenTrustMode::None, signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256, Algorithm::RS256]), access_token_config: TokenValidationConfig::access_token(), id_token_config: TokenValidationConfig::id_token(), @@ -54,29 +53,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), - }, - 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()), - )]), + 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()), + )]), + }, + }) + .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/build_ctx.rs b/jans-cedarling/cedarling/src/authz/build_ctx.rs new file mode 100644 index 00000000000..265a4f7e7aa --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/build_ctx.rs @@ -0,0 +1,191 @@ +// 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::HashMap; + +use super::{AuthorizeEntitiesData, AuthzConfig}; +use crate::common::cedar_schema::cedar_json::attribute::Attribute; +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::cedar_schema::CEDAR_NAMESPACE_SEPARATOR; +use cedar_policy::ContextJsonError; +use serde_json::{json, map::Entry, Value}; + +/// Constructs the authorization context by adding the built entities from the tokens +pub fn build_context( + config: &AuthzConfig, + request_context: Value, + entities_data: &AuthorizeEntitiesData, + schema: &cedar_policy::Schema, + action: &cedar_policy::EntityUid, +) -> Result { + let namespace = config.policy_store.namespace(); + let action_name = &action.id().escaped(); + let json_schema = &config.policy_store.schema.json; + let action_schema = json_schema + .get_action(namespace, action_name) + .ok_or(BuildContextError::UnknownAction(action_name.to_string()))?; + + // Get the entities required for the context + let mut ctx_entity_refs = json!({}); + let type_ids = entities_data.type_ids(); + if let Some(ctx) = action_schema.applies_to.context.as_ref() { + match ctx { + Attribute::Record { attrs, .. } => { + for (key, attr) in attrs.iter() { + if let Some(entity_ref) = + build_entity_refs_from_attr(namespace, attr, &type_ids, json_schema)? + { + ctx_entity_refs[key] = entity_ref; + } + } + }, + Attribute::EntityOrCommon { name, .. } => { + // TODO: handle potential namespace collisions when Cedarling starts + // supporting multiple namespaces + if let Some((_namespace, attr)) = json_schema.get_common_type(name) { + match attr { + Attribute::Record { attrs, .. } => { + for (key, attr) in attrs.iter() { + if let Some(entity_ref) = build_entity_refs_from_attr( + namespace, + attr, + &type_ids, + json_schema, + )? { + ctx_entity_refs[key] = entity_ref; + } + } + }, + attr => { + return Err(BuildContextError::InvalidKind( + attr.kind_str().to_string(), + "record".to_string(), + )) + }, + } + } + }, + attr => { + return Err(BuildContextError::InvalidKind( + attr.kind_str().to_string(), + "record or common".to_string(), + )) + }, + } + } + + let context = merge_json_values(request_context, ctx_entity_refs)?; + let context: cedar_policy::Context = + cedar_policy::Context::from_json_value(context, Some((schema, action)))?; + + Ok(context) +} + +/// Builds the JSON entity references from a given attribute. +/// +/// Returns `Ok(None)` if the attr is not an Entity Reference +fn build_entity_refs_from_attr( + namespace: &str, + attr: &Attribute, + type_ids: &HashMap, + schema: &CedarSchemaJson, +) -> Result, BuildContextError> { + match attr { + Attribute::Entity { name, .. } => map_entity_id(namespace, name, type_ids), + Attribute::EntityOrCommon { name, .. } => { + if let Some((entity_namespace, _)) = schema.get_entity_from_base_name(name) { + if namespace == entity_namespace { + return map_entity_id(namespace, name, type_ids); + } + } + Ok(None) + }, + _ => Ok(None), + } +} + +/// Maps a known entity ID to the entity reference +fn map_entity_id( + namespace: &str, + name: &str, + type_ids: &HashMap, +) -> Result, BuildContextError> { + if let Some(type_id) = type_ids.get(name).as_ref() { + let name = join_namespace(namespace, name); + Ok(Some(json!({"type": name, "id": type_id}))) + } else { + Err(BuildContextError::MissingEntityId(name.to_string())) + } +} + +/// Joins the given type name with the given namespace if it's not an empty string. +fn join_namespace(namespace: &str, type_name: &str) -> String { + if namespace.is_empty() { + return type_name.to_string(); + } + [namespace, type_name].join(CEDAR_NAMESPACE_SEPARATOR) +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildContextError { + /// Error encountered while validating context according to the schema + #[error("failed to merge JSON objects due to conflicting keys: {0}")] + KeyConflict(String), + /// Error encountered while deserializing the Context from JSON + #[error(transparent)] + DeserializeFromJson(#[from] ContextJsonError), + /// Error encountered if the action being used as the reference to build the Context + /// is not in the schema + #[error("failed to find the action `{0}` in the schema")] + UnknownAction(String), + /// Error encountered while building entity references in the Context + #[error("failed to build entity reference for `{0}` since an entity id was not provided")] + MissingEntityId(String), + #[error("invalid action context type: {0}. expected: {1}")] + InvalidKind(String, String), +} + +pub fn merge_json_values(mut base: Value, other: Value) -> Result { + if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) { + for (key, value) in additional_map { + if let Entry::Vacant(entry) = base_map.entry(key) { + entry.insert(value.clone()); + } else { + return Err(BuildContextError::KeyConflict(key.clone())); + } + } + } + Ok(base) +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn can_merge_json_objects() { + let obj1 = json!({ "a": 1, "b": 2 }); + let obj2 = json!({ "c": 3, "d": 4 }); + let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4}); + + let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects"); + + assert_eq!(result, expected); + } + + #[test] + fn errors_on_same_keys() { + // Test for only two objects + let obj1 = json!({ "a": 1, "b": 2 }); + let obj2 = json!({ "b": 3, "c": 4 }); + let result = merge_json_values(obj1, obj2); + + assert!( + matches!(result, Err(BuildContextError::KeyConflict(key)) if key.as_str() == "b"), + "Expected an error due to conflicting keys" + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entities/create.rs b/jans-cedarling/cedarling/src/authz/entities/create.rs deleted file mode 100644 index f1346c22594..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/create.rs +++ /dev/null @@ -1,440 +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::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression}; - -use super::trait_as_expression::AsExpression; -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}; - -pub const CEDAR_POLICY_SEPARATOR: &str = "::"; - -/// Meta information about an entity type. -/// Is used to store in `static` variable. -#[derive(Debug)] -pub(crate) struct EntityMetadata<'a> { - pub entity_type: EntityParsedTypeName<'a>, - pub entity_id_data_key: &'a str, -} - -impl<'a> EntityMetadata<'a> { - /// create new instance of EntityMetadata. - pub fn new(entity_type: EntityParsedTypeName<'a>, entity_id_data_key: &'a str) -> Self { - Self { - entity_type, - entity_id_data_key, - } - } - - /// Create entity from token data. - // we also can create entity using the ['create_entity'] function. - pub fn create_entity( - &'a self, - schema: &'a CedarSchemaJson, - token: &Token, - parents: HashSet, - claim_mapping: &ClaimMappings, - ) -> Result { - let entity_uid = build_entity_uid( - self.entity_type.full_type_name().as_str(), - token - .get_claim(self.entity_id_data_key) - .ok_or(CreateCedarEntityError::MissingClaim( - self.entity_id_data_key.to_string(), - ))? - .as_str()?, - )?; - - create_entity( - entity_uid, - &self.entity_type, - schema, - token.claims(), - parents, - claim_mapping, - ) - } -} - -/// build [`EntityUid`] based on input parameters -pub(crate) fn build_entity_uid( - entity_type: &str, - entity_id: &str, -) -> Result { - let entity_uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(entity_type) - .map_err(|err| CreateCedarEntityError::EntityTypeName(entity_type.to_string(), err))?, - EntityId::new(entity_id), - ); - - Ok(entity_uid) -} - -/// Parsed result of entity type name and namespace. -/// Analog to the internal cedar_policy type `InternalName` -#[derive(Debug)] -pub(crate) struct EntityParsedTypeName<'a> { - pub type_name: &'a str, - pub namespace: &'a str, -} -impl<'a> EntityParsedTypeName<'a> { - pub fn new(typename: &'a str, namespace: &'a str) -> Self { - EntityParsedTypeName { - type_name: typename, - namespace, - } - } - - pub fn full_type_name(&self) -> String { - if self.namespace.is_empty() { - self.type_name.to_string() - } else { - [self.namespace, self.type_name].join(CEDAR_POLICY_SEPARATOR) - } - } -} - -/// Parse entity type name and namespace from entity type string. -/// return (typename, namespace) -pub fn parse_namespace_and_typename(raw_entity_type: &str) -> (&str, String) { - let mut raw_path: Vec<&str> = raw_entity_type.split(CEDAR_POLICY_SEPARATOR).collect(); - let typename = raw_path.pop().unwrap_or_default(); - let namespace = raw_path.join(CEDAR_POLICY_SEPARATOR); - (typename, namespace) -} - -/// fetch the schema record for a given entity type from the cedar schema json -fn fetch_schema_record<'a>( - entity_info: &EntityParsedTypeName, - schema: &'a CedarSchemaJson, -) -> Result<&'a CedarSchemaEntityShape, CreateCedarEntityError> { - let entity_shape = schema - .entity_schema(entity_info.namespace, entity_info.type_name) - .ok_or(CreateCedarEntityError::CouldNotFindEntity( - entity_info.type_name.to_string(), - ))?; - - if let Some(entity_record) = &entity_shape.shape { - if !entity_record.is_record() { - return Err(CreateCedarEntityError::NotRecord( - entity_info.type_name.to_string(), - )); - }; - } - - Ok(entity_shape) -} - -/// get mapping of the entity attributes -fn entity_meta_attributes( - schema_record: &CedarSchemaRecord, -) -> Result, GetCedarTypeError> { - schema_record - .attributes - .iter() - .map(|(attribute_name, attribute)| { - attribute - .get_type() - .map(|attr_type| EntityAttributeMetadata { - attribute_name: attribute_name.as_str(), - cedar_policy_type: attr_type, - is_required: attribute.is_required(), - }) - }) - .collect::, _>>() -} - -/// Build attributes for the entity -fn build_entity_attributes( - schema: &CedarSchemaJson, - parsed_typename: &EntityParsedTypeName, - tkn_data: &TokenClaims, - claim_mapping: &ClaimMappings, -) -> Result, CreateCedarEntityError> { - // fetch the schema entity shape from the json-schema. - let schema_shape = fetch_schema_record(parsed_typename, schema)?; - - if let Some(schema_record) = &schema_shape.shape { - let attr_vec = entity_meta_attributes(schema_record)? - .into_iter() - .filter_map(|attr: EntityAttributeMetadata| { - let attr_name = attr.attribute_name; - let cedar_exp_result = token_attribute_to_cedar_exp( - &attr, - tkn_data, - parsed_typename, - schema, - claim_mapping, - ); - match (cedar_exp_result, attr.is_required) { - (Ok(cedar_exp), _) => Some(Ok((attr_name.to_string(), cedar_exp))), - ( - Err(CreateCedarEntityError::MissingClaim(_)), - false, - // when the attribute is not required and not found in token data we skip it - ) => None, - (Err(err), _) => Some(Err(err)), - } - }) - .collect::, CreateCedarEntityError>>()?; - Ok(HashMap::from_iter(attr_vec)) - } else { - Ok(HashMap::new()) - } -} - -/// Create entity from token payload data. -pub fn create_entity( - entity_uid: EntityUid, - parsed_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - tkn_data: &TokenClaims, - parents: HashSet, - claim_mapping: &ClaimMappings, -) -> Result { - let attrs = build_entity_attributes(schema, parsed_typename, tkn_data, claim_mapping)?; - - let entity_uid_string = entity_uid.to_string(); - cedar_policy::Entity::new(entity_uid, attrs, parents) - .map_err(|err| CreateCedarEntityError::CreateEntity(entity_uid_string, err)) -} - -/// Meta information about an attribute for cedar policy. -pub struct EntityAttributeMetadata<'a> { - // The name of the attribute in the cedar policy - // mapped one-to-one with the attribute in the token data. - pub attribute_name: &'a str, - // The type of the cedar policy attribute. - pub cedar_policy_type: CedarType, - // if this attribute is required - pub is_required: bool, -} - -/// Get the cedar policy expression value for a given type. -fn token_attribute_to_cedar_exp( - attribute_metadata: &EntityAttributeMetadata, - tkn_data: &TokenClaims, - entity_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - let token_claim_key = attribute_metadata.attribute_name; - - let token_claim_value = - tkn_data - .get_claim(token_claim_key) - .ok_or(CreateCedarEntityError::MissingClaim( - token_claim_key.to_string(), - ))?; - - get_expression( - &attribute_metadata.cedar_policy_type, - &token_claim_value, - entity_typename, - schema, - claim_mapping, - ) -} - -/// Build [`RestrictedExpression`] based on input parameters. -fn get_expression( - cedar_type: &CedarType, - claim: &TokenClaim, - base_entity_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - match cedar_type { - CedarType::String => Ok(claim.as_str()?.to_string().to_expression()), - CedarType::Long => Ok(claim.as_i64()?.to_expression()), - CedarType::Boolean => Ok(claim.as_bool()?.to_expression()), - CedarType::TypeName(cedar_typename) => { - match schema.find_type(cedar_typename, base_entity_typename.namespace) { - Some(SchemaDefinedType::Entity(_)) => { - get_entity_expression(cedar_typename, base_entity_typename, claim) - }, - Some(SchemaDefinedType::CommonType(record)) => { - let record_typename = - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace); - - get_record_expression(record, &record_typename, claim, schema, claim_mapping) - .map_err(|err| { - CreateCedarEntityError::CreateRecord( - record_typename.full_type_name(), - Box::new(err), - ) - }) - }, - None => Err(CreateCedarEntityError::FindType( - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace) - .full_type_name(), - )), - } - }, - CedarType::Set(cedar_type) => { - let vec_of_expression = claim - .as_array()? - .into_iter() - .map(|payload| { - get_expression( - cedar_type, - &payload, - base_entity_typename, - schema, - claim_mapping, - ) - }) - .collect::, _>>()?; - - Ok(RestrictedExpression::new_set(vec_of_expression)) - }, - } -} - -/// Create [`RestrictedExpression`] with entity UID as token_claim_value -fn get_entity_expression( - cedar_typename: &str, - base_entity_typename: &EntityParsedTypeName<'_>, - token_claim: &TokenClaim, -) -> Result { - let restricted_expression = { - let entity_full_type_name = - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace) - .full_type_name(); - - let uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(entity_full_type_name.as_str()).map_err(|err| { - CreateCedarEntityError::EntityTypeName(entity_full_type_name.to_string(), err) - })?, - EntityId::new(token_claim.as_str()?), - ); - RestrictedExpression::new_entity_uid(uid) - }; - Ok(restricted_expression) -} - -/// Build [`RestrictedExpression`] based on token_claim_value. -/// It tries to find mapping and apply it to `token_claim` json value. -fn get_record_expression( - record: &CedarSchemaRecord, - cedar_record_type: &EntityParsedTypeName<'_>, - token_claim: &TokenClaim, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - // map json value of `token_claim` to TokenPayload object (HashMap) - let mapped_claim: TokenClaims = - match claim_mapping.get_mapping(token_claim.key(), &cedar_record_type.full_type_name()) { - Some(m) => m.apply_mapping(token_claim.value()).into(), - // if we do not have mapping, and value is json object, return TokenPayload based on it. - // if value is not json object, return empty value - None => { - if let Some(map) = token_claim.value().as_object() { - TokenClaims::from_json_map(map.to_owned()) - } else { - TokenClaims::default() - } - }, - }; - - let mut record_restricted_exps = Vec::new(); - - for (attribute_key, entity_attribute) in record.attributes.iter() { - let attribute_type = entity_attribute.get_type()?; - - let mapped_claim_value = - mapped_claim - .get_claim(attribute_key) - .ok_or(CreateCedarEntityError::MissingClaim( - attribute_key.to_string(), - ))?; - - let exp = get_expression( - &attribute_type, - &mapped_claim_value, - cedar_record_type, - schema, - claim_mapping, - ) - .map_err(|err| { - CreateCedarEntityError::BuildAttribute( - cedar_record_type.full_type_name(), - attribute_key.to_string(), - Box::new(err), - ) - })?; - - record_restricted_exps.push((attribute_key.to_string(), exp)); - } - - let restricted_expression = - RestrictedExpression::new_record(record_restricted_exps.into_iter()) - .map_err(CreateCedarEntityError::CreateRecordFromIter)?; - Ok(restricted_expression) -} - -/// Describe errors on creating entity -#[derive(thiserror::Error, Debug)] -pub enum CreateCedarEntityError { - /// Could not parse entity type - #[error("could not parse entity type name: {0}, error: {1}")] - EntityTypeName(String, cedar_policy::ParseErrors), - - /// Could find entity type in the `cedar-policy` schema - #[error("could find entity type: {0} in the schema")] - CouldNotFindEntity(String), - - /// Type in the schema is not record - #[error("type: {0} in the schema is not record")] - NotRecord(String), - - /// Could create entity - #[error("could create entity with uid: {0}, error: {1}")] - CreateEntity(String, cedar_policy::EntityAttrEvaluationError), - - /// Could not get attribute value from payload - #[error("could not get attribute value from payload: {0}")] - GetTokenClaim(#[from] TokenClaimTypeError), - - /// Could not retrieve attribute from cedar-policy schema - #[error("could not retrieve attribute from cedar-policy schema: {0}")] - GetCedarType(#[from] GetCedarTypeError), - - /// Error on cedar-policy type attribute - #[error("err build cedar-policy type: {0}, mapped JWT attribute `{1}`: {2}")] - BuildAttribute(String, String, Box), - - /// Error on creating `cedar-policy` record, in schema it is named as type - #[error("could not create `cedar-policy` record/type {0} : {1}")] - CreateRecord(String, Box), - - /// Wrapped error on [`RestrictedExpression::new_record`] - // this error probably newer happen - #[error("could not build expression from list of expressions: {0}")] - CreateRecordFromIter(cedar_policy::ExpressionConstructionError), - - /// Cause when cannot find record/type in json schema. - #[error("could find record/type: {0}")] - FindType(String), - - /// Error when using the transaction token. Its usage is currently not implemented. - #[error("transaction token not implemented")] - TransactionToken, - - /// Indicates that the creation of an Entity failed due to the absence of available tokens. - #[error("no available token to build the entity from")] - UnavailableToken, - - /// Missing claim - #[error("missing claim: {0}")] - MissingClaim(String), -} diff --git a/jans-cedarling/cedarling/src/authz/entities/mod.rs b/jans-cedarling/cedarling/src/authz/entities/mod.rs deleted file mode 100644 index cad03f4e371..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/mod.rs +++ /dev/null @@ -1,278 +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. - -//! Module for creating cedar-policy entities - -mod create; -mod trait_as_expression; -mod user; -mod workload; - -#[cfg(test)] -mod test_create; - -use std::collections::HashSet; - -use cedar_policy::{Entity, EntityUid}; -use create::{ - build_entity_uid, create_entity, parse_namespace_and_typename, EntityMetadata, - EntityParsedTypeName, -}; -pub use create::{CreateCedarEntityError, CEDAR_POLICY_SEPARATOR}; -pub use user::*; -pub use workload::*; - -use super::request::ResourceData; -use super::AuthorizeError; -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"; -const DEFAULT_USERINFO_TKN_ENTITY_TYPE_NAME: &str = "Userinfo_token"; -const DEFAULT_TKN_PRINCIPAL_IDENTIFIER: &str = "jti"; - -pub struct DecodedTokens<'a> { - pub access_token: Option>, - pub id_token: Option>, - pub userinfo_token: Option>, -} - -impl DecodedTokens<'_> { - pub fn iter(&self) -> impl Iterator { - [ - self.access_token.as_ref(), - self.id_token.as_ref(), - self.userinfo_token.as_ref(), - ] - .into_iter() - .flatten() - } -} - -pub struct TokenEntities { - pub access: Option, - pub id: Option, - pub userinfo: Option, -} - -pub fn create_token_entities( - conf: &AuthorizationConfig, - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result { - let schema = &policy_store.schema.json; - let namespace = policy_store.namespace(); - - // create access token entity - let access = if let Some(token) = tokens.access_token.as_ref() { - let type_name = conf - .mapping_access_token - .as_deref() - .unwrap_or(DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateAccessTokenEntity)?, - ) - } else { - None - }; - - // create id token entity - let id = if let Some(token) = tokens.id_token.as_ref() { - let type_name = conf - .mapping_id_token - .as_deref() - .unwrap_or(DEFAULT_ID_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateIdTokenEntity)?, - ) - } else { - None - }; - - // create userinfo token entity - let userinfo = if let Some(token) = tokens.userinfo_token.as_ref() { - let type_name = conf - .mapping_userinfo_token - .as_deref() - .unwrap_or(DEFAULT_USERINFO_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateUserinfoTokenEntity)?, - ) - } else { - None - }; - - Ok(TokenEntities { - access, - id, - userinfo, - }) -} - -fn create_token_entity( - token: &Token, - schema: &CedarSchemaJson, - namespace: &str, - type_name: &str, -) -> Result { - let claim_mapping = token.claim_mapping(); - let tkn_metadata = EntityMetadata::new( - EntityParsedTypeName { - type_name, - namespace, - }, - token - .metadata() - .principal_identifier - .as_deref() - .unwrap_or(DEFAULT_TKN_PRINCIPAL_IDENTIFIER), - ); - tkn_metadata.create_entity(schema, token, HashSet::new(), claim_mapping) -} - -/// Describe errors on creating resource entity -#[derive(thiserror::Error, Debug)] -pub enum ResourceEntityError { - #[error("could not create resource entity: {0}")] - Create(#[from] CreateCedarEntityError), -} - -/// Create entity from [`ResourceData`] -pub fn create_resource_entity( - resource: ResourceData, - schema: &CedarSchemaJson, -) -> Result { - let entity_uid = resource.entity_uid().map_err(|err| { - CreateCedarEntityError::EntityTypeName(resource.resource_type.clone(), err) - })?; - - let (typename, namespace) = parse_namespace_and_typename(&resource.resource_type); - - Ok(create_entity( - entity_uid, - &EntityParsedTypeName::new(typename, namespace.as_str()), - schema, - &resource.payload.into(), - HashSet::new(), - // we no need mapping for resource because user put json structure and it should be correct - &ClaimMappings::default(), - )?) -} - -/// Describe errors on creating role entity -#[derive(thiserror::Error, Debug)] -pub enum RoleEntityError { - #[error("could not create Jans::Role entity from {token_kind} token: {error}")] - Create { - error: CreateCedarEntityError, - token_kind: TokenKind, - }, - - /// Indicates that the creation of the Role Entity failed due to the absence of available tokens. - #[error("Role Entity creation failed: no available token to build the entity from")] - UnavailableToken, -} - -/// Create `Role` entites from based on `TrustedIssuer` role mapping for each token or default value of `RoleMapping` -pub fn create_role_entities( - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result, RoleEntityError> { - let mut role_entities = Vec::new(); - - for token in tokens.iter() { - let mut entities = extract_roles_from_token(policy_store, token)?; - role_entities.append(&mut entities); - } - - Ok(role_entities) -} - -/// Extract `Role` entites based on single `RoleMapping` -fn extract_roles_from_token( - policy_store: &PolicyStore, - token: &Token, -) -> Result, RoleEntityError> { - let parsed_typename = EntityParsedTypeName::new("Role", policy_store.namespace()); - let role_entity_type = parsed_typename.full_type_name(); - - // get payload of role id in JWT token data - let Some(payload) = token.get_claim(token.role_mapping()) else { - // if key not found we return empty vector - return Ok(Vec::new()); - }; - - // it can be 2 scenario when field is array or field is string - let entity_uid_vec: Vec = if let Ok(payload_str) = payload.as_str() { - // case if it string - let entity_uid = - build_entity_uid(role_entity_type.as_str(), payload_str).map_err(|err| { - RoleEntityError::Create { - error: err, - token_kind: token.kind, - } - })?; - vec![entity_uid] - } else { - // case if it array of string - match payload - // get as array - .as_array() - { - Ok(payload_vec) => { - payload_vec - .iter() - .map(|payload_el| { - // get each element of array as `str` - payload_el.as_str().map_err(|err| RoleEntityError::Create { - error: err.into(), - token_kind: token.kind, - }) - // build entity uid - .and_then(|name| build_entity_uid(role_entity_type.as_str(), name) - .map_err(|err| RoleEntityError::Create { - error: err, - token_kind: token.kind, - })) - }) - .collect::, _>>()? - }, - Err(err) => { - // Handle the case where the payload is neither a string nor an array - return Err(RoleEntityError::Create { - error: err.into(), - token_kind: token.kind, - }); - }, - } - }; - - let schema = &policy_store.schema.json; - - // create role entity for each entity uid - entity_uid_vec - .into_iter() - .map(|entity_uid| { - create_entity( - entity_uid, - &parsed_typename, - schema, - token.claims(), - HashSet::new(), - token.claim_mapping(), - ) - .map_err(|err| RoleEntityError::Create { - error: err, - token_kind: token.kind, - }) - }) - .collect::, _>>() -} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create.rs b/jans-cedarling/cedarling/src/authz/entities/test_create.rs deleted file mode 100644 index b00c2c335aa..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create.rs +++ /dev/null @@ -1,575 +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. - -//! Testing the creating entities - -use std::collections::HashSet; - -use test_utils::{assert_eq, SortedJson}; - -use super::create::*; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::jwt::{Token, TokenClaimTypeError, TokenClaims}; - -// test all successful cases -// with empty namespace -#[test] -fn successful_scenario_empty_namespace() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect("entity should be created"); - - let entity_json = entity.to_json_value().expect("should serialize to json"); - - let expected = serde_json::json!({ - "uid": { - "type": "Test", - "id": "test_id" - }, - "attrs": { - "entity_uid_key": { - "__entity": { - "type": "Test2", - "id": "unique_id" - } - }, - "long_key": 12345, - "string_key": "test string value", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }, - "parents": [] - }); - - assert_eq!(expected.sorted(), entity_json.sorted()); -} - -// test all successful cases -// with empty namespace -#[test] -fn successful_scenario_not_empty_namespace() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "Jans", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect("entity should be created"); - - let entity_json = entity.to_json_value().expect("should serialize to json"); - - let expected = serde_json::json!({ - "uid": { - "type": "Jans::Test", - "id": "test_id" - }, - "attrs": { - "entity_uid_key": { - "__entity": { - "type": "Jans::Test2", - "id": "unique_id" - } - }, - "long_key": 12345, - "string_key": "test string value", - "bool_key": true - }, - "parents": [] - }); - - assert_eq!(expected.sorted(), entity_json.sorted()); -} - -/// test wrong string type in token payload -#[test] -fn get_token_claim_type_string_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "string_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - // This will trigger the type error, because it's not a String. - test_key: 123, - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); - } -} - -/// test wrong long type in token payload -#[test] -fn get_token_claim_type_long_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "long_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - // This will trigger the type error, because it's not an i64. - "long_key": "str", - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); - } -} - -/// test wrong entity_uid type in token payload -#[test] -fn get_token_claim_type_entity_uid_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "entity_uid_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - // This will trigger the type error, because it's not a String. - "entity_uid_key": 123, - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); - } -} - -/// test wrong boolean type in token payload -#[test] -fn get_token_claim_type_boolean_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "bool_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - // This will trigger the type error, because it's not a bool. - "bool_key": 123, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); - } -} - -/// test wrong set type in token payload, should be array of string -#[test] -fn get_token_claim_type_set_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "set_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - "bool_key": false, - // This will trigger the type error, because it's not a array of string. - "set_key": 1, - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); - } -} - -/// test wrong set type in token payload, should be array of array of string -#[test] -fn get_token_claim_type_set_of_set_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - "bool_key": false, - "set_key": ["some_string"], - // This will trigger the type error, because it's not a array of array of string. - "set_set_key": ["some_string"] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get("set_set_key").unwrap(); - let origin_type = - TokenClaimTypeError::json_value_type_name(&json_attr_value.as_array().unwrap()[0]); - - // key set_set_key and zero element in array - let test_key = "set_set_key[0]"; - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!("expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}"); - } -} - -/// create entity with wrong cedar typename -#[test] -fn get_token_claim_cedar_typename_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - // Mistake in entity type name, should be `"Jans::Test"`, it will trigger error - let (typename, namespace) = parse_namespace_and_typename("Jans:::Test"); - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: &namespace, - type_name: typename, - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::EntityTypeName(typename, _) = &entity_creation_error { - assert_eq!("Jans:::Test", typename); - } else { - panic!( - "error should be CedarPolicyCreateTypeError::EntityTypeName, but got {:?}", - entity_creation_error - ); - } -} - -/// create entity with wrong cedar typename in the attribute -// The JSON schema contains an error.r: -// -// "entity_uid_key": { -// "type": "EntityOrCommon", -// "name": ":Test2" -// }, -// -// ":Test2" is not correct type definition, it will trigger error -#[test] -fn get_token_claim_cedar_typename_in_attr_error() { - let schema_json = include_str!("test_create_data/type_error_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "Jans", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::FindType(typename) = &entity_creation_error { - assert_eq!("Jans:::Test2", typename); - } else { - panic!( - "error should be CedarPolicyCreateTypeError::EntityTypeName, but got {:?}", - entity_creation_error - ); - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema b/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema deleted file mode 100644 index ea5cd8c527a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema +++ /dev/null @@ -1,26 +0,0 @@ -entity Test = { - string_key: String, - long_key: Long, - bool_key: Boolean, - entity_uid_key: Test2, - optional_key?: String, - set_key: Set, - set_set_key: Set>, -}; - -entity Test2 = { -}; - - -namespace Jans{ - entity Test = { - string_key: String, - long_key: Long, - bool_key: Boolean, - entity_uid_key: Test2, - optional_key?: String - }; - - entity Test2 = { - }; -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json b/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json deleted file mode 100644 index 794637d08ee..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "Jans": { - "entityTypes": { - "Test2": {}, - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": "Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - } - }, - "actions": {} - }, - "": { - "entityTypes": { - "Test2": {}, - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": "Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "set_key": { - "type": "Set", - "element": { - "type": "EntityOrCommon", - "name": "String" - } - }, - "set_set_key": { - "type": "Set", - "element": { - "type": "Set", - "element": { - "type": "EntityOrCommon", - "name": "String" - } - } - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - } - }, - "actions": {} - } -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json b/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json deleted file mode 100644 index 7ee0fa03dbe..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "Jans": { - "entityTypes": { - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": ":Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - }, - "Test2": {} - }, - "actions": {} - } -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs b/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs deleted file mode 100644 index cd66476a33a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs +++ /dev/null @@ -1,29 +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 cedar_policy::RestrictedExpression; - -/// Trait to cast type to [`RestrictedExpression`] -pub(crate) trait AsExpression { - fn to_expression(self) -> RestrictedExpression; -} - -impl AsExpression for i64 { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_long(self) - } -} - -impl AsExpression for String { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_string(self) - } -} - -impl AsExpression for bool { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_bool(self) - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/user.rs b/jans-cedarling/cedarling/src/authz/entities/user.rs deleted file mode 100644 index da43a4f6480..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/user.rs +++ /dev/null @@ -1,252 +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::collections::HashSet; -use std::fmt; - -use cedar_policy::EntityUid; - -use super::{CreateCedarEntityError, DecodedTokens, EntityMetadata, EntityParsedTypeName}; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::policy_store::{PolicyStore, TokenKind}; -use crate::jwt::Token; - -/// Create user entity -pub fn create_user_entity( - entity_mapping: Option<&str>, - policy_store: &PolicyStore, - tokens: &DecodedTokens, - parents: HashSet, -) -> Result { - let schema: &CedarSchemaJson = &policy_store.schema.json; - let namespace = policy_store.namespace(); - let mut errors = Vec::new(); - - // helper closure to attempt entity creation from a token - let try_create_entity = |token_kind: TokenKind, token: Option<&Token>| { - if let Some(token) = token { - let claim_mapping = token.claim_mapping(); - let user_mapping = token.user_mapping(); - let entity_metadata = EntityMetadata::new( - EntityParsedTypeName { - type_name: entity_mapping.unwrap_or("User"), - namespace, - }, - user_mapping, - ); - entity_metadata - .create_entity(schema, token, parents.clone(), claim_mapping) - .map_err(|e| (token_kind, e)) - } else { - Err((token_kind, CreateCedarEntityError::UnavailableToken)) - } - }; - - // attempt entity creation for each token type that contains user info - for (token_kind, token) in [ - (TokenKind::Userinfo, tokens.userinfo_token.as_ref()), - (TokenKind::Id, tokens.id_token.as_ref()), - ] { - match try_create_entity(token_kind, token) { - Ok(entity) => return Ok(entity), - Err(e) => errors.push(e), - } - } - - Err(CreateUserEntityError { errors }) -} - -#[derive(Debug, thiserror::Error)] -pub struct CreateUserEntityError { - pub errors: Vec<(TokenKind, CreateCedarEntityError)>, -} - -impl fmt::Display for CreateUserEntityError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.errors.is_empty() { - writeln!( - f, - "Failed to create User Entity since no tokens were provided" - )?; - } else { - writeln!( - f, - "Failed to create User Entity due to the following errors:" - )?; - for (token_kind, error) in &self.errors { - writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - use cedar_policy::{Entity, RestrictedExpression}; - use serde_json::json; - use test_utils::assert_eq; - - use super::create_user_entity; - use crate::authz::entities::DecodedTokens; - use crate::common::policy_store::TokenKind; - use crate::init::policy_store::load_policy_store; - use crate::jwt::Token; - use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: Some(Token::new_id( - HashMap::from([ - ("sub".to_string(), json!("user-1")), - ("country".to_string(), json!("US")), - ]) - .into(), - None, - )), - userinfo_token: None, - }; - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect("expected to create user entity"); - assert_eq!( - result, - Entity::new( - "Jans::User::\"user-1\"" - .parse() - .expect("expected to create user UID"), - HashMap::from([( - "country".to_string(), - RestrictedExpression::new_string("US".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected user entity") - ) - } - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - id_token: None, - access_token: None, - userinfo_token: Some(Token::new_userinfo( - HashMap::from([ - ("sub".to_string(), json!("user-1")), - ("country".to_string(), json!("US")), - ]) - .into(), - None, - )), - }; - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect("expected to create user entity"); - assert_eq!( - result, - Entity::new( - "Jans::User::\"user-1\"" - .parse() - .expect("expected to create user UID"), - HashMap::from([( - "country".to_string(), - RestrictedExpression::new_string("US".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected user entity") - ) - } - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access(HashMap::from([]).into(), None)), - id_token: Some(Token::new_id(HashMap::from([]).into(), None)), - userinfo_token: Some(Token::new_userinfo(HashMap::from([]).into(), None)), - }; - - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect_err("expected to error while creating user entity"); - - for (tkn_kind, err) in result.errors.iter() { - match tkn_kind { - TokenKind::Access => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Id => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Userinfo => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Transaction => (), // we don't support these yet - } - } - } - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: None, - userinfo_token: None, - }; - - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect_err("expected to error while creating user entity"); - - assert_eq!(result.errors.len(), 2); - for (_tkn_kind, err) in result.errors.iter() { - assert!( - matches!(err, CreateCedarEntityError::UnavailableToken), - "expected error UnavailableToken, got: {:?}", - err - ); - } - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/workload.rs b/jans-cedarling/cedarling/src/authz/entities/workload.rs deleted file mode 100644 index 6ce1acafb8a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/workload.rs +++ /dev/null @@ -1,244 +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::collections::HashSet; -use std::fmt; - -use super::{CreateCedarEntityError, DecodedTokens, EntityMetadata, EntityParsedTypeName}; -use crate::common::policy_store::{PolicyStore, TokenKind}; -use crate::jwt::Token; - -/// Create workload entity -pub fn create_workload_entity( - entity_mapping: Option<&str>, - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result { - let namespace = policy_store.namespace(); - let schema = &policy_store.schema.json; - let mut errors = Vec::new(); - - // helper closure to attempt entity creation from a token - let try_create_entity = |token_kind: TokenKind, token: Option<&Token>, key: &str| { - if let Some(token) = token { - let claim_mapping = token.claim_mapping(); - let entity_metadta = EntityMetadata::new( - EntityParsedTypeName { - type_name: entity_mapping.unwrap_or("Workload"), - namespace, - }, - key, - ); - entity_metadta - .create_entity(schema, token, HashSet::new(), claim_mapping) - .map_err(|e| (token_kind, e)) - } else { - Err((token_kind, CreateCedarEntityError::UnavailableToken)) - } - }; - - // attempt entity creation for each token type - for (token_kind, token, key) in [ - (TokenKind::Access, tokens.access_token.as_ref(), "client_id"), - (TokenKind::Id, tokens.id_token.as_ref(), "aud"), - ] { - match try_create_entity(token_kind, token, key) { - Ok(entity) => return Ok(entity), - Err(e) => errors.push(e), - } - } - - Err(CreateWorkloadEntityError { errors }) -} - -#[derive(Debug, thiserror::Error)] -pub struct CreateWorkloadEntityError { - pub errors: Vec<(TokenKind, CreateCedarEntityError)>, -} - -impl fmt::Display for CreateWorkloadEntityError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.errors.is_empty() { - writeln!( - f, - "Failed to create Workload Entity since no tokens were provided" - )?; - } else { - writeln!( - f, - "Failed to create Workload Entity due to the following errors:" - )?; - for (token_kind, error) in &self.errors { - writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - use cedar_policy::{Entity, RestrictedExpression}; - use serde_json::json; - use test_utils::assert_eq; - - use super::create_workload_entity; - use crate::authz::entities::DecodedTokens; - use crate::common::policy_store::TokenKind; - use crate::init::policy_store::load_policy_store; - use crate::jwt::Token; - use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: Some(Token::new_id( - HashMap::from([ - ("aud".to_string(), json!("workload-1")), - ("org_id".to_string(), json!("some-org-123")), - ]) - .into(), - None, - )), - userinfo_token: None, - }; - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect("expected to create workload entity"); - assert_eq!( - result, - Entity::new( - "Jans::Workload::\"workload-1\"" - .parse() - .expect("expected to create workload UID"), - HashMap::from([( - "org_id".to_string(), - RestrictedExpression::new_string("some-org-123".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected workload entity") - ) - } - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access( - HashMap::from([ - ("client_id".to_string(), json!("workload-1")), - ("org_id".to_string(), json!("some-org-123")), - ]) - .into(), - None, - )), - id_token: None, - userinfo_token: None, - }; - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect("expected to create workload entity"); - assert_eq!( - result, - Entity::new( - "Jans::Workload::\"workload-1\"" - .parse() - .expect("expected to create workload UID"), - HashMap::from([( - "org_id".to_string(), - RestrictedExpression::new_string("some-org-123".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected workload entity") - ) - } - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access(HashMap::from([]).into(), None)), - id_token: Some(Token::new_id(HashMap::from([]).into(), None)), - userinfo_token: Some(Token::new_userinfo(HashMap::from([]).into(), None)), - }; - - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect_err("expected to error while creating workload entity"); - - for (tkn_kind, err) in result.errors.iter() { - match tkn_kind { - TokenKind::Access => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "client_id"), - "expected error MissingClaim(\"client_id\")" - ), - TokenKind::Id => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "aud"), - "expected error MissingClaim(\"aud\")" - ), - _ => (), // we don't create workload tokens using other tokens - } - } - } - - #[test] - 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(), - ), - }) - .expect("Should load policy store") - .store; - - // we can only create the workload from the access_token and id_token - let tokens = DecodedTokens { - access_token: None, - id_token: None, - userinfo_token: None, - }; - - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect_err("expected to error while creating workload entity"); - - assert_eq!(result.errors.len(), 2); - for (_tkn_kind, err) in result.errors.iter() { - assert!( - matches!(err, CreateCedarEntityError::UnavailableToken), - "expected error UnavailableToken, got: {:?}", - err - ); - } - } -} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder.rs b/jans-cedarling/cedarling/src/authz/entity_builder.rs new file mode 100644 index 00000000000..4e3d948a8a8 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder.rs @@ -0,0 +1,471 @@ +// 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. + +mod build_attrs; +mod build_expr; +mod build_resource_entity; +mod build_role_entity; +mod build_token_entities; +mod build_user_entity; +mod build_workload_entity; +mod mapping; + +use crate::common::cedar_schema::CEDAR_NAMESPACE_SEPARATOR; +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::policy_store::TokenKind; +use crate::jwt::{Token, TokenClaimTypeError}; +use crate::{AuthorizationConfig, ResourceData}; +use build_attrs::{BuildAttrError, ClaimAliasMap, build_entity_attrs_from_tkn}; +use build_expr::*; +use build_resource_entity::{BuildResourceEntityError, JsonTypeError}; +use build_role_entity::BuildRoleEntityError; +pub use build_token_entities::BuildTokenEntityError; +use build_user_entity::BuildUserEntityError; +use build_workload_entity::BuildWorkloadEntityError; +use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::convert::Infallible; +use std::fmt; +use std::str::FromStr; + +use super::AuthorizeEntitiesData; + +const DEFAULT_WORKLOAD_ENTITY_NAME: &str = "Workload"; +const DEFAULT_USER_ENTITY_NAME: &str = "User"; +const DEFAULT_ACCESS_TKN_ENTITY_NAME: &str = "Access_token"; +const DEFAULT_ID_TKN_ENTITY_NAME: &str = "id_token"; +const DEFAULT_USERINFO_TKN_ENTITY_NAME: &str = "Userinfo_token"; +const DEFAULT_ROLE_ENTITY_NAME: &str = "Role"; + +pub struct DecodedTokens<'a> { + pub access: Option>, + pub id: Option>, + pub userinfo: Option>, +} + +/// The names of the entities in the schema +pub struct EntityNames { + user: String, + workload: String, + id_token: String, + access_token: String, + userinfo_token: String, + role: String, +} + +impl From<&AuthorizationConfig> for EntityNames { + fn from(config: &AuthorizationConfig) -> Self { + Self { + user: config + .mapping_user + .clone() + .unwrap_or_else(|| DEFAULT_USER_ENTITY_NAME.to_string()), + workload: config + .mapping_workload + .clone() + .unwrap_or_else(|| DEFAULT_WORKLOAD_ENTITY_NAME.to_string()), + id_token: config + .mapping_id_token + .clone() + .unwrap_or_else(|| DEFAULT_ID_TKN_ENTITY_NAME.to_string()), + access_token: config + .mapping_access_token + .clone() + .unwrap_or_else(|| DEFAULT_ACCESS_TKN_ENTITY_NAME.to_string()), + userinfo_token: config + .mapping_userinfo_token + .clone() + .unwrap_or_else(|| DEFAULT_USERINFO_TKN_ENTITY_NAME.to_string()), + // TODO: implement a bootstrap property to set the Role entity name + role: DEFAULT_ROLE_ENTITY_NAME.to_string(), + } + } +} + +impl Default for EntityNames { + fn default() -> Self { + Self { + user: DEFAULT_USER_ENTITY_NAME.to_string(), + workload: DEFAULT_WORKLOAD_ENTITY_NAME.to_string(), + id_token: DEFAULT_ID_TKN_ENTITY_NAME.to_string(), + access_token: DEFAULT_ACCESS_TKN_ENTITY_NAME.to_string(), + userinfo_token: DEFAULT_USERINFO_TKN_ENTITY_NAME.to_string(), + role: DEFAULT_ROLE_ENTITY_NAME.to_string(), + } + } +} + +pub struct EntityBuilder { + schema: CedarSchemaJson, + entity_names: EntityNames, + build_workload: bool, + build_user: bool, +} + +impl EntityBuilder { + pub fn new( + schema: CedarSchemaJson, + entity_names: EntityNames, + build_workload: bool, + build_user: bool, + ) -> Self { + Self { + schema, + entity_names, + build_workload, + build_user, + } + } + + pub fn build_entities( + &self, + tokens: &DecodedTokens, + resource: &ResourceData, + ) -> Result { + let workload = if self.build_workload { + Some(self.build_workload_entity(tokens)?) + } else { + None + }; + + let (user, roles) = if self.build_user { + let roles = self.try_build_role_entities(tokens)?; + let parents = roles + .iter() + .map(|role| role.uid()) + .collect::>(); + (Some(self.build_user_entity(tokens, parents)?), roles) + } else { + (None, vec![]) + }; + + let access_token = if let Some(token) = tokens.access.as_ref() { + Some( + self.build_access_tkn_entity(token) + .map_err(BuildCedarlingEntityError::AccessToken)?, + ) + } else { + None + }; + + let id_token = if let Some(token) = tokens.id.as_ref() { + Some( + self.build_id_tkn_entity(token) + .map_err(BuildCedarlingEntityError::IdToken)?, + ) + } else { + None + }; + + let userinfo_token = if let Some(token) = tokens.userinfo.as_ref() { + Some( + self.build_userinfo_tkn_entity(token) + .map_err(BuildCedarlingEntityError::UserinfoToken)?, + ) + } else { + None + }; + + let resource = self.build_resource_entity(resource)?; + + Ok(AuthorizeEntitiesData { + workload, + user, + access_token, + id_token, + userinfo_token, + resource, + roles, + }) + } +} + +/// Builds a Cedar Entity using a JWT +fn build_entity( + schema: &CedarSchemaJson, + entity_name: &str, + token: &Token, + id_src_claim: &str, + claim_aliases: Vec, + parents: HashSet, +) -> Result { + // Get entity Id from the specified token claim + let entity_id = token + .get_claim(id_src_claim) + .ok_or(BuildEntityError::MissingClaim(id_src_claim.to_string()))? + .as_str()? + .to_owned(); + + // Get entity namespace and type + let mut entity_name = entity_name.to_string(); + let (namespace, entity_type) = schema + .get_entity_from_base_name(&entity_name) + .ok_or(BuildEntityError::EntityNotInSchema(entity_name.to_string()))?; + if !namespace.is_empty() { + entity_name = [namespace.as_str(), &entity_name].join(CEDAR_NAMESPACE_SEPARATOR); + } + + // Build entity attributes + let entity_attrs = build_entity_attrs_from_tkn(schema, entity_type, token, claim_aliases) + .map_err(BuildEntityError::BuildAttribute)?; + + // Build cedar entity + let entity_type_name = + EntityTypeName::from_str(&entity_name).map_err(BuildEntityError::ParseEntityTypeName)?; + let entity_id = EntityId::from_str(&entity_id).map_err(BuildEntityError::ParseEntityId)?; + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(Entity::new(entity_uid, entity_attrs, parents)?) +} + +/// Errors encountered when building a Cedarling-specific entity +#[derive(Debug, thiserror::Error)] +pub enum BuildCedarlingEntityError { + #[error(transparent)] + Workload(#[from] BuildWorkloadEntityError), + #[error(transparent)] + User(#[from] BuildUserEntityError), + #[error(transparent)] + Role(#[from] BuildRoleEntityError), + #[error("failed to build resource entity: {0}")] + Resource(#[from] BuildResourceEntityError), + #[error("error while building Access Token entity: {0}")] + AccessToken(#[source] BuildTokenEntityError), + #[error("error while building Id Token entity: {0}")] + IdToken(#[source] BuildTokenEntityError), + #[error("error while building Userinfo Token entity: {0}")] + UserinfoToken(#[source] BuildTokenEntityError), +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildEntityError { + #[error("failed to parse entity type name: {0}")] + ParseEntityTypeName(#[source] cedar_policy::ParseErrors), + #[error("failed to parse entity id: {0}")] + ParseEntityId(#[source] Infallible), + #[error("failed to evaluate entity or tag: {0}")] + AttrEvaluation(#[from] cedar_policy::EntityAttrEvaluationError), + #[error("failed to build entity since a token was not provided")] + TokenUnavailable, + #[error("the given token is missing a `{0}` claim")] + MissingClaim(String), + #[error(transparent)] + TokenClaimTypeMismatch(#[from] TokenClaimTypeError), + #[error(transparent)] + JsonTypeError(#[from] JsonTypeError), + #[error("the entity `{0}` is not defined in the schema")] + EntityNotInSchema(String), + #[error(transparent)] + BuildAttribute(#[from] BuildAttrError), + #[error("got {0} token, expected: {1}")] + InvalidToken(TokenKind, TokenKind), +} + +impl BuildEntityError { + pub fn json_type_err(expected_type_name: &str, got_value: &Value) -> Self { + Self::JsonTypeError(JsonTypeError::type_mismatch(expected_type_name, got_value)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::{cedar_schema::cedar_json::CedarSchemaJson, policy_store::TrustedIssuer}, + jwt::{Token, TokenClaims}, + }; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_entity_using_jwt() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let entity = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect("should successfully build entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn errors_on_invalid_entity_type_name() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload!": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + + let err = build_entity( + &schema, + "Workload!", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error while parsing entity type name"); + + assert!( + matches!(err, BuildEntityError::ParseEntityTypeName(_)), + "expected ParseEntityTypeName error but got: {:?}", + err + ); + } + + #[test] + fn errors_when_token_is_missing_entity_id_claim() { + let schema = serde_json::from_value::(json!({})) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error while parsing entity type name"); + + assert!( + matches!( + err, + BuildEntityError::MissingClaim(ref claim_name) + if claim_name =="client_id" + ), + "expected MissingClaim error but got: {}", + err + ); + } + + #[test] + fn errors_token_claim_has_unexpected_type() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([("client_id".to_string(), json!(123))])), + Some(&iss), + ); + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error due to unexpected json type"); + + assert!( + matches!( + err, + BuildEntityError::TokenClaimTypeMismatch(ref err) + if err == &TokenClaimTypeError::type_mismatch("client_id", "String", &json!(123)) + ), + "expected TokenClaimTypeMismatch error but got: {:?}", + err + ); + } + + #[test] + fn errors_when_entity_not_in_schema() { + let schema = serde_json::from_value::(json!({})) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("client-123"), + )])), + Some(&iss), + ); + + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error due to entity not being in the schema"); + assert!( + matches!( + err, + BuildEntityError::EntityNotInSchema(ref type_name) + if type_name == "Workload" + ), + "expected EntityNotInSchema error but got: {:?}", + err + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs new file mode 100644 index 00000000000..e56471cdbb8 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs @@ -0,0 +1,318 @@ +// 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 super::*; +use crate::{common::cedar_schema::cedar_json::entity_type::EntityType, jwt::Token}; +use cedar_policy::RestrictedExpression; +use serde_json::Value; +use std::collections::HashMap; + +/// Builds Cedar entity attributes using a JWT. +/// +/// This uses claim mapping metadata to unwrap claims into their respective Cedar types +pub fn build_entity_attrs_from_tkn( + schema: &CedarSchemaJson, + entity_type: &EntityType, + token: &Token, + claim_aliases: Vec, +) -> Result, BuildAttrError> { + let mut entity_attrs = HashMap::new(); + + let shape = match entity_type.shape.as_ref() { + Some(shape) => shape, + None => return Ok(entity_attrs), + }; + + let mut claims = token.claims_value().clone(); + apply_claim_aliases(&mut claims, claim_aliases); + + for (attr_name, attr) in shape.attrs.iter() { + let expression = if let Some(mapping) = token.claim_mapping().get(attr_name) { + let claim = claims.get(attr_name).ok_or_else(|| { + BuildAttrError::new( + attr_name, + BuildAttrErrorKind::MissingSource(attr_name.to_string()), + ) + })?; + let mapped_claim = mapping.apply_mapping(claim); + attr.build_expr(&mapped_claim, attr_name, schema) + .map_err(|err| BuildAttrError::new(attr_name, err.into()))? + } else { + match attr.build_expr(&claims, attr_name, schema) { + Ok(expr) => expr, + Err(err) if attr.is_required() => Err(BuildAttrError::new(attr_name, err.into()))?, + // silently fail when attribute isn't required + Err(_) => continue, + } + }; + + if let Some(expr) = expression { + entity_attrs.insert(attr_name.to_string(), expr); + } + } + + Ok(entity_attrs) +} + +pub fn build_entity_attrs_from_values( + schema: &CedarSchemaJson, + entity_type: &EntityType, + src: &HashMap, +) -> Result, BuildAttrError> { + let mut entity_attrs = HashMap::new(); + + let shape = match entity_type.shape.as_ref() { + Some(shape) => shape, + None => return Ok(entity_attrs), + }; + + for (attr_name, attr) in shape.attrs.iter() { + let val = match src.get(attr_name) { + Some(val) => val, + None if attr.is_required() => { + return Err(BuildAttrError::new( + attr_name, + BuildAttrErrorKind::MissingSource(attr_name.to_string()), + )); + }, + _ => continue, + }; + + let mapped_src = serde_json::from_value::>(val.clone()); + let src = if let Ok(mapped_src) = mapped_src.as_ref() { + mapped_src + } else { + src + }; + + let expression = match attr.build_expr(src, attr_name, schema) { + Ok(expr) => expr, + Err(err) if attr.is_required() => { + return Err(BuildAttrError::new(attr_name, err.into()))?; + }, + // move on to the next attribute if this isn't required + Err(_) => continue, + }; + + if let Some(expr) = expression { + entity_attrs.insert(attr_name.to_string(), expr); + } + } + + Ok(entity_attrs) +} + +/// Describes how to rename a claim named `from` to `to` +pub struct ClaimAliasMap<'a> { + from: &'a str, + to: &'a str, +} + +impl<'a> ClaimAliasMap<'a> { + pub fn new(from: &'a str, to: &'a str) -> Self { + Self { from, to } + } +} + +fn apply_claim_aliases(claims: &mut HashMap, aliases: Vec) { + for map in aliases { + if let Some(claim) = claims.get(map.from) { + claims.insert(map.to.to_string(), claim.clone()); + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to build `{attr_name}` attribute: {source}")] +pub struct BuildAttrError { + attr_name: String, + #[source] + source: BuildAttrErrorKind, +} + +impl BuildAttrError { + fn new(name: impl ToString, src: BuildAttrErrorKind) -> Self { + Self { + attr_name: name.to_string(), + source: src, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildAttrErrorKind { + #[error("missing attribute source: `{0}`")] + MissingSource(String), + #[error("failed to build restricted expression: {0}")] + BuildExpression(#[from] BuildExprError), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::{ + cedar_schema::cedar_json::{ + attribute::Attribute, + entity_type::{EntityShape, EntityType}, + }, + policy_store::TrustedIssuer, + }, + jwt::TokenClaims, + }; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_entity_attrs_from_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("workload-123"), + )])), + Some(&iss), + ); + + let attrs = build_entity_attrs_from_tkn(&schema, &entity_type, &token, Vec::new()) + .expect("should build entity attrs"); + // RestrictedExpression does not implement PartialEq so the best we can do is check + // if the attribute was created + assert!( + attrs.contains_key("client_id"), + "there should be a `client_id` attribute" + ); + } + + #[test] + fn errors_when_tkn_missing_src() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let iss = TrustedIssuer::default(); + let token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + + let err = build_entity_attrs_from_tkn(&schema, &entity_type, &token, Vec::new()) + .expect_err("should error due to missing source"); + assert!( + matches!( + err, + BuildAttrError { + attr_name: ref name, + source: BuildAttrErrorKind::BuildExpression(BuildExprError::MissingSource(ref src_name))} + if name == "client_id" && + src_name == "client_id" + ), + "expected MissingSource error but got: {:?}", + err, + ); + } + + #[test] + fn can_build_entity_attrs_from_value() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let src_values = HashMap::from([("client_id".to_string(), json!("workload-123"))]); + + let attrs = build_entity_attrs_from_values(&schema, &entity_type, &src_values) + .expect("should build entity attrs"); + // RestrictedExpression does not implement PartialEq so the best we can do is check + // if the attribute was created + assert!( + attrs.contains_key("client_id"), + "there should be a `client_id` attribute" + ); + } + + #[test] + fn errors_when_values_missing_src() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let src_values = HashMap::new(); + + let err = build_entity_attrs_from_values(&schema, &entity_type, &src_values) + .expect_err("should error due to missing source"); + assert!( + matches!( + err, + BuildAttrError{ + attr_name: ref name, + source: BuildAttrErrorKind::MissingSource(ref src_name)} + if name == "client_id" && + src_name == "client_id"), + "expected MissingSource error but got: {:?}", + err, + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs new file mode 100644 index 00000000000..2f8c67049db --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs @@ -0,0 +1,586 @@ +// 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::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::cedar_schema::cedar_json::attribute::Attribute; +use cedar_policy::{ + EntityId, EntityTypeName, EntityUid, ExpressionConstructionError, ParseErrors, + RestrictedExpression, +}; +use serde_json::Value; +use std::collections::HashMap; +use std::str::FromStr; + +use super::CEDAR_NAMESPACE_SEPARATOR; + +impl Attribute { + pub fn kind_str(&self) -> &str { + match self { + Attribute::String { .. } => "String", + Attribute::Long { .. } => "Long", + Attribute::Boolean { .. } => "Boolean", + Attribute::Record { .. } => "Record", + Attribute::Set { .. } => "Set", + Attribute::Entity { .. } => "Entity", + Attribute::Extension { .. } => "Extension", + Attribute::EntityOrCommon { .. } => "EntityOrCommon", + } + } + + /// Builds a [`RestrictedExpression`] while checking the schema + pub fn build_expr( + &self, + attr_src: &HashMap, + src_key: &str, + schema: &CedarSchemaJson, + ) -> Result, BuildExprError> { + match self { + // Handle String attributes + Attribute::String { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))? + .to_string(); + Ok(Some(RestrictedExpression::new_string(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Long attributes + Attribute::Long { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_i64() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "number", claim))?; + Ok(Some(RestrictedExpression::new_long(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Boolean attributes + Attribute::Boolean { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_bool() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "bool", claim))?; + Ok(Some(RestrictedExpression::new_bool(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Record attributes + Attribute::Record { attrs, required } => { + let mut fields = HashMap::new(); + for (name, kind) in attrs.iter() { + if let Some(expr) = kind.build_expr(attr_src, name, schema)? { + fields.insert(name.to_string(), expr); + } + } + + if fields.is_empty() && !required { + Ok(None) + } else { + Ok(Some(RestrictedExpression::new_record(fields)?)) + } + }, + + // Handle Set attributes + Attribute::Set { required, element } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_array() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "array", claim))?; + + let mut values = Vec::new(); + for (i, val) in claim.iter().enumerate() { + let claim_name = i.to_string(); + if let Some(expr) = element.build_expr( + &HashMap::from([(claim_name.clone(), val.clone())]), + &claim_name, + schema, + )? { + values.push(expr); + } + } + Ok(Some(RestrictedExpression::new_set(values))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Entity attributes + Attribute::Entity { required, name } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))?; + + let mut name = name.to_string(); + if let Some((namespace, _)) = schema.get_entity_from_base_name(&name) { + if !namespace.is_empty() { + name = [namespace, name.as_str()].join(CEDAR_NAMESPACE_SEPARATOR); + } + } else if *required { + return Err(BuildExprError::EntityNotInSchema(name.to_string())); + } else { + return Ok(None); + } + + let type_name = EntityTypeName::from_str(&name) + .map_err(|e| BuildExprError::ParseEntityTypeName(name, e))?; + let type_id = EntityId::new(claim); + let uid = EntityUid::from_type_name_and_id(type_name, type_id); + Ok(Some(RestrictedExpression::new_entity_uid(uid))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Extension attributes + Attribute::Extension { required, name } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))?; + let expr = match name.as_str() { + "ipaddr" => RestrictedExpression::new_ip(claim), + "decimal" => RestrictedExpression::new_decimal(claim), + name => RestrictedExpression::new_unknown(name), + }; + Ok(Some(expr)) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle EntityOrCommon attributes + Attribute::EntityOrCommon { required, name } => { + if let Some((_namespace_name, attr)) = schema.get_common_type(name) { + attr.build_expr(attr_src, src_key, schema) + } else if schema.get_entity_from_base_name(name).is_some() { + let attr = Attribute::Entity { + required: *required, + name: name.to_string(), + }; + attr.build_expr(attr_src, src_key, schema) + } else if let Some(attr) = str_to_primitive_type(*required, name) { + attr.build_expr(attr_src, src_key, schema) + } else if *required { + Err(BuildExprError::UnkownType(name.to_string())) + } else { + Ok(None) + } + }, + } + } +} + +fn str_to_primitive_type(required: bool, name: &str) -> Option { + let primitive_type = match name { + "String" => Attribute::String { required }, + "Long" => Attribute::Long { required }, + "Boolean" => Attribute::Boolean { required }, + _ => return None, + }; + Some(primitive_type) +} + +/// Errors when building a [`RestrictedExpression`] +#[derive(Debug, thiserror::Error)] +pub enum BuildExprError { + #[error("the given attribute source data is missing the key: {0}")] + MissingSource(String), + #[error(transparent)] + TypeMismatch(#[from] KeyedJsonTypeError), + #[error(transparent)] + ConstructionError(#[from] ExpressionConstructionError), + #[error("the type of `{0}` could not be determined")] + UnkownType(String), + #[error("the entity type `{0}` is not in the schema")] + EntityNotInSchema(String), + #[error("failed to parse entity type name \"{0}\": {1}")] + ParseEntityTypeName(String, ParseErrors), +} + +#[derive(Debug, thiserror::Error)] +#[error("type mismatch for key '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] +pub struct KeyedJsonTypeError { + pub key: String, + pub expected_type: String, + pub actual_type: String, +} + +impl KeyedJsonTypeError { + /// Returns the JSON type name of the given value. + pub fn value_type_name(value: &Value) -> &'static str { + match value { + Value::Null => "null", + Value::Bool(_) => "bool", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Array(_) => "array", + Value::Object(_) => "object", + } + } + + /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. + pub fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { + let got_value_type_name = Self::value_type_name(got_value).to_string(); + + Self { + key: key.to_string(), + expected_type: expected_type_name.to_string(), + actual_type: got_value_type_name, + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + authz::entity_builder::BuildExprError, + common::cedar_schema::cedar_json::{CedarSchemaJson, attribute::Attribute}, + }; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_string_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("attr-val"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_long_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Long" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::long(); + let src = HashMap::from([("src_key".to_string(), json!(123))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_boolean_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Boolean" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::boolean(); + let src = HashMap::from([("src_key".to_string(), json!(true))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_record_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "outer_attr": { + "type": "Record", + "attributes": { + "inner_attr": { "type": "String" } + }, + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::record(HashMap::from([( + "inner_attr".to_string(), + Attribute::string(), + )])); + let src = HashMap::from([("inner_attr".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_set_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Set", + "element": { + "type": "String", + } + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::set(Attribute::string()); + let src = HashMap::from([("src_key".to_string(), json!(["admin", "user"]))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn errors_when_expected_set_has_different_types() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Set", + "element": { + "type": "String", + } + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::set(Attribute::string()); + let src = HashMap::from([("src_key".to_string(), json!(["admin", 123]))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!(err, BuildExprError::TypeMismatch(_)), + "should error due to type mismatch but got: {:?}", + err + ); + } + + #[test] + fn can_build_entity_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { + "OtherEntity": {}, + "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Entity", + "name": "OtherEntity", + }, + }, + } + } + }} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built"); + } + + #[test] + fn can_build_entity_expr_from_entity_or_common() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { + "OtherEntity": {}, + "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "EntityOrCommon", + "name": "OtherEntity", + }, + }, + } + } + }} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built"); + } + + #[test] + fn errors_when_entity_isnt_in_schema() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Entity", + "name": "OtherEntity", + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!( + err, + BuildExprError::EntityNotInSchema(ref entity_name) + if entity_name == "OtherEntity" + ), + "should error due to type mismatch but got: {:?}", + err + ); + } + + #[test] + fn can_build_ip_addr_extension_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Extension", "name": "ipaddr" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("0.0.0.0"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_decimal_extension_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Extension", "name": "decimal" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("1.1"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_skip_non_required_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::String { required: false }; + let src = HashMap::new(); + let expr = attr + .build_expr(&src, "client_id", &schema) + .expect("should not error"); + assert!(expr.is_none(), "a restricted expression shouldn't built") + } + + #[test] + fn errors_on_type_mismatch() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!(123))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!(err, BuildExprError::TypeMismatch(_)), + "should error due to type mismatch but got: {:?}", + err + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs new file mode 100644 index 00000000000..6878749b1c1 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs @@ -0,0 +1,331 @@ +// 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 build_attrs::build_entity_attrs_from_values; +use cedar_policy::{EntityAttrEvaluationError, ExpressionConstructionError, ParseErrors}; +use serde_json::Value; + +use super::*; +use crate::ResourceData; + +impl EntityBuilder { + pub fn build_resource_entity( + &self, + resource: &ResourceData, + ) -> Result { + let entity_type_name = EntityTypeName::from_str(&resource.resource_type)?; + let (_namespace_name, entity_type) = self + .schema + .get_entity_from_base_name(entity_type_name.basename()) + .ok_or(BuildEntityError::EntityNotInSchema( + entity_type_name.to_string(), + ))?; + + let entity_attrs = + build_entity_attrs_from_values(&self.schema, entity_type, &resource.payload)?; + + // Build cedar entity + let entity_id = + EntityId::from_str(&resource.id).map_err(BuildEntityError::ParseEntityId)?; + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(Entity::new(entity_uid, entity_attrs, HashSet::new())?) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildResourceEntityError { + #[error(transparent)] + BuildEntity(#[from] BuildEntityError), + #[error(transparent)] + TypeMismatch(#[from] JsonTypeError), + #[error(transparent)] + ExpressionConstructExpression(#[from] ExpressionConstructionError), + #[error(transparent)] + EntityAttrEvaluationError(#[from] EntityAttrEvaluationError), + #[error(transparent)] + BuildAttr(#[from] BuildAttrError), + #[error("invalid entity name: {0}")] + InvalidEntityName(#[from] ParseErrors), +} + +#[derive(Debug, thiserror::Error, PartialEq)] +#[error("JSON value type mismatch: expected '{expected_type}', but found '{actual_type}'")] +pub struct JsonTypeError { + pub expected_type: String, + pub actual_type: String, +} + +impl JsonTypeError { + /// Returns the JSON type name of the given value. + pub fn value_type_name(value: &Value) -> String { + match value { + Value::Null => "null".to_string(), + Value::Bool(_) => "bool".to_string(), + Value::Number(_) => "number".to_string(), + Value::String(_) => "string".to_string(), + Value::Array(_) => "array".to_string(), + Value::Object(_) => "object".to_string(), + } + } + + /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. + pub fn type_mismatch(expected_type_name: &str, got_value: &Value) -> Self { + let got_value_type_name = Self::value_type_name(got_value); + + Self { + expected_type: expected_type_name.to_string(), + actual_type: got_value_type_name, + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use cedar_policy::EvalResult; + use serde_json::json; + + #[test] + fn can_build_entity() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "header": { + "type": "Record", + "attributes": { + "Accept": { "type": "EntityOrCommon", "name": "String" }, + }, + }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::from([ + ("header".to_string(), json!({"Accept": "test"})), + ( + "url".to_string(), + json!({"host": "protected.host", "protocol": "http", "path": "/protected"}), + ), + ]), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + let url = entity + .attr("url") + .expect("entity must have an `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record + .get("host") + .expect("expected `url` to have a `host` attribute"), + &EvalResult::String("protected.host".to_string()) + ); + assert_eq!( + record + .get("protocol") + .expect("expected `url` to have a `domain` attribute"), + &EvalResult::String("http".to_string()) + ); + assert_eq!( + record + .get("path") + .expect("expected `url` to have a `path` attribute"), + &EvalResult::String("/protected".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + url + ); + } + + let header = entity + .attr("header") + .expect("entity must have an `header` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = header { + assert_eq!(record.len(), 1); + assert_eq!( + record + .get("Accept") + .expect("expected `url` to have an `Accept` attribute"), + &EvalResult::String("test".to_string()) + ); + } else { + panic!( + "expected the attribute `header` to be a record, got: {:?}", + header + ); + } + } + + #[test] + fn can_build_entity_with_optional_attr() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "url": { "type": "EntityOrCommon", "name": "Url", "required": false}, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::new(), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + assert!( + entity.attr("url").is_none(), + "entity should not have a `url` attribute" + ); + } + + #[test] + fn can_build_entity_with_optional_record_attr() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "header": { + "type": "Record", + "attributes": { + "Accept": { "type": "EntityOrCommon", "name": "String", "required": false }, + }, + }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::from([ + ( + "url".to_string(), + json!({"host": "protected.host", "protocol": "http", "path": "/protected"}), + ), + ("header".to_string(), json!({})), + ]), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + let url = entity + .attr("url") + .expect("entity must have an `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record + .get("host") + .expect("expected `url` to have a `host` attribute"), + &EvalResult::String("protected.host".to_string()) + ); + assert_eq!( + record + .get("protocol") + .expect("expected `url` to have a `domain` attribute"), + &EvalResult::String("http".to_string()) + ); + assert_eq!( + record + .get("path") + .expect("expected `url` to have a `path` attribute"), + &EvalResult::String("/protected".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + url + ); + } + + let header = entity + .attr("header") + .expect("entity must have an `header` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = header { + assert_eq!(record.len(), 0, "the header attribute must be empty"); + } else { + panic!( + "expected the attribute `header` to be a record, got: {:?}", + header + ); + } + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs new file mode 100644 index 00000000000..0be6ba7a284 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs @@ -0,0 +1,286 @@ +// 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 super::*; +use cedar_policy::{EntityId, EntityTypeName, EntityUid}; +use serde::Deserialize; + +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +enum UnifyClaims { + Single(String), + Multiple(Vec), +} + +impl UnifyClaims { + fn iter<'a>(&'a self) -> Box + 'a> { + match self { + Self::Single(ref v) => Box::new(std::iter::once(v)), + Self::Multiple(ref vs) => Box::new(vs.iter()), + } + } +} + +impl EntityBuilder { + /// Tries to build role entities using each given token. Will return an empty Vec + /// if no entities were created. + pub fn try_build_role_entities( + &self, + tokens: &DecodedTokens, + ) -> Result, BuildRoleEntityError> { + // Get entity namespace and type + let mut entity_name = self.entity_names.role.to_string(); + if let Some((namespace, _entity_type)) = self.schema.get_entity_from_base_name(&entity_name) + { + if !namespace.is_empty() { + entity_name = [namespace.as_str(), &entity_name].join(CEDAR_NAMESPACE_SEPARATOR); + } + } + + let mut entities = HashMap::new(); + + let token_refs = [ + tokens.userinfo.as_ref(), + tokens.id.as_ref(), + tokens.access.as_ref(), + ]; + for token in token_refs.into_iter().flatten() { + let role_claim = token.role_mapping(); + if let Some(claim) = token.get_claim(role_claim).as_ref() { + let unified_claims = UnifyClaims::deserialize(claim.value()); + let claim_role_name_iter = match unified_claims { + Ok(ref unified_claims) => unified_claims.iter(), + Err(_) => { + return Err(BuildRoleEntityError::map_tkn_err( + token, + BuildEntityError::TokenClaimTypeMismatch( + TokenClaimTypeError::type_mismatch( + role_claim, + "String or Array", + claim.value(), + ), + ), + )) + }, + }; + + for claim_role_name in claim_role_name_iter { + if !entities.contains_key(claim_role_name) { + let entity = build_entity(&entity_name, claim_role_name) + .map_err(|e| BuildRoleEntityError::map_tkn_err(token, e))?; + entities.insert(claim_role_name.clone(), entity); + } + } + } + } + + Ok(entities.into_values().collect()) + } +} + +fn build_entity(name: &str, id: &str) -> Result { + let name = EntityTypeName::from_str(name).map_err(BuildEntityError::ParseEntityTypeName)?; + let id = EntityId::from_str(id).map_err(BuildEntityError::ParseEntityId)?; + let uid = EntityUid::from_type_name_and_id(name, id); + let entity = Entity::new(uid, HashMap::new(), HashSet::new())?; + Ok(entity) +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildRoleEntityError { + #[error("failed to build role entity from access token: {0}")] + Access(#[source] BuildEntityError), + #[error("failed to build role entity from id token: {0}")] + Id(#[source] BuildEntityError), + #[error("failed to build role entity from userinfo token: {0}")] + Userinfo(#[source] BuildEntityError), +} + +impl BuildRoleEntityError { + pub fn map_tkn_err(token: &Token, err: BuildEntityError) -> Self { + match token.kind { + TokenKind::Access => BuildRoleEntityError::Access(err), + TokenKind::Id => BuildRoleEntityError::Id(err), + TokenKind::Userinfo => BuildRoleEntityError::Userinfo(err), + TokenKind::Transaction => unimplemented!("transaction tokens are not yet supported"), + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::TrustedIssuer; + use crate::jwt::{Token, TokenClaims}; + use serde_json::json; + use std::collections::HashMap; + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "entityTypes": { + "Role": {}, + "User": { + "memberOfTypes": ["Role"], + "shape": { + "type": "Record", + "attributes": {}, + } + }}} + })) + .expect("should successfully create test schema") + } + + fn test_build_entity_from_str_claim(tokens: DecodedTokens) { + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entity = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + assert_eq!(entity.len(), 1); + assert_eq!(entity[0].uid().to_string(), "Jans::Role::\"admin\""); + } + + #[test] + fn can_build_using_userinfo_tkn_vec_claim() { + let iss = TrustedIssuer::default(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "role".to_string(), + json!(["admin", "user"]), + )])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entity = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + assert_eq!(entity.len(), 2); + let entity_uids = entity + .iter() + .map(|e| e.uid().to_string()) + .collect::>(); + assert_eq!( + entity_uids, + HashSet::from(["Jans::Role::\"admin\"", "Jans::Role::\"user\""].map(|s| s.to_string())) + ); + } + + #[test] + fn can_build_using_userinfo_tkn_string_claim() { + let iss = TrustedIssuer::default(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_build_using_id_tkn() { + let iss = TrustedIssuer::default(); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_build_using_access_tkn() { + let iss = TrustedIssuer::default(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn ignores_duplicate_roles() { + let iss = TrustedIssuer::default(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_create_multiple_different_roles_from_different_tokens() { + let iss = TrustedIssuer::default(); + let schema = test_schema(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("role1"))])), + Some(&iss), + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("role2"))])), + Some(&iss), + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "role".to_string(), + json!(["role3", "role4"]), + )])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entities = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + let entities = entities + .iter() + .map(|e| e.uid().to_string()) + .collect::>(); + let expected_entities = (1..=4) + .map(|x| format!("Jans::Role::\"role{}\"", x)) + .collect::>(); + assert_eq!(entities, expected_entities); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs new file mode 100644 index 00000000000..38be043fc10 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs @@ -0,0 +1,368 @@ +// 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 super::*; + +const DEFAULT_TKN_PRINCIPAL_IDENTIFIER: &str = "jti"; + +impl EntityBuilder { + pub fn build_access_tkn_entity(&self, token: &Token) -> Result { + if token.kind != TokenKind::Access { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Access, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Access), + }); + } + let entity_name = self.entity_names.access_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + pub fn build_id_tkn_entity(&self, token: &Token) -> Result { + if token.kind != TokenKind::Id { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Id, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Id), + }); + } + let entity_name = self.entity_names.id_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + pub fn build_userinfo_tkn_entity( + &self, + token: &Token, + ) -> Result { + if token.kind != TokenKind::Userinfo { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Userinfo, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Userinfo), + }); + } + let entity_name = self.entity_names.userinfo_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + fn build_tkn_entity( + &self, + entity_name: &str, + token: &Token, + ) -> Result { + let id_src_claim = token + .metadata() + .principal_identifier + .as_deref() + .unwrap_or(DEFAULT_TKN_PRINCIPAL_IDENTIFIER); + build_entity( + &self.schema, + entity_name, + token, + id_src_claim, + vec![], + HashSet::new(), + ) + .map_err(|err| BuildTokenEntityError { + token_kind: token.kind, + err, + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to create {token_kind} token entity: {err}")] +pub struct BuildTokenEntityError { + pub token_kind: TokenKind, + pub err: BuildEntityError, +} + +impl BuildTokenEntityError { + pub fn access_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Access, + err: BuildEntityError::TokenUnavailable, + } + } + + pub fn id_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Id, + err: BuildEntityError::TokenUnavailable, + } + } + + pub fn userinfo_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Userinfo, + err: BuildEntityError::TokenUnavailable, + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ClaimMappings, TokenEntityMetadata, TrustedIssuer}; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Access_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "id_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "Userinfo_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { "type": "EntityOrCommon", "name": "Url" } + }, + } + } + } + }})) + .expect("should deserialize schema") + } + + fn test_issusers() -> HashMap { + let token_entity_metadata = TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(), + ..Default::default() + }; + let iss = TrustedIssuer { + access_tokens: token_entity_metadata.clone(), + id_tokens: token_entity_metadata.clone(), + userinfo_tokens: token_entity_metadata, + ..Default::default() + }; + let issuers = HashMap::from([("test_iss".into(), iss.clone())]); + issuers + } + + fn test_build_entity(tkn_entity_type_name: &str, token: Token, build_tkn_entity_fn: F) + where + F: FnOnce(&Token) -> Result, + { + let entity = + build_tkn_entity_fn(&token).expect("expected to successfully build token entity"); + + assert_eq!( + entity.uid().to_string(), + format!("Jans::{}::\"tkn-123\"", tkn_entity_type_name) + ); + + assert_eq!( + entity + .attr("jti") + .expect("expected entity to have a `jti` attribute") + .unwrap(), + EvalResult::String("tkn-123".to_string()), + ); + + let trusted_iss = entity + .attr("trusted_issuer") + .expect("expected entity to have a `trusted_issuer` attribute") + .unwrap(); + if let EvalResult::EntityUid(ref uid) = trusted_iss { + assert_eq!(uid.type_name().basename(), "TrustedIssuer"); + assert_eq!( + uid.id().escaped(), + "https://some-iss.com/.well-known/openid-configuration" + ); + } else { + panic!( + "expected the attribute `trusted_issuer` to be an EntityUid, got: {:?}", + trusted_iss + ); + } + } + + #[test] + fn can_build_access_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("Access_token", access_token, |tkn| { + builder.build_access_tkn_entity(tkn) + }); + } + + #[test] + fn can_build_id_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("id_token", id_token, |tkn| builder.build_id_tkn_entity(tkn)); + } + + #[test] + fn can_build_userinfo_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("Userinfo_token", userinfo_token, |tkn| { + builder.build_userinfo_tkn_entity(tkn) + }); + } + + #[test] + fn errors_when_given_incorrect_tkn_kind() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let tkn_claims = TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])); + let iss = Some(issuers.get("test_iss").unwrap()); + let access_token = Token::new_access(tkn_claims.clone(), iss); + let id_token = Token::new_id(tkn_claims.clone(), iss); + let userinfo_token = Token::new_userinfo(tkn_claims, iss); + + for tkn in [&id_token, &userinfo_token].iter() { + let err = builder + .build_access_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Access && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Access + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + + for tkn in [&access_token, &userinfo_token].iter() { + let err = builder + .build_id_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Id && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Id + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + + for tkn in [&access_token, &id_token].iter() { + let err = builder + .build_userinfo_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Userinfo && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Userinfo + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs new file mode 100644 index 00000000000..d22e45b87f3 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs @@ -0,0 +1,291 @@ +// 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 super::*; +use cedar_policy::Entity; +use std::collections::HashSet; + +impl EntityBuilder { + pub fn build_user_entity( + &self, + tokens: &DecodedTokens, + parents: HashSet, + ) -> Result { + let entity_name = self.entity_names.user.as_ref(); + let mut errors = vec![]; + + for token in [tokens.userinfo.as_ref(), tokens.id.as_ref()] + .iter() + .flatten() + { + let user_id_claim = token.user_mapping(); + match build_entity( + &self.schema, + entity_name, + token, + user_id_claim, + vec![], + parents.clone(), + ) { + Ok(entity) => return Ok(entity), + Err(err) => errors.push((token.kind, err)), + } + } + + Err(BuildUserEntityError { errors }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct BuildUserEntityError { + pub errors: Vec<(TokenKind, BuildEntityError)>, +} + +impl fmt::Display for BuildUserEntityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.errors.is_empty() { + writeln!( + f, + "failed to create User Entity since no tokens were provided" + )?; + } else { + writeln!( + f, + "failed to create User Entity due to the following errors:" + )?; + for (token_kind, error) in &self.errors { + writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ClaimMappings, TokenEntityMetadata, TrustedIssuer}; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + fn test_iss() -> TrustedIssuer { + let token_entity_metadata = TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "email": { + "parser": "regex", + "type": "Jans::Email", + "regex_expression" : "^(?P[^@]+)@(?P.+)$", + "UID": {"attr": "uid", "type":"String"}, + "DOMAIN": {"attr": "domain", "type":"String"}, + }, + })) + .unwrap(), + ..Default::default() + }; + TrustedIssuer { + id_tokens: token_entity_metadata.clone(), + userinfo_tokens: token_entity_metadata, + ..Default::default() + } + } + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Email": { + "type": "Record", + "attributes": { + "uid": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "User": { + "memberOf": ["Role"], + "shape": { + "type": "Record", + "attributes": { + "email": { "type": "EntityOrCommon", "name": "Email" }, + "sub": { "type": "String" }, + }, + } + } + } + }})) + .expect("should successfully create test schema") + } + + fn test_successfully_building_user_entity(tokens: DecodedTokens) { + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let entity = builder + .build_user_entity(&tokens, HashSet::new()) + .expect("expected to build user entity"); + + assert_eq!(entity.uid().to_string(), "Jans::User::\"user-123\""); + + assert_eq!( + entity.attr("sub").unwrap().unwrap(), + EvalResult::String("user-123".to_string()), + ); + + let email = entity + .attr("email") + .expect("entity must have an `email` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = email { + assert_eq!(record.len(), 2); + assert_eq!( + record.get("uid").unwrap(), + &EvalResult::String("test".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("email.com".to_string()) + ); + } else { + panic!( + "expected the attribute `email` to be a record, got: {:?}", + email + ); + } + } + + #[test] + fn can_build_using_userinfo_tkn() { + let iss = test_iss(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("email".to_string(), json!("test@email.com")), + ("sub".to_string(), json!("user-123")), + ("role".to_string(), json!(["admin", "user"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + test_successfully_building_user_entity(tokens); + } + + #[test] + fn can_build_using_id_tkn() { + let iss = test_iss(); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("email".to_string(), json!("test@email.com")), + ("sub".to_string(), json!("user-123")), + ("role".to_string(), json!(["admin", "user"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + test_successfully_building_user_entity(tokens); + } + + #[test] + fn errors_when_token_has_missing_claim() { + let iss = test_iss(); + let schema = test_schema(); + + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), Some(&iss)); + let userinfo_token = Token::new_userinfo(TokenClaims::new(HashMap::new()), Some(&iss)); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let err = builder + .build_user_entity(&tokens, HashSet::new()) + .expect_err("expected to error while building the user entity"); + + assert_eq!(err.errors.len(), 2); + for (i, expected_kind) in [TokenKind::Userinfo, TokenKind::Id].iter().enumerate() { + assert!( + matches!( + err.errors[i], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == expected_kind && + claim_name == "sub" + ), + "expected an error due to missing the `sub` claim, got: {:?}", + err.errors[i] + ); + } + } + + #[test] + fn errors_when_tokens_unavailable() { + let schema = test_schema(); + + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: None, + }; + + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let err = builder + .build_user_entity(&tokens, HashSet::new()) + .expect_err("expected to error while building the user entity"); + + assert_eq!(err.errors.len(), 0); + } + + #[test] + fn can_build_entity_with_roles() { + let iss = test_iss(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("sub".to_string(), json!("user-123")), + ("email".to_string(), json!("someone@email.com")), + ("role".to_string(), json!(["role1", "role2", "role3"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let roles = HashSet::from([ + "Role::\"role1\"".parse().unwrap(), + "Role::\"role2\"".parse().unwrap(), + "Role::\"role3\"".parse().unwrap(), + ]); + + let user_entity = builder + .build_user_entity(&tokens, roles.clone()) + .expect("expected to build user entity"); + + let (_, _, parents) = user_entity.into_inner(); + assert_eq!(parents, roles,); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs new file mode 100644 index 00000000000..3f444413a16 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs @@ -0,0 +1,463 @@ +// 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 super::*; +use cedar_policy::Entity; +use std::collections::HashSet; + +// Default claims to use for the Workload Entity's ID. +const DEFAULT_ACCESS_TKN_WORKLOAD_CLAIM: &str = "client_id"; +const DEFAULT_ID_TKN_WORKLOAD_CLAIM: &str = "aud"; + +impl EntityBuilder { + pub fn build_workload_entity( + &self, + tokens: &DecodedTokens, + ) -> Result { + let entity_name = self.entity_names.workload.as_ref(); + let mut errors = vec![]; + + for (workload_id_claim, token_option, claim_aliases) in [ + ( + DEFAULT_ACCESS_TKN_WORKLOAD_CLAIM, + tokens.access.as_ref(), + vec![], + ), + ( + DEFAULT_ID_TKN_WORKLOAD_CLAIM, + tokens.id.as_ref(), + vec![ClaimAliasMap::new("aud", "client_id")], + ), + ] + .into_iter() + { + if let Some(token) = token_option { + match build_entity( + &self.schema, + entity_name, + token, + workload_id_claim, + claim_aliases, + HashSet::new(), + ) { + Ok(entity) => return Ok(entity), + Err(err) => errors.push((token.kind, err)), + } + } + } + + Err(BuildWorkloadEntityError { errors }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct BuildWorkloadEntityError { + pub errors: Vec<(TokenKind, BuildEntityError)>, +} + +impl fmt::Display for BuildWorkloadEntityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.errors.is_empty() { + writeln!( + f, + "failed to create Workload Entity since no tokens were provided" + )?; + } else { + writeln!( + f, + "failed to create Workload Entity due to the following errors:" + )?; + for (token_kind, error) in &self.errors { + writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::authz::entity_builder::BuildEntityError; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ + ClaimMappings, TokenEntityMetadata, TokenKind, TrustedIssuer, + }; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_using_access_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expeted to successfully build workload entity"); + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn can_build_using_id_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("aud".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn can_build_expression_with_regex_mapping() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Email": { + "type": "Record", + "attributes": { + "uid": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "email": { "type": "EntityOrCommon", "name": "Email" }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + }} + })) + .unwrap(); + let iss = TrustedIssuer { + access_tokens: TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "email": { + "parser": "regex", + "type": "Jans::Email", + "regex_expression" : "^(?P[^@]+)@(?P.+)$", + "UID": {"attr": "uid", "type":"String"}, + "DOMAIN": {"attr": "domain", "type":"String"}, + }, + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(), + ..Default::default() + }, + ..Default::default() + }; + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("email".to_string(), json!("test@example.com")), + ("url".to_string(), json!("https://test.com/example")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + + assert_eq!( + entity + .attr("client_id") + .expect("expected to workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + + let email = entity + .attr("email") + .expect("expected workload entity to have an `email` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = email { + assert_eq!(record.len(), 2); + assert_eq!( + record.get("uid").unwrap(), + &EvalResult::String("test".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("example.com".to_string()) + ); + } else { + panic!( + "expected the attribute `email` to be a record, got: {:?}", + email + ); + } + + let url = entity + .attr("url") + .expect("entity must have a `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record.get("scheme").unwrap(), + &EvalResult::String("https".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("test.com".to_string()) + ); + assert_eq!( + record.get("path").unwrap(), + &EvalResult::String("/example".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + email + ); + } + } + + #[test] + fn can_build_entity_with_entity_ref() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "entityTypes": { + "TrustedIss": {}, + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "iss": { "type": "EntityOrCommon", "name": "TrustedIss" }, + }, + } + } + }} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ( + "iss".to_string(), + json!("https://test.com/.well-known/openid-configuration"), + ), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + + assert_eq!( + entity + .attr("client_id") + .expect("expected to workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + + let iss = entity + .attr("iss") + .expect("entity must have a `iss` attribute") + .unwrap(); + if let EvalResult::EntityUid(uid) = iss { + assert_eq!(uid.type_name().namespace(), "Jans"); + assert_eq!(uid.type_name().basename(), "TrustedIss"); + assert_eq!( + uid.id().escaped(), + "https://test.com/.well-known/openid-configuration" + ); + } else { + panic!( + "expected the attribute `iss` to be an EntityUid, got: {:?}", + iss + ); + } + } + + #[test] + fn errors_when_token_has_missing_claim() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), Some(&iss)); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + let err = builder + .build_workload_entity(&tokens) + .expect_err("expected to error while building the workload entity"); + + assert_eq!(err.errors.len(), 2); + assert!( + matches!( + err.errors[0], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == &TokenKind::Access && + claim_name == "client_id" + ), + "expected an error due to missing the `client_id` claim" + ); + assert!( + matches!( + err.errors[1], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == &TokenKind::Id && + claim_name == "aud" + ), + "expected an error due to missing the `aud` claim" + ); + } + + #[test] + fn errors_when_tokens_unavailable() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: None, + }; + let err = builder.build_workload_entity(&tokens).unwrap_err(); + + assert_eq!(err.errors.len(), 0); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs b/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs new file mode 100644 index 00000000000..0f1edb8d466 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/mapping.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::common::policy_store::ClaimMappings; +use serde_json::Value; +use std::collections::HashMap; + +impl ClaimMappings { + /// Creates new claims and adds it to the HashMap of the given claims + /// if a mapping exists + /// + /// * Note that this will overwrite existing names + pub fn apply_mapping(&self, claims: &HashMap) -> HashMap { + let mut mapped_claims = HashMap::new(); + for (name, claim) in claims.iter() { + if let Some(mapping) = self.get(name) { + let applied_mapping = mapping.apply_mapping(claim); + mapped_claims.extend(applied_mapping); + } + } + mapped_claims + } +} + +#[cfg(test)] +mod test { + use crate::common::policy_store::ClaimMappings; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + #[test] + fn can_apply_mapping() { + let claims = HashMap::from([ + ("email".to_string(), json!("test@test.com")), + ("url".to_string(), json!("https://example.com/test")), + ]); + let claim_mapping = serde_json::from_value::(json!({ + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(); + let mapped_claims = claim_mapping.apply_mapping(&claims); + assert_eq!( + mapped_claims, + HashMap::from([ + ("scheme".to_string(), json!("https")), + ("domain".to_string(), json!("example.com")), + ("path".to_string(), json!("/test")), + ]) + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/merge_json.rs b/jans-cedarling/cedarling/src/authz/merge_json.rs deleted file mode 100644 index c60a2d032a3..00000000000 --- a/jans-cedarling/cedarling/src/authz/merge_json.rs +++ /dev/null @@ -1,56 +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 serde_json::Value; - -#[derive(Debug, thiserror::Error)] -pub enum MergeError { - #[error("Failed to merge JSON objects due to conflicting keys: {0}")] - KeyConflict(String), -} - -pub fn merge_json_values(mut base: Value, other: Value) -> Result { - if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) { - for (key, value) in additional_map { - if base_map.contains_key(key) { - return Err(MergeError::KeyConflict(key.clone())); - } - base_map.insert(key.clone(), value.clone()); - } - } - Ok(base) -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use super::merge_json_values; - use crate::authz::merge_json::MergeError; - - #[test] - fn can_merge_json_objects() { - let obj1 = json!({ "a": 1, "b": 2 }); - let obj2 = json!({ "c": 3, "d": 4 }); - let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4}); - - let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects"); - - assert_eq!(result, expected); - } - - #[test] - fn errors_on_same_keys() { - // Test for only two objects - let obj1 = json!({ "a": 1, "b": 2 }); - let obj2 = json!({ "b": 3, "c": 4 }); - let result = merge_json_values(obj1, obj2); - - assert!( - matches!(result, Err(MergeError::KeyConflict(key)) if key.as_str() == "b"), - "Expected an error due to conflicting keys" - ); - } -} diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index 198b6425785..8c7786ae54f 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -8,40 +8,36 @@ //! - evaluate if authorization is granted for *user* //! - evaluate if authorization is granted for *client* / *workload * -use std::collections::{HashMap, HashSet}; -use std::io::Cursor; -use std::str::FromStr; -use std::sync::Arc; - +use crate::authorization_config::IdTokenTrustMode; use crate::bootstrap_config::AuthorizationConfig; 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, }; +use build_ctx::*; +use cedar_policy::{Entities, Entity, EntityUid}; +use chrono::Utc; +use entity_builder::*; +use request::Request; +use std::collections::HashMap; +use std::io::Cursor; +use std::str::FromStr; +use std::sync::Arc; +use trust_mode::*; mod authorize_result; -mod merge_json; +mod build_ctx; +mod trust_mode; -pub(crate) mod entities; +pub(crate) mod entity_builder; pub(crate) mod request; -use std::time::Instant; pub use authorize_result::AuthorizeResult; -use cedar_policy::{ContextJsonError, Entities, Entity, EntityUid}; -use entities::{ - create_resource_entity, create_role_entities, create_token_entities, create_user_entity, - create_workload_entity, CreateCedarEntityError, CreateUserEntityError, - CreateWorkloadEntityError, DecodedTokens, ResourceEntityError, RoleEntityError, - CEDAR_POLICY_SEPARATOR, -}; -use merge_json::{merge_json_values, MergeError}; -use request::Request; -use serde_json::Value; /// Configuration to Authz to initialize service without errors pub(crate) struct AuthzConfig { @@ -59,11 +55,23 @@ pub(crate) struct AuthzConfig { pub struct Authz { config: AuthzConfig, authorizer: cedar_policy::Authorizer, + entity_builder: EntityBuilder, } impl Authz { /// Create a new Authorization Service pub(crate) fn new(config: AuthzConfig) -> Self { + let json_schema = config.policy_store.schema.json.clone(); + let entity_names = EntityNames::from(&config.authorization); + let build_workload = config.authorization.use_workload_principal; + let build_user = config.authorization.use_user_principal; + let entity_builder = entity_builder::EntityBuilder::new( + json_schema, + entity_names, + build_workload, + build_user, + ); + config.log_service.log( LogEntry::new_with_data( config.pdp_id, @@ -78,58 +86,75 @@ impl Authz { Self { config, authorizer: cedar_policy::Authorizer::new(), + entity_builder, } } // 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 = if let Some(tkn) = request.tokens.access_token.as_ref() { + Some( + self.config + .jwt_service + .process_token(TokenStr::Access(tkn)) + .await?, + ) + } else { + None + }; + let id = if let Some(tkn) = request.tokens.id_token.as_ref() { + Some( + self.config + .jwt_service + .process_token(TokenStr::Id(tkn)) + .await?, + ) + } else { + None + }; + let userinfo = if let Some(tkn) = request.tokens.userinfo_token.as_ref() { + Some( self.config .jwt_service .process_token(TokenStr::Userinfo(tkn)) - }) - .transpose()?; + .await?, + ) + } else { + None + }; Ok(DecodedTokens { - access_token, - id_token, - userinfo_token, + access, + id, + userinfo, }) } /// 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?; + + if let IdTokenTrustMode::Strict = self.config.authorization.id_token_trust_mode { + validate_id_tkn_trust_mode(&tokens)?; + } // 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 = self + .entity_builder + .build_entities(&tokens, &request.resource)?; // Get entity UIDs what we will be used on authorize check let resource_uid = entities_data.resource.uid(); @@ -239,7 +264,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,30 +278,16 @@ 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| { + access: tokens.access.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -282,7 +295,7 @@ impl Authz { .as_str(), ) }), - id_token: tokens.access_token.as_ref().map(|tkn| { + id_token: tokens.id.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -290,7 +303,7 @@ impl Authz { .as_str(), ) }), - userinfo: tokens.userinfo_token.as_ref().map(|tkn| { + userinfo: tokens.userinfo.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -301,6 +314,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 +325,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) } @@ -342,104 +382,18 @@ impl Authz { Ok(response) } - /// Build all the Cedar [`Entities`] from a [`Request`] - /// - /// [`Entities`]: Entity + #[cfg(test)] pub fn build_entities( &self, request: &Request, - tokens: &DecodedTokens, + tokens: &DecodedTokens<'_>, ) -> Result { - let policy_store = &self.config.policy_store; - let auth_conf = &self.config.authorization; - - // build workload entity - let workload = if self.config.authorization.use_workload_principal { - Some(create_workload_entity( - auth_conf.mapping_workload.as_deref(), - policy_store, - tokens, - )?) - } else { - None - }; - - // build role entity - let roles = create_role_entities(policy_store, tokens)?; - - // build user entity - let user = if self.config.authorization.use_user_principal { - Some(create_user_entity( - auth_conf.mapping_user.as_deref(), - policy_store, - tokens, - HashSet::from_iter(roles.iter().map(|e| e.uid())), - )?) - } else { - None - }; - - let token_entities = create_token_entities(auth_conf, policy_store, tokens)?; - - // build resource entity - let resource = create_resource_entity( - request.resource.clone(), - &self.config.policy_store.schema.json, - )?; - - Ok(AuthorizeEntitiesData { - workload, - access_token: token_entities.access, - id_token: token_entities.id, - userinfo_token: token_entities.userinfo, - user, - resource, - roles, - }) + Ok(self + .entity_builder + .build_entities(tokens, &request.resource)?) } } -/// Constructs the authorization context by adding the built entities from the tokens -fn build_context( - config: &AuthzConfig, - request_context: Value, - entities_data: &AuthorizeEntitiesData, - schema: &cedar_policy::Schema, - action: &cedar_policy::EntityUid, -) -> Result { - let namespace = config.policy_store.namespace(); - let action_name = action.id().escaped().to_string(); - let action_schema = config - .policy_store - .schema - .json - .find_action(&action_name, namespace) - .map_err(|e| BuildContextError::FindActionSchema(action_name.clone(), e))? - .ok_or(BuildContextError::MissingActionSchema(action_name))?; - - let mut id_mapping = HashMap::new(); - for entity in entities_data.iter() { - // we strip the namespace from the type_name then make it lowercase - // example: 'Jans::Id_token' -> 'id_token' - let type_name = entity.uid().type_name().to_string(); - let type_name = type_name - .strip_prefix(&format!("{}{}", namespace, CEDAR_POLICY_SEPARATOR)) - .unwrap_or(&type_name) - .to_lowercase(); - let type_id = entity.uid().id().escaped(); - id_mapping.insert(type_name, type_id.to_string()); - } - - let entities_context = action_schema.build_ctx_entity_refs_json(id_mapping)?; - - let context = merge_json_values(entities_context, request_context)?; - - let context: cedar_policy::Context = - cedar_policy::Context::from_json_value(context, Some((schema, action)))?; - - Ok(context) -} - /// Helper struct to hold named parameters for [`Authz::execute_authorize`] method. struct ExecuteAuthorizeParameters<'a> { entities: &'a Entities, @@ -462,6 +416,17 @@ pub struct AuthorizeEntitiesData { } impl AuthorizeEntitiesData { + // NOTE: the type ids created from these does not include the namespace + fn type_ids(&self) -> HashMap { + self.iter() + .map(|entity| { + let type_name = entity.uid().type_name().basename().to_string(); + let type_id = entity.uid().id().escaped().to_string(); + (type_name, type_id) + }) + .collect::>() + } + /// Create iterator to get all entities fn into_iter(self) -> impl Iterator { vec![self.resource].into_iter().chain(self.roles).chain( @@ -510,27 +475,6 @@ pub enum AuthorizeError { /// Error encountered while processing JWT token data #[error(transparent)] ProcessTokens(#[from] jwt::JwtProcessingError), - /// Error encountered while creating id token entity - #[error("could not create id_token entity: {0}")] - CreateIdTokenEntity(CreateCedarEntityError), - /// Error encountered while creating userinfo entity - #[error("could not create userinfo entity: {0}")] - CreateUserinfoTokenEntity(CreateCedarEntityError), - /// Error encountered while creating access_token entity - #[error("could not create access_token entity: {0}")] - CreateAccessTokenEntity(CreateCedarEntityError), - /// Error encountered while creating user entity - #[error("could not create User entity: {0}")] - CreateUserEntity(#[from] CreateUserEntityError), - /// Error encountered while creating workload - #[error(transparent)] - CreateWorkloadEntity(#[from] CreateWorkloadEntityError), - /// Error encountered while creating resource entity - #[error("{0}")] - ResourceEntity(#[from] ResourceEntityError), - /// Error encountered while creating role entity - #[error(transparent)] - RoleEntity(#[from] RoleEntityError), /// Error encountered while parsing Action to EntityUid #[error("could not parse action: {0}")] Action(cedar_policy::ParseErrors), @@ -552,25 +496,12 @@ pub enum AuthorizeError { /// Error encountered while building the context for the request #[error("Failed to build context: {0}")] BuildContext(#[from] BuildContextError), -} - -#[derive(Debug, thiserror::Error)] -pub enum BuildContextError { - /// Error encountered while validating context according to the schema - #[error(transparent)] - Merge(#[from] MergeError), - /// Error encountered while deserializing the Context from JSON - #[error(transparent)] - DeserializeFromJson(#[from] ContextJsonError), - /// Error encountered while deserializing the Context from JSON - #[error("Failed to find the action `{0}` in the schema: {0}")] - FindActionSchema(String, FindActionError), - /// Error encountered while deserializing the Context from JSON - #[error("The action `{0}` was not found in the schema")] - MissingActionSchema(String), - /// Error encountered while deserializing the Context from JSON + /// Error encountered while building the context for the request + #[error("error while running on strict id token trust mode: {0}")] + IdTokenTrustMode(#[from] IdTokenTrustModeError), + /// Error encountered while building Cedar Entities #[error(transparent)] - BuildJson(#[from] BuildJsonCtxError), + BuildEntity(#[from] BuildCedarlingEntityError), } #[derive(Debug, derive_more::Error, derive_more::Display)] diff --git a/jans-cedarling/cedarling/src/authz/request.rs b/jans-cedarling/cedarling/src/authz/request.rs index d618c803b98..daa225775f3 100644 --- a/jans-cedarling/cedarling/src/authz/request.rs +++ b/jans-cedarling/cedarling/src/authz/request.rs @@ -4,12 +4,9 @@ // Copyright (c) 2024, Gluu, Inc. use std::collections::HashMap; -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 +19,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 +34,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")] @@ -49,12 +46,3 @@ pub struct ResourceData { #[serde(flatten)] pub payload: HashMap, } - -impl ResourceData { - pub(crate) fn entity_uid(&self) -> Result { - Ok(EntityUid::from_type_name_and_id( - EntityTypeName::from_str(&self.resource_type)?, - EntityId::new(&self.id), - )) - } -} diff --git a/jans-cedarling/cedarling/src/authz/trust_mode.rs b/jans-cedarling/cedarling/src/authz/trust_mode.rs new file mode 100644 index 00000000000..7f679b78a36 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/trust_mode.rs @@ -0,0 +1,338 @@ +// 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 super::entity_builder::DecodedTokens; +use crate::{ + common::policy_store::TokenKind, + jwt::{Token, TokenClaimTypeError}, +}; + +/// Enforces the trust mode setting set by the `CEDARLING_ID_TOKEN_TRUST_MODE` +/// bootstrap property. +/// +/// # Trust Modes +/// +/// There are currently two trust modes: +/// - None +/// - Strict +/// +/// # Strict Mode +/// +/// Strict mode requires the following: +/// - `id_token.aud` == `access_token.client_id` +/// - if a Userinfo token is present: +/// - `userinfo_token.aud` == `access_token.client_id` +/// - `userinfo_token.sub` == `id_token.sub` +pub fn validate_id_tkn_trust_mode(tokens: &DecodedTokens) -> Result<(), IdTokenTrustModeError> { + let access_tkn = tokens + .access + .as_ref() + .ok_or(IdTokenTrustModeError::MissingAccessToken)?; + let id_tkn = tokens + .id + .as_ref() + .ok_or(IdTokenTrustModeError::MissingIdToken)?; + + let access_tkn_client_id = get_tkn_claim_as_str(access_tkn, "client_id")?; + let id_tkn_aud = get_tkn_claim_as_str(id_tkn, "aud")?; + + if access_tkn_client_id != id_tkn_aud { + return Err(IdTokenTrustModeError::AccessTokenClientIdMismatch); + } + + let userinfo_tkn = match tokens.userinfo.as_ref() { + Some(token) => token, + None => return Ok(()), + }; + let userinfo_tkn_aud = get_tkn_claim_as_str(userinfo_tkn, "aud")?; + + if userinfo_tkn_aud != id_tkn_aud { + return Err(IdTokenTrustModeError::SubMismatchIdTokenUserinfo); + } + if userinfo_tkn_aud != access_tkn_client_id { + return Err(IdTokenTrustModeError::ClientIdUserinfoAudMismatch); + } + + Ok(()) +} + +fn get_tkn_claim_as_str( + token: &Token, + claim_name: &str, +) -> Result, IdTokenTrustModeError> { + token + .get_claim(claim_name) + .ok_or_else(|| { + IdTokenTrustModeError::MissingRequiredClaim(claim_name.to_string(), token.kind) + }) + .and_then(|claim| { + claim + .as_str() + .map(|s| s.into()) + .map_err(|e| IdTokenTrustModeError::TokenClaimTypeError(token.kind, e)) + }) +} + +#[derive(Debug, thiserror::Error)] +pub enum IdTokenTrustModeError { + #[error("the access token's `client_id` does not match with the id token's `aud`")] + AccessTokenClientIdMismatch, + #[error("an access token is required when using strict mode")] + MissingAccessToken, + #[error("an id token is required when using strict mode")] + MissingIdToken, + #[error("the id token's `sub` does not match with the userinfo token's `sub`")] + SubMismatchIdTokenUserinfo, + #[error("the access token's `client_id` does not match with the userinfo token's `aud`")] + ClientIdUserinfoAudMismatch, + #[error("missing a required claim `{0}` from `{1}` token")] + MissingRequiredClaim(String, TokenKind), + #[error("invalid claim type in {0} token: {1}")] + TokenClaimTypeError(TokenKind, TokenClaimTypeError), +} + +#[cfg(test)] +mod test { + use super::{IdTokenTrustModeError, validate_id_tkn_trust_mode}; + use crate::authz::entity_builder::DecodedTokens; + use crate::common::policy_store::TokenKind; + use crate::jwt::{Token, TokenClaims}; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn success_without_userinfo_tkn() { + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + validate_id_tkn_trust_mode(&tokens).expect("should not error"); + } + + #[test] + fn errors_when_missing_access_tkn() { + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!(err, IdTokenTrustModeError::MissingAccessToken), + "expected error due to missing access token, got: {:?}", + err + ) + } + + #[test] + fn errors_when_access_tkn_missing_required_claim() { + let access_token = Token::new_access(TokenClaims::new(HashMap::new()), None); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!( + err, + IdTokenTrustModeError::MissingRequiredClaim(ref claim_name, tkn_kind) + if claim_name == "client_id" && + tkn_kind == TokenKind::Access + ), + "expected error due to access token missing a required claim, got: {:?}", + err + ) + } + + #[test] + fn errors_when_missing_id_tkn() { + let access_token = Token::new_id( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!(err, IdTokenTrustModeError::MissingIdToken), + "expected error due to missing id token, got: {:?}", + err + ) + } + + #[test] + fn errors_when_id_tkn_missing_required_claim() { + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), None); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!( + err, + IdTokenTrustModeError::MissingRequiredClaim(ref claim_name, tkn_kind) + if claim_name == "aud" && + tkn_kind == TokenKind::Id + ), + "expected error due to id token missing a required claim, got: {:?}", + err + ) + } + + #[test] + fn errors_when_access_tkn_client_id_id_tkn_aud_mismatch() { + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([( + "aud".to_string(), + json!("another-id-123"), + )])), + None, + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!(err, IdTokenTrustModeError::AccessTokenClientIdMismatch), + "expected error due to the access_token's `client_id` not matching with the id_token's `aud`, got: {:?}", + err + ) + } + + #[test] + fn success_with_userinfo_tkn() { + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + validate_id_tkn_trust_mode(&tokens).expect("should not error"); + } + + #[test] + fn errors_when_userinfo_tkn_missing_required_claim() { + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let userinfo_token = Token::new_userinfo(TokenClaims::new(HashMap::new()), None); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!( + err, + IdTokenTrustModeError::MissingRequiredClaim(ref claim_name, tkn_kind) + if claim_name == "aud" && + tkn_kind == TokenKind::Userinfo + ), + "expected error due to id token missing a required claim, got: {:?}", + err + ) + } + + #[test] + fn errors_when_access_tkn_client_id_userinfo_tkn_aud_mismatch() { + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("some-id-123"), + )])), + None, + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("aud".to_string(), json!("some-id-123"))])), + None, + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "aud".to_string(), + json!("another-id-123"), + )])), + None, + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + let err = validate_id_tkn_trust_mode(&tokens).expect_err("should error"); + assert!( + matches!(err, IdTokenTrustModeError::SubMismatchIdTokenUserinfo), + "expected error due to the id_token's `aud` not matching with the userinfo_token's `aud`, got: {:?}", + err + ) + } +} 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/authorization_config.rs b/jans-cedarling/cedarling/src/bootstrap_config/authorization_config.rs index eddd923c8cc..e71d7e679b3 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/authorization_config.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/authorization_config.rs @@ -4,6 +4,7 @@ // Copyright (c) 2024, Gluu, Inc. use super::WorkloadBoolOp; +use serde::Deserialize; /// Configuration to specify authorization workflow. /// - If we use user entity as principal. @@ -54,4 +55,47 @@ pub struct AuthorizationConfig { /// Name of Cedar userinfo schema entity pub mapping_userinfo_token: Option, + + /// Sets the validation level for ID tokens. + /// + /// The available levels are [`None`] and [`Strict`]. + /// + /// # Strict Mode + /// + /// In `Strict` mode, the following conditions must be met for a token + /// to be considered valid: + /// + /// - The `id_token`'s `aud` (audience) must match the `access_token`'s `client_id` + /// - If a Userinfo token is present: + /// - Its `sub` (subject) must match the `id_token`'s `sub`. + /// - Its `aud` (audience) must match the `access_token`'s `client_id`. + /// + /// [`None`]: IdTokenTrustMode::None + /// [`Strict`]: IdTokenTrustMode::Strict + pub id_token_trust_mode: IdTokenTrustMode, +} + +/// Defines the level of validation for ID tokens. +#[derive(Debug, Clone, PartialEq, Default, Deserialize, Copy)] +#[serde(rename_all = "lowercase")] +pub enum IdTokenTrustMode { + /// No validation is performed on the ID token. + None, + /// Strict validation of the ID token. + /// + /// In this mode, the following conditions must be met: + /// + /// - The `id_token`'s `aud` (audience) must match the `access_token`'s `client_id`. + /// - If a Userinfo token is present: + /// - Its `sub` (subject) must match the `id_token`'s `sub`. + /// - Its `aud` must match the `access_token`'s `client_id`. + #[default] + Strict, +} + +/// Error when parsing [`IdTokenTrustMode`] +#[derive(Default, Debug, derive_more::Display, derive_more::Error)] +#[display("Invalid `IdTokenTrustMode`: {trust_mode}. should be `strict` or `none`")] +pub struct IdTknTrustModeParseError { + trust_mode: String, } diff --git a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs index be2b1726e35..fe9800ebc08 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/decode.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/decode.rs @@ -3,22 +3,20 @@ // // Copyright (c) 2024, Gluu, Inc. +use super::authorization_config::{AuthorizationConfig, IdTokenTrustMode}; +use super::{ + BootstrapConfig, BootstrapConfigLoadingError, JwtConfig, LogConfig, LogTypeConfig, + MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, TokenValidationConfig, +}; +use crate::log::LogLevel; +use jsonwebtoken::Algorithm; +use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashSet; use std::fmt::Display; use std::fs; use std::path::Path; use std::str::FromStr; -use jsonwebtoken::Algorithm; -use serde::{Deserialize, Deserializer, Serialize}; - -use super::authorization_config::AuthorizationConfig; -use super::{ - BootstrapConfig, BootstrapConfigLoadingError, IdTokenTrustMode, JwtConfig, LogConfig, - LogTypeConfig, MemoryLogConfig, PolicyStoreConfig, PolicyStoreSource, TokenValidationConfig, -}; -use crate::log::LogLevel; - #[derive(Deserialize, PartialEq, Debug, Default)] /// Struct that represent mapping mapping `Bootstrap properties` to be JSON and YAML compatible /// from [link](https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#bootstrap-properties) @@ -493,8 +491,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()?; @@ -503,7 +502,6 @@ impl BootstrapConfig { jwks, jwt_sig_validation: raw.jwt_sig_validation.into(), jwt_status_validation: raw.jwt_status_validation.into(), - id_token_trust_mode: raw.id_token_trust_mode, signature_algorithms_supported: raw.jwt_signature_algorithms_supported.clone(), access_token_config: TokenValidationConfig { iss_validation: raw.at_iss_validation.into(), @@ -541,6 +539,7 @@ impl BootstrapConfig { mapping_id_token: raw.mapping_id_token.clone(), mapping_access_token: raw.mapping_access_token.clone(), mapping_userinfo_token: raw.mapping_userinfo_token.clone(), + id_token_trust_mode: raw.id_token_trust_mode, }; Ok(Self { diff --git a/jans-cedarling/cedarling/src/bootstrap_config/jwt_config.rs b/jans-cedarling/cedarling/src/bootstrap_config/jwt_config.rs index c9e1a72d27e..ff16cbe66b8 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/jwt_config.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/jwt_config.rs @@ -3,11 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. -use std::collections::HashSet; -use std::str::FromStr; - use jsonwebtoken::Algorithm; -use serde::Deserialize; +use std::collections::HashSet; /// The set of Bootstrap properties related to JWT validation. #[derive(Debug, PartialEq)] @@ -38,23 +35,6 @@ pub struct JwtConfig { /// /// [`IETF Draft`]: https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/ pub jwt_status_validation: bool, - /// Sets the validation level for ID tokens. - /// - /// The available levels are [`None`] and [`Strict`]. - /// - /// # Strict Mode - /// - /// In `Strict` mode, the following conditions must be met for a token - /// to be considered valid: - /// - /// - The `id_token`'s `aud` (audience) must match the `access_token`'s `client_id` - /// - If a Userinfo token is present: - /// - Its `sub` (subject) must match the `id_token`'s `sub`. - /// - Its `aud` (audience) must match the `access_token`'s `client_id`. - /// - /// [`None`]: IdTokenTrustMode::None - /// [`Strict`]: IdTokenTrustMode::Strict - pub id_token_trust_mode: IdTokenTrustMode, /// Only tokens signed with algorithms in this list can be valid. pub signature_algorithms_supported: HashSet, /// Validation options related to the Access token @@ -177,44 +157,6 @@ impl TokenValidationConfig { } } -/// Defines the level of validation for ID tokens. -#[derive(Debug, Clone, PartialEq, Default, Deserialize, Copy)] -#[serde(rename_all = "lowercase")] -pub enum IdTokenTrustMode { - /// No validation is performed on the ID token. - None, - /// Strict validation of the ID token. - /// - /// In this mode, the following conditions must be met: - /// - /// - The `id_token`'s `aud` (audience) must match the `access_token`'s `client_id`. - /// - If a Userinfo token is present: - /// - Its `sub` (subject) must match the `id_token`'s `sub`. - /// - Its `aud` must match the `access_token`'s `client_id`. - #[default] - Strict, -} - -impl FromStr for IdTokenTrustMode { - type Err = IdTknTrustModeParseError; - - fn from_str(s: &str) -> Result { - let s = s.to_lowercase(); - match s.as_str() { - "strict" => Ok(IdTokenTrustMode::Strict), - "none" => Ok(IdTokenTrustMode::None), - _ => Err(IdTknTrustModeParseError { trust_mode: s }), - } - } -} - -/// Error when parsing [`IdTokenTrustMode`] -#[derive(Default, Debug, derive_more::Display, derive_more::Error)] -#[display("Invalid `IdTokenTrustMode`: {trust_mode}. should be `strict` or `none`")] -pub struct IdTknTrustModeParseError { - trust_mode: String, -} - impl Default for JwtConfig { /// Cedarling will use the strictest validation options by default. fn default() -> Self { @@ -222,7 +164,6 @@ impl Default for JwtConfig { jwks: None, jwt_sig_validation: true, jwt_status_validation: true, - id_token_trust_mode: IdTokenTrustMode::Strict, signature_algorithms_supported: HashSet::new(), access_token_config: TokenValidationConfig::access_token(), id_token_config: TokenValidationConfig::id_token(), @@ -238,7 +179,6 @@ impl JwtConfig { jwks: None, jwt_sig_validation: false, jwt_status_validation: false, - id_token_trust_mode: IdTokenTrustMode::None, signature_algorithms_supported: HashSet::new(), access_token_config: TokenValidationConfig::default(), id_token_config: TokenValidationConfig::default(), diff --git a/jans-cedarling/cedarling/src/bootstrap_config/mod.rs b/jans-cedarling/cedarling/src/bootstrap_config/mod.rs index 59720f4a1a6..38501001893 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. @@ -182,7 +183,6 @@ mod test { jwks: None, jwt_sig_validation: true, jwt_status_validation: false, - id_token_trust_mode: IdTokenTrustMode::Strict, signature_algorithms_supported: HashSet::from([Algorithm::HS256, Algorithm::RS256]), access_token_config: TokenValidationConfig { exp_validation: true, @@ -237,7 +237,6 @@ mod test { jwks: None, jwt_sig_validation: true, jwt_status_validation: false, - id_token_trust_mode: IdTokenTrustMode::Strict, signature_algorithms_supported: HashSet::from([Algorithm::HS256, Algorithm::RS256]), access_token_config: TokenValidationConfig { exp_validation: true, 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..c1387ed61fb 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs @@ -3,407 +3,137 @@ // // Copyright (c) 2024, Gluu, Inc. -//! Module contains the JSON representation of a [cedar_policy::Schema] -//! Support translated schema from human representation to JSON via CLI version `cedar-policy-cli 4.1`. -//! To translate human redable format to JSON via CLI use next command: -//! `cedar translate-schema --direction cedar-to-json -s .\cedar.schema` -//! [cedar json schema grammar](https://docs.cedarpolicy.com/schema/json-schema-grammar.html) - documentation about json structure of cedar schema. - -mod action; -mod entity_types; - -use std::collections::HashMap; - -use action::ActionSchema; -pub use action::{BuildJsonCtxError, FindActionError}; -use derive_more::derive::Display; -pub use entity_types::{CedarSchemaEntityShape, CedarSchemaRecord}; - -/// Represent `cedar-policy` schema type for external usage. -#[derive(Debug, PartialEq, Hash, Eq, Display)] -pub enum CedarType { - Long, - String, - Boolean, - TypeName(String), - Set(Box), -} - -/// Possible errors that may occur when retrieving a [`CedarType`] from cedar-policy schema. -#[derive(Debug, thiserror::Error)] -pub enum GetCedarTypeError { - /// Error while getting `cedar-policy` schema not implemented type - #[error("could not get cedar-policy type {0}, it is not implemented")] - TypeNotImplemented(String), -} - -/// Enum to get info about type based on name. -/// Is used as a result in [`CedarSchemaJson::find_type`] -pub enum SchemaDefinedType<'a> { - Entity(&'a CedarSchemaEntityShape), - CommonType(&'a CedarSchemaRecord), -} - -/// JSON representation of a [`cedar_policy::Schema`] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +//! This module is responsible for deserializing the JSON Cedar schema + +use action::*; +use attribute::*; +use entity_type::*; +use serde::Deserialize; +use std::{collections::HashMap, str::FromStr}; + +pub(crate) mod action; +pub(crate) mod attribute; +pub(crate) mod entity_type; + +mod deserialize; + +pub type ActionName = String; +pub type ActionGroupName = String; +pub type AttributeName = String; +pub type CommonTypeName = String; +pub type EntityName = String; +pub type EntityTypeName = String; +pub type EntityOrCommonName = String; +pub type ExtensionName = String; +pub type NamespaceName = String; + +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct CedarSchemaJson { #[serde(flatten)] - pub namespace: HashMap, + namespaces: HashMap, } impl CedarSchemaJson { - /// Get schema record by namespace name and entity type name - pub fn entity_schema( - &self, - namespace: &str, - typename: &str, - ) -> Option<&CedarSchemaEntityShape> { - let namespace = self.namespace.get(namespace)?; - namespace.entity_types.get(typename) + pub fn get_action(&self, namespace: &str, name: &str) -> Option<&Action> { + self.namespaces + .get(namespace) + .and_then(|nmspce| nmspce.actions.get(name)) } - /// Find the typename if exist in the schema and return it definition - pub fn find_type(&self, type_name: &str, namespace: &str) -> Option { - let namespace = self.namespace.get(namespace)?; - - let schema_type = namespace - .common_types - .get(type_name) - .as_ref() - .map(|common_type| SchemaDefinedType::CommonType(common_type)); - - if schema_type.is_some() { - return schema_type; + pub fn get_common_type(&self, name: &str) -> Option<(&NamespaceName, &Attribute)> { + for (namespace_name, namespace) in self.namespaces.iter() { + if let Some(attr) = namespace.common_types.get(name) { + return Some((namespace_name, attr)); + } } + None + } - let schema_type = namespace - .entity_types - .get(type_name) - .as_ref() - .map(|entity| SchemaDefinedType::Entity(entity)); - if schema_type.is_some() { - return schema_type; + pub fn get_entity_from_base_name( + &self, + base_name: &str, + ) -> Option<(&NamespaceName, &EntityType)> { + for (namespace_name, namespace) in self.namespaces.iter() { + if let Some(entity_type) = namespace.entity_types.get(base_name) { + return Some((namespace_name, entity_type)); + } } + None + } + pub fn get_entity_from_full_name( + &self, + full_name: &str, + ) -> Option<(NamespaceName, &EntityType)> { + let full_name = cedar_policy::EntityTypeName::from_str(full_name).ok()?; + let namespace_name = full_name.namespace(); + if let Some(namespace) = self.namespaces.get(&namespace_name) { + let base_name = full_name.basename(); + if let Some(entity_type) = namespace.entity_types.get(base_name) { + return Some((namespace_name, entity_type)); + } + } None } } -/// CedarSchemaEntities hold all entities and their shapes in the namespace. -// It may contain more fields, but we don't need all of them. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaEntities { +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct Namespace { #[serde(rename = "entityTypes", default)] - pub entity_types: HashMap, + entity_types: HashMap, #[serde(rename = "commonTypes", default)] - pub common_types: HashMap, - pub actions: HashMap, + common_types: HashMap, + #[serde(default)] + actions: HashMap, } #[cfg(test)] -mod tests { - use std::collections::HashSet; - - use action::CtxAttribute; - use serde_json::json; - use test_utils::{assert_eq, SortedJson}; - - use super::entity_types::*; +mod test_deserialize_json_cedar_schema { use super::*; + use serde_json::json; + use std::collections::HashSet; - /// Test to parse the cedar json schema - /// to debug deserialize the schema #[test] - fn parse_correct_example() { - let json_value = include_str!("test_files/test_data_cedar.json"); - - let parsed_cedar_schema: CedarSchemaJson = - 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, - }, - ), - ]), - }), + fn can_deserialize_entity_types() { + let schema = json!({ + "Jans": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "department": { "type": "String" }, + "jobLevel": { "type": "Long" } + } + } + }, + "UserGroup": {}, }, - ), - ("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 { - 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, - }, - )]), + } + }); + let schema = serde_json::from_value::(schema).unwrap(); + let namespace = Namespace { + entity_types: HashMap::from([ + ("User".into(), EntityType { + member_of: Some(HashSet::from(["UserGroup".into()])), + shape: Some(EntityShape::required(HashMap::from([ + ("department".into(), Attribute::string()), + ("jobLevel".into(), Attribute::long()), + ]))), + tags: None, + }), + ("UserGroup".into(), EntityType { + member_of: None, + shape: None, + tags: None, + }), + ]), + common_types: HashMap::new(), + actions: HashMap::new(), }; - - assert_eq!( - serde_json::json!(parsed_cedar_schema).sorted(), - serde_json::json!(schema_to_compare).sorted() - ); - } - - /// test to check if we get error on parsing invalid `EntityOrCommon` type - #[test] - fn parse_error_entity_or_common() { - // In this file we skipped field `name` for `EntityOrCommon` - let json_value = include_str!("test_files/test_data_cedar_err_entity_or_common.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: failed to deserialize EntityOrCommon: \ - missing field `name` at line 17 column 1" - ) - } - - /// test to check if we get error on parsing invalid `PrimitiveType` type - #[test] - fn parse_error_primitive_type() { - // In this file we use `"type": 123` but in OK case should be `"type": "Long"` - let json_value = include_str!("test_files/test_data_cedar_err_primitive_type.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: invalid type: integer `123`, expected a \ - string at line 17 column 1" - ) - } - - /// test to check if we get error on parsing invalid nested Sets :`Set>` type - #[test] - fn parse_error_set_entity_or_common() { - // In this file we skipped field `name` for `EntityOrCommon` in the nested set - let json_value = include_str!("test_files/test_data_cedar_err_set.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: failed to deserialize Set: failed to \ - deserialize Set: failed to deserialize EntityOrCommon: missing field `name` at line \ - 24 column 1" - ) - } - - /// test to check if we get error on parsing invalid type in field `is_required` - #[test] - fn parse_error_field_is_required() { - // In this file we use ` "required": 1234` but in OK case should be ` "required": false` or omit - let json_value = include_str!("test_files/test_data_cedar_err_field_is_required.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityAttribute, field 'is_required': invalid type: \ - integer `1234`, expected a boolean at line 22 column 1" - ) - } - - #[test] - fn can_parse_action_with_ctx() { - let expected_principal_entities = - HashSet::from(["Jans::Workload".into(), "Jans::User".into()]); - let expected_resource_entities = HashSet::from(["Jans::Issue".into()]); - let expected_context_entities = Some(HashSet::from([ - CtxAttribute { - namespace: "Jans".into(), - key: "access_token".into(), - kind: CedarType::TypeName("Access_token".to_string()), - }, - CtxAttribute { - namespace: "Jans".into(), - key: "time".into(), - kind: CedarType::Long, - }, - CtxAttribute { - namespace: "Jans".into(), - key: "user".into(), - kind: CedarType::TypeName("User".to_string()), - }, - CtxAttribute { - namespace: "Jans".into(), - key: "workload".into(), - kind: CedarType::TypeName("Workload".to_string()), - }, - ])); - - // Test case where the context is a record: - // action "Update" appliesTo { - // principal: [Workload, User], - // resource: [Issue], - // context: { - // time: Long, - // user: User, - // workload: Workload, - // access_token: Access_token, - // }}; - let json_value = include_str!("./test_files/test_schema.json"); - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("Should parse JSON schema"); - let action = parsed_cedar_schema - .find_action("UpdateWithRecordCtx", "Jans") - .expect("Should not error while finding action") - .expect("Action should not be none"); - assert_eq!(action.principal_entities, expected_principal_entities); - assert_eq!(action.resource_entities, expected_resource_entities); - assert_eq!(action.context_entities, expected_context_entities); - - // Test case where the context is a type: - // action "Update" appliesTo { - // principal: [Workload, User], - // resource: [Issue], - // context: Context - // }; - let json_value = include_str!("./test_files/test_schema.json"); - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("Should parse JSON schema"); - let action = parsed_cedar_schema - .find_action("UpdateWithTypeCtx", "Jans") - .expect("Should not error while finding action") - .expect("Action should not be none"); - assert_eq!(action.principal_entities, expected_principal_entities); - assert_eq!(action.resource_entities, expected_resource_entities); - assert_eq!(action.context_entities, expected_context_entities); - - let id_mapping = HashMap::from([ - ("access_token".into(), "tkn-1".into()), - ("user".into(), "user-123".into()), - ("workload".into(), "workload-321".into()), - ]); - let ctx_json = action - .build_ctx_entity_refs_json(id_mapping) - .expect("Should build JSON context"); - assert_eq!( - ctx_json, - json!({ - "access_token": { "type": "Jans::Access_token", "id": "tkn-1" }, - "user": { "type": "Jans::User", "id": "user-123" }, - "workload": { "type": "Jans::Workload", "id": "workload-321" }, - }) - ) + assert_eq!(schema, CedarSchemaJson { + namespaces: HashMap::from([("Jans".into(), namespace)]) + }); } } 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..72859e180ec 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 @@ -3,460 +3,177 @@ // // Copyright (c) 2024, Gluu, Inc. -use std::collections::{HashMap, HashSet}; - -use serde::ser::SerializeMap; -use serde::{de, Deserialize, Serialize}; -use serde_json::{json, Value}; - -use super::entity_types::{ - CedarSchemaEntityAttribute, CedarSchemaEntityType, PrimitiveType, PrimitiveTypeKind, -}; -use super::{ - CedarSchemaEntities, CedarSchemaJson, CedarSchemaRecord, CedarType, GetCedarTypeError, -}; -use crate::authz::entities::CEDAR_POLICY_SEPARATOR; -use crate::common::cedar_schema::cedar_json::SchemaDefinedType; - -type AttrName = String; - -#[derive(Debug, Eq, Hash, PartialEq)] -pub struct CtxAttribute { - pub namespace: String, - pub key: String, - pub kind: CedarType, -} - -pub struct Action<'a> { - pub principal_entities: HashSet, - pub resource_entities: HashSet, - pub context_entities: Option>, - pub schema_entities: &'a CedarSchemaEntities, - pub schema: &'a ActionSchema, +use super::attribute::Attribute; +use super::*; +use serde::Deserialize; +use std::collections::HashSet; + +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct Action { + #[serde(rename = "memberOf", default)] + member_of: Option>, + #[serde(rename = "appliesTo")] + pub applies_to: AppliesTo, } -impl Action<'_> { - /// Builds the JSON representation of context entities for a given action. +#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] +pub struct ActionGroup { + id: EntityName, + /// Specifies membership for an action group in a different namespace. /// - /// This method processes the context attributes of the action and generates a - /// corresponding JSON value. The context may include entity references (with - /// `type` and `id`) and other values, which can be mapped through the provided - /// `id_mapping` and `value_mapping`. - /// - /// The `id_mapping` param is a A `HashMap` that maps context attribute keys - /// (like `"access_token"`) to their corresponding `id`s (like `"acs-tkn-1"`). - /// - /// # Usage Example - /// - /// ```rs - /// let id_mapping = HashMap::from([("access_token".to_string(), "acs-tkn-1".to_string())]); - /// let json = action.build_ctx_entities_json(id_mapping, value_mapping); - /// ``` - pub fn build_ctx_entity_refs_json( - &self, - id_mapping: HashMap, - ) -> Result { - let mut json = json!({}); - - 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, - None => Err(BuildJsonCtxError::MissingIdMapping(attr.key.clone()))?, - }; - let type_name = - [attr.namespace.as_str(), type_name].join(CEDAR_POLICY_SEPARATOR); - json[attr.key.as_str()] = json!({"type": type_name, "id": id}); - } - } - } - - Ok(json) - } + /// e.g.: `kind: "My::Namespace::Action"` + #[serde(rename = "type", default)] + kind: Option, } -#[derive(Debug, thiserror::Error)] -pub enum BuildJsonCtxError { - /// If an entity reference is provided but the ID is missing from `id_mapping`. - /// - /// This is usually caused by: - /// - disabling workload AuthZ but having a Workload entity in the context schema - /// - disabling user AuthZ but referencing User entity in the context schema - #[error( - "An entity reference for `{0}` is required by the schema but an ID was not provided via \ - the `id_mapping`" - )] - MissingIdMapping(String), - /// If a non-entity attribute is provided but the value is missing from `value_mapping`. - #[error( - "A non-entity attribute for `{0}` is required by the schema but a value was not provided \ - via the `value_mapping`" - )] - MissingValueMapping(String), -} - -impl CedarSchemaJson { - /// Find the action in the schema - pub fn find_action( - &self, - action_name: &str, - namespace: &str, - ) -> Result, FindActionError> { - let schema_entities = match self.namespace.get(namespace) { - Some(entities) => entities, - None => return Ok(None), - }; - - let action_schema = match schema_entities.actions.get(action_name) { - Some(schema) => schema, - None => return Ok(None), - }; - - let principal_entities = HashSet::from_iter( - action_schema - .principal_types - .iter() - .map(|principal_type| [namespace, principal_type].join(CEDAR_POLICY_SEPARATOR)), - ); - let resource_entities = HashSet::from_iter( - action_schema - .resource_types - .iter() - .map(|resource_type| [namespace, resource_type].join(CEDAR_POLICY_SEPARATOR)), - ); - let context_entities = action_schema - .context - .as_ref() - .map(|ctx| self.process_action_context(ctx, namespace)) - .transpose()?; - - Ok(Some(Action { - principal_entities, - resource_entities, - context_entities, - schema_entities, - schema: action_schema, - })) - } - - fn process_action_context( - &self, - ctx: &RecordOrType, - namespace: &str, - ) -> Result, FindActionError> { - let mut entities = HashSet::::new(); - - match ctx { - // Case: the context is defined as a record in the schema - // for example: - // Jans { - // action View appliesTo { - // principal: [User], - // resource: [File], - // context: { - // "status": String, - // "id_token": Id_token, - // }, - // }; - // } - RecordOrType::Record(record) => { - for (key, attr) in record.attributes.iter() { - entities.insert(CtxAttribute { - namespace: namespace.to_string(), - key: key.to_string(), - kind: attr.get_type()?, - }); - } - }, - // Case: the context is defined as a type in the schema - // for example: - // Jans { - // type Context = { - // "status": String, - // "id_token": Id_token, - // }; - // action View appliesTo { - // principal: [User], - // resource: [File], - // context: Context, - // }; - // } - RecordOrType::Type(entity_type) => match entity_type { - CedarSchemaEntityType::Primitive(primitive_type) => { - if let PrimitiveTypeKind::TypeName(type_name) = &primitive_type.kind { - let cedar_type = self.find_type(type_name, namespace).unwrap(); - match cedar_type { - SchemaDefinedType::CommonType(common) => { - for (key, attr) in common.attributes.iter() { - entities.insert(CtxAttribute { - namespace: namespace.to_string(), - key: key.to_string(), - kind: attr.get_type()?, - }); - } - }, - SchemaDefinedType::Entity(_) => { - Err(FindActionError::EntityContext(entity_type.clone()))? - }, - } - } - }, - CedarSchemaEntityType::Set(_) => { - Err(FindActionError::SetContext(entity_type.clone()))? - }, - CedarSchemaEntityType::Typed(_) => { - Err(FindActionError::TypedContext(entity_type.clone()))? - }, - }, - } - - Ok(entities) - } -} - -/// Represents an action in the Cedar JSON schema -#[derive(Default, Debug, PartialEq, Clone)] -pub struct ActionSchema { - pub resource_types: HashSet, - pub principal_types: HashSet, - pub context: Option, -} - -impl Serialize for ActionSchema { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_map(Some(1))?; - state.serialize_entry( - "appliesTo", - &json!({ - "resourceTypes": self.resource_types, - "principalTypes": self.principal_types, - "context": self.context, - }), - )?; - state.end() - } -} - -impl<'de> Deserialize<'de> for ActionSchema { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let mut action = HashMap::>::deserialize(deserializer)?; - let mut action = action - .remove("appliesTo") - .ok_or(de::Error::missing_field("appliesTo"))?; - - let resource_types = action - .remove("resourceTypes") - .map(|val| serde_json::from_value::>(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("resourceTypes"))?; - - let principal_types = action - .remove("principalTypes") - .map(|val| serde_json::from_value::>(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("principalTypes"))?; - - let context = action - .remove("context") - .map(|val| serde_json::from_value::(val).map_err(de::Error::custom)) - .transpose()?; - - Ok(Self { - resource_types, - principal_types, - context, - }) - } -} - -#[derive(Debug, PartialEq, Clone, Serialize)] -pub enum RecordOrType { - Record(CedarSchemaRecord), - Type(CedarSchemaEntityType), -} - -impl<'de> Deserialize<'de> for RecordOrType { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let mut context = HashMap::::deserialize(deserializer)?; - let context_type = context - .remove("type") - .map(|val| serde_json::from_value::(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("type"))?; - - match context_type.as_str() { - "Record" => { - let attributes = context - .remove("attributes") - .map(|val| { - serde_json::from_value::>(val) - .map_err(de::Error::custom) - }) - .transpose()? - .ok_or(de::Error::missing_field("attributes"))?; - Ok(RecordOrType::Record(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes, - })) - }, - type_name => Ok(RecordOrType::Type(CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::TypeName(type_name.to_string()), - }, - ))), - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum FindActionError { - #[error("Error while collecting entities from action schema: {0}")] - CollectEntities(#[from] GetCedarTypeError), - #[error("Using `Set` as the context type is unsupported: {0:#?}")] - SetContext(CedarSchemaEntityType), - #[error("Using `Entity` as the context type is unsupported: {0:#?}")] - EntityContext(CedarSchemaEntityType), - #[error("Using `Typed` as the context type is unsupported: {0:#?}")] - TypedContext(CedarSchemaEntityType), +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct AppliesTo { + #[serde(rename = "principalTypes", default)] + pub principal_types: HashSet, + #[serde(rename = "resourceTypes", default)] + pub resource_types: HashSet, + #[serde(default)] + pub context: Option, } #[cfg(test)] -mod test { +mod test_deserialize_action { + use super::super::attribute::Attribute; + use super::{Action, ActionGroup, AppliesTo}; + use serde_json::json; use std::collections::{HashMap, HashSet}; + use test_utils::assert_eq; - use serde::Deserialize; - use serde_json::{json, Value}; - - use super::ActionSchema; - 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)] - struct MockJsonSchema { - actions: HashMap, - } - - fn build_schema(ctx: Option) -> Value { - let mut schema = json!({ - "actions": { - "Update": { - "appliesTo": { - "resourceTypes": ["Issue"], - "principalTypes": ["Workload", "User"] - } - } + #[test] + fn can_deserialize() { + // Case: both principal types and resource types is empty + let action = json!({ + "appliesTo": { + "principalTypes": [], + "resourceTypes": [], } }); - if let Some(ctx) = ctx { - schema["actions"]["Update"]["appliesTo"]["context"] = ctx; - } - schema - } - - 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, - }, - )]), - } - } - - #[test] - pub fn can_deserialize_empty_ctx() { - let schema = build_schema(None); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::new(), + resource_types: HashSet::new(), + context: None, + }, + }); - let expected = build_expected(None); + // Case: resource types is empty + let action = json!({ + "appliesTo": { + "principalTypes": ["PrincipalEntityType1"], + "resourceTypes": [], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::from(["PrincipalEntityType1".into()]), + resource_types: HashSet::new(), + context: None, + }, + }); - assert_eq!(result, expected) + // Case: only principal types is empty + let action = json!({ + "appliesTo": { + "principalTypes": [], + "resourceTypes": ["ResourceEntityType1"], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::new(), + resource_types: HashSet::from(["ResourceEntityType1".into()]), + context: None, + }, + }); } #[test] - pub fn can_deserialize_record_ctx() { - let schema = build_schema(Some(json!({ - "type": "Record", - "attributes": { - "token": { - "type": "EntityOrCommon", - "name": "Access_token" - }, - "username": { - "type": "EntityOrCommon", - "name": "String" - } + fn can_deserialize_with_member_of() { + // Case: action group type is not provided + let action = json!({ + "memberOf": [{"id": "read"}], + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo"], } - }))); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); - - 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, - }, - ), - ]), - }))); + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: Some(HashSet::from([ActionGroup { + id: "read".into(), + kind: None + }])), + applies_to: AppliesTo { + principal_types: HashSet::from(["User".into()]), + resource_types: HashSet::from(["Photo".into()]), + context: None, + }, + }); - assert_eq!(result, expected) + // Case: an action group type is provided + let action = json!({ + "memberOf": [{ + "id": "read", + "type": "My::Namespace::Action", + }], + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo"], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: Some(HashSet::from([ActionGroup { + id: "read".into(), + kind: Some("My::Namespace::Action".into()), + }])), + applies_to: AppliesTo { + principal_types: HashSet::from(["User".into()]), + resource_types: HashSet::from(["Photo".into()]), + context: None, + }, + }); } #[test] - pub fn can_deserialize_entity_or_common_ctx() { - let schema = build_schema(Some(json!({ - "type": "Context", - }))); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); - - let expected = build_expected(Some(RecordOrType::Type(CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::TypeName("Context".to_string()), + fn can_deserialize_with_context() { + let action = json!({ + "appliesTo": { + "principalTypes": ["PrincipalEntityType1"], + "resourceTypes": ["ResourceEntityType1"], + "context": { + "type": "Record", + "attributes": { + "field1": { "type": "Boolean" }, + "field2": { "type": "Long" }, + "field3": { "type": "String", "required": false }, + } + }, }, - )))); - - assert_eq!(result, expected) + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::from(["PrincipalEntityType1".into()]), + resource_types: HashSet::from(["ResourceEntityType1".into()]), + context: Some(Attribute::record(HashMap::from([ + ("field1".into(), Attribute::boolean()), + ("field2".into(), Attribute::long()), + ("field3".into(), Attribute::String { required: false }) + ]))), + }, + }); } } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs new file mode 100644 index 00000000000..91f6492f622 --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs @@ -0,0 +1,283 @@ +// 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 super::deserialize::*; +use super::*; +use serde::{Deserialize, de}; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Clone)] +pub enum Attribute { + String { + required: bool, + }, + Long { + required: bool, + }, + Boolean { + required: bool, + }, + Record { + required: bool, + attrs: HashMap, + }, + Set { + required: bool, + element: Box, + }, + Entity { + required: bool, + name: EntityName, + }, + Extension { + required: bool, + name: ExtensionName, + }, + EntityOrCommon { + required: bool, + name: EntityOrCommonName, + }, +} + +impl<'de> Deserialize<'de> for Attribute { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut attr = HashMap::::deserialize(deserializer)?; + let kind = attr + .remove("type") + .ok_or(de::Error::missing_field("type"))?; + let required = attr + .remove("required") + .map(serde_json::from_value::) + .transpose() + .map_err(|e| { + de::Error::custom(format!("error while deserializing JSON Value to bool: {e}")) + })? + .unwrap_or(true); + let kind = String::deserialize(&kind).map_err(de::Error::custom)?; + let attr = match kind.as_str() { + "String" => Attribute::String { required }, + "Long" => Attribute::Long { required }, + "Boolean" => Attribute::Boolean { required }, + "Record" => { + let attrs = attr + .remove("attributes") + .ok_or(de::Error::missing_field("attributes"))?; + let attrs = deserialize_record_attrs::(attrs)?; + Self::Record { required, attrs } + }, + "Set" => { + let element = attr + .remove("element") + .ok_or(de::Error::missing_field("element"))?; + let element = serde_json::from_value::(element).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar element attribute: {e}" + )) + })?; + + Self::Set { + required, + element: Box::new(element), + } + }, + "Entity" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::Entity { required, name } + }, + "Extension" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::Extension { required, name } + }, + "EntityOrCommon" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::EntityOrCommon { required, name } + }, + name => Self::EntityOrCommon { + required, + name: name.to_string(), + }, + }; + + Ok(attr) + } +} + +impl Attribute { + pub fn is_required(&self) -> bool { + *match self { + Attribute::String { required } => required, + Attribute::Long { required } => required, + Attribute::Boolean { required } => required, + Attribute::Record { required, .. } => required, + Attribute::Set { required, .. } => required, + Attribute::Entity { required, .. } => required, + Attribute::Extension { required, .. } => required, + Attribute::EntityOrCommon { required, .. } => required, + } + } +} + +#[cfg(test)] +/// Helper methods to easily create required attributes +impl Attribute { + pub fn string() -> Self { + Self::String { required: true } + } + + pub fn long() -> Self { + Self::Long { required: true } + } + + pub fn boolean() -> Self { + Self::Boolean { required: true } + } + + pub fn record(attrs: HashMap) -> Self { + Self::Record { + required: true, + attrs, + } + } + + pub fn set(element: Self) -> Self { + Self::Set { + required: true, + + element: Box::new(element), + } + } + + pub fn entity(name: &str) -> Self { + Self::Entity { + required: true, + name: name.into(), + } + } + + pub fn extension(name: &str) -> Self { + Self::Extension { + required: true, + name: name.into(), + } + } + + pub fn entity_or_common(name: &str) -> Self { + Self::EntityOrCommon { + required: true, + name: name.into(), + } + } +} + +#[cfg(test)] +mod test { + use super::Attribute; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_deserialize_string() { + let attr_json = json!({"type": "String"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::string()); + } + + #[test] + fn can_deserialize_long() { + let attr_json = json!({"type": "Long"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::long()); + } + + #[test] + fn can_deserialize_boolean() { + let attr_json = json!({"type": "Boolean"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::boolean()); + } + + #[test] + fn can_deserialize_record() { + let attr_json = json!({ + "type": "Record", + "attributes": { + "primary": { "type": "String" }, + "secondary": { "type": "String" }, + }, + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + let expected = HashMap::from([ + ("primary".into(), Attribute::string()), + ("secondary".into(), Attribute::string()), + ]); + assert_eq!(deserialized, Attribute::record(expected)); + } + + #[test] + fn can_deserialize_set() { + let attr_json = json!({ + "type": "Set", + "element": { + "type": "EntityOrCommon", + "name": "Subscription" + } + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!( + deserialized, + Attribute::set(Attribute::entity_or_common("Subscription")) + ); + } + + #[test] + fn can_deserialize_entity() { + let attr_json = json!({ + "type": "Entity", + "name": "Role", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::entity("Role")); + } + + #[test] + fn can_deserialize_extension() { + let attr_json = json!({ + "type": "Extension", + "name": "decimal", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::extension("decimal"),); + } + + #[test] + fn can_deserialize_entity_or_common() { + let attr_json = json!({ + "type": "EntityOrCommon", + "name": "String", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::entity_or_common("String"),); + } + + #[test] + fn can_deserialize_non_required_attr() { + let attr_json = json!({"type": "String", "required": false}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::String { required: false }); + } +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs new file mode 100644 index 00000000000..55cf8cfd77d --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs @@ -0,0 +1,36 @@ +// 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 super::*; +use serde::de; +use serde_json::Value; +use std::collections::HashMap; + +/// Deserialize a [`Value`] to a to the attrs of a [`AttributeKind::Record`] +pub fn deserialize_record_attrs<'de, D>( + attrs: Value, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let attrs_json = serde_json::from_value::>(attrs).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar record attribute: {e}" + )) + })?; + + // loop through each attr then deserialize into Self + let mut attrs = HashMap::::new(); + for (key, val) in attrs_json.into_iter() { + let val = serde_json::from_value::(val).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar record attribute: {e}" + )) + })?; + attrs.insert(key, val); + } + + Ok(attrs) +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs new file mode 100644 index 00000000000..217d1c32791 --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs @@ -0,0 +1,171 @@ +// 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 super::attribute::Attribute; +use super::deserialize::*; +use super::*; +use serde::{Deserialize, de}; +use serde_json::Value; +use std::collections::HashSet; + +#[derive(Debug, PartialEq, Clone)] +pub struct EntityShape { + pub required: bool, + pub attrs: HashMap, +} + +#[cfg(test)] +impl EntityShape { + pub fn required(attrs: HashMap) -> Self { + Self { + required: true, + attrs, + } + } +} + +#[derive(Debug, PartialEq, Deserialize, Clone)] +pub struct EntityType { + #[serde(rename = "memberOfTypes")] + pub member_of: Option>, + #[serde(deserialize_with = "deserialize_entity_shape", default)] + pub shape: Option, + #[serde(default)] + pub tags: Option, +} + +// Forces the `shape` field into the [`AttributeKind::Shape`] variant. +fn deserialize_entity_shape<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let mut attr = HashMap::::deserialize(deserializer)?; + let kind = attr + .remove("type") + .ok_or(de::Error::missing_field("type"))?; + let required = attr + .remove("required") + .map(serde_json::from_value::) + .transpose() + .map_err(|e| { + de::Error::custom(format!("error while deserializing JSON Value to bool: {e}")) + })? + .unwrap_or(true); + let kind = String::deserialize(&kind).map_err(de::Error::custom)?; + let attr = match kind.as_str() { + "Record" => { + let attrs = attr + .remove("attributes") + .ok_or(de::Error::missing_field("attributes"))?; + let attrs = deserialize_record_attrs::(attrs)?; + EntityShape { required, attrs } + }, + variant => { + return Err(de::Error::custom(format!( + "invalid type: {}, expected {}", + variant, "Record" + ))); + }, + }; + + Ok(Some(attr)) +} + +#[cfg(test)] +mod test_deserialize_entity_type { + use super::super::attribute::Attribute; + use super::*; + use serde_json::json; + use std::collections::{HashMap, HashSet}; + use test_utils::assert_eq; + + #[test] + fn can_deserialize() { + let entity_type = json!({ + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + }); + let entity_type = serde_json::from_value::(entity_type).unwrap(); + assert_eq!(entity_type, EntityType { + member_of: None, + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: None, + }); + } + + #[test] + fn can_deserialize_with_member_of() { + let with_member_of = json!({ + "memberOfTypes": ["UserGroup"], + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + }); + let with_member_of = serde_json::from_value::(with_member_of).unwrap(); + assert_eq!(with_member_of, EntityType { + member_of: Some(HashSet::from(["UserGroup".into()])), + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: None, + }); + } + + #[test] + fn can_deserialize_with_tags() { + let with_tags = json!({ + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + "tags": { + "type": "Set", + "element": { + "type": "EntityOrCommon", + "name": "String" + } + } + }); + let with_tags = serde_json::from_value::(with_tags).unwrap(); + assert_eq!(with_tags, EntityType { + member_of: None, + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: Some(Attribute::set(Attribute::entity_or_common("String",))) + }); + } + + #[test] + fn errors_on_invalid_shape() { + let entity_type = json!({ + "shape": { + "type": "Set", + }, + }); + let err = serde_json::from_value::(entity_type).unwrap_err(); + assert!( + err.to_string() + .contains("invalid type: Set, expected Record") + ); + } +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs deleted file mode 100644 index 9d855ed6449..00000000000 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs +++ /dev/null @@ -1,226 +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::collections::HashMap; - -use super::{CedarType, GetCedarTypeError}; - -/// CedarSchemaEntityShape hold shape of an entity. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaEntityShape { - pub shape: Option, -} - -/// CedarSchemaRecord defines type name and attributes for an entity. -/// Record ::= '"type": "Record", "attributes": {' [ RecordAttr { ',' RecordAttr } ] '}' -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaRecord { - #[serde(rename = "type")] - pub entity_type: String, - // represent RecordAttr - // RecordAttr ::= STR ': {' Type [',' '"required"' ':' ( true | false )] '}' - // attributes as key is used attribute name - pub attributes: HashMap, -} - -impl CedarSchemaRecord { - // if we want to create entity from attributes it should be record - pub fn is_record(&self) -> bool { - self.entity_type == "Record" - } -} - -/// CedarSchemaRecordAttr defines possible type variants of the entity attribute. -/// RecordAttr ::= STR ': {' Type [',' '"required"' ':' ( true | false )] '}' -#[derive(Debug, Clone, PartialEq, serde::Serialize, Hash)] -pub struct CedarSchemaEntityAttribute { - pub cedar_type: CedarSchemaEntityType, - pub required: bool, -} - -impl CedarSchemaEntityAttribute { - pub fn is_required(&self) -> bool { - self.required - } - - pub fn get_type(&self) -> Result { - self.cedar_type.get_type() - } -} - -impl<'de> serde::Deserialize<'de> for CedarSchemaEntityAttribute { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; - - // used only for deserialization - #[derive(serde::Deserialize)] - pub struct IsRequired { - required: Option, - } - - let is_required = IsRequired::deserialize(&value).map_err(|err| { - serde::de::Error::custom(format!( - "could not deserialize CedarSchemaEntityAttribute, field 'is_required': {}", - err - )) - })?; - - let cedar_type = CedarSchemaEntityType::deserialize(value).map_err(|err| { - serde::de::Error::custom(format!( - "could not deserialize CedarSchemaEntityType: {}", - err - )) - })?; - - Ok(CedarSchemaEntityAttribute { - cedar_type, - required: is_required.required.unwrap_or(true), - }) - } -} - -#[derive(Debug, Clone, PartialEq, serde::Serialize, Hash)] -pub enum CedarSchemaEntityType { - Set(Box), - Typed(EntityType), - Primitive(PrimitiveType), -} - -impl CedarSchemaEntityType { - pub fn get_type(&self) -> Result { - match self { - Self::Set(v) => Ok(CedarType::Set(Box::new(v.element.get_type()?))), - Self::Typed(v) => v.get_type(), - Self::Primitive(primitive) => Ok(primitive.kind.get_type()), - } - } -} - -impl<'de> serde::Deserialize<'de> for CedarSchemaEntityType { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - // is used only on deserialization. - #[derive(serde::Deserialize)] - struct TypeStruct { - #[serde(rename = "type")] - type_name: String, - } - - let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; - - let entity_type = match TypeStruct::deserialize(&value) - .map_err(serde::de::Error::custom)? - .type_name - .as_str() - { - "Set" => { - CedarSchemaEntityType::Set(Box::new(SetEntityType::deserialize(&value).map_err( - |err| serde::de::Error::custom(format!("failed to deserialize Set: {}", err)), - )?)) - }, - "EntityOrCommon" => { - CedarSchemaEntityType::Typed(EntityType::deserialize(&value).map_err(|err| { - serde::de::Error::custom(format!( - "failed to deserialize EntityOrCommon: {}", - err - )) - })?) - }, - _ => CedarSchemaEntityType::Primitive(PrimitiveType::deserialize(&value).map_err( - |err| { - // will newer happen because we know that field "type" is string - serde::de::Error::custom(format!( - "failed to deserialize PrimitiveType: {}", - err - )) - }, - )?), - }; - - Ok(entity_type) - } -} - -/// The Primitive element describes -/// Primitive ::= '"type":' ('"Long"' | '"String"' | '"Boolean"' | TYPENAME) -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Hash)] -pub struct PrimitiveType { - #[serde(rename = "type")] - pub kind: PrimitiveTypeKind, -} - -/// Variants of primitive type. -/// Primitive ::= '"type":' ('"Long"' | '"String"' | '"Boolean"' | TYPENAME) -#[derive(Debug, Clone, serde::Serialize, PartialEq, Hash)] -pub enum PrimitiveTypeKind { - Long, - String, - Boolean, - TypeName(String), -} - -impl PrimitiveTypeKind { - pub fn get_type(&self) -> CedarType { - match self { - PrimitiveTypeKind::Long => CedarType::Long, - PrimitiveTypeKind::String => CedarType::String, - PrimitiveTypeKind::Boolean => CedarType::Boolean, - PrimitiveTypeKind::TypeName(name) => CedarType::TypeName(name.to_string()), - } - } -} - -/// impement custom deserialization to deserialize it correctly -impl<'de> serde::Deserialize<'de> for PrimitiveTypeKind { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s: String = serde::Deserialize::deserialize(deserializer)?; - match s.as_str() { - "Long" => Ok(PrimitiveTypeKind::Long), - "String" => Ok(PrimitiveTypeKind::String), - "Boolean" => Ok(PrimitiveTypeKind::Boolean), - _ => Ok(PrimitiveTypeKind::TypeName(s)), - } - } -} - -/// This structure can hold `Extension`, `EntityOrCommon`, `EntityRef` -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Hash)] -pub struct EntityType { - // it also can be primitive type - #[serde(rename = "type")] - pub kind: String, - pub name: String, -} - -impl EntityType { - pub fn get_type(&self) -> Result { - if self.kind == "EntityOrCommon" { - match self.name.as_str() { - "Long" => Ok(CedarType::Long), - "String" => Ok(CedarType::String), - "Boolean" => Ok(CedarType::Boolean), - type_name => Ok(CedarType::TypeName(type_name.to_string())), - } - } else { - Err(GetCedarTypeError::TypeNotImplemented(self.kind.to_string())) - } - } -} - -/// Describes the Set element -/// Set ::= '"type": "Set", "element": ' TypeJson -// "type": "Set" checked during deserialization -#[derive(Debug, Clone, serde::Deserialize, PartialEq, serde::Serialize, Hash)] -pub struct SetEntityType { - pub element: CedarSchemaEntityType, -} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs b/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs index dda9cef224f..6d0489ce491 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs @@ -3,8 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. -pub(crate) use cedar_json::CedarSchemaJson; pub(crate) mod cedar_json; +pub(crate) const CEDAR_NAMESPACE_SEPARATOR: &str = "::"; /// cedar_schema value which specifies both encoding and content_type /// diff --git a/jans-cedarling/cedarling/src/common/policy_store.rs b/jans-cedarling/cedarling/src/common/policy_store.rs index 48ca81d04d3..cba32133696 100644 --- a/jans-cedarling/cedarling/src/common/policy_store.rs +++ b/jans-cedarling/cedarling/src/common/policy_store.rs @@ -202,7 +202,7 @@ pub struct TokensMetadata<'a> { pub tx_tokens: &'a TokenEntityMetadata, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub enum TokenKind { /// Access token used for granting access to resources. Access, @@ -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..6137dc069ce 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` @@ -17,9 +17,29 @@ use serde_json::Value; pub struct ClaimMappings(HashMap); impl ClaimMappings { - pub fn get_mapping(&self, field: &str, cedar_policy_type: &str) -> Option<&ClaimMapping> { + pub fn get(&self, claim: &str) -> Option<&ClaimMapping> { + self.0.get(claim) + } + + // returns (claim_name, &ClaimMapping) + pub fn get_mapping_for_type(&self, type_name: &str) -> Option<(&String, &ClaimMapping)> { + // PERF: we can probably avoiding iterating through all of this by changing the + // `claim_mapping` in the Token Entity Metadata Schema self.0 - .get(field) + .iter() + .find_map(|(claim_name, mapping)| match mapping { + ClaimMapping::Regex(regex_mapping) => { + (regex_mapping.cedar_policy_type == type_name).then_some((claim_name, mapping)) + }, + ClaimMapping::Json { r#type } => { + (r#type == type_name).then_some((claim_name, mapping)) + }, + }) + } + + pub fn get_mapping(&self, claim: &str, cedar_policy_type: &str) -> Option<&ClaimMapping> { + self.0 + .get(claim) .filter(|claim_mapping| match claim_mapping { ClaimMapping::Regex(regexp_mapping) => { regexp_mapping.cedar_policy_type == cedar_policy_type @@ -281,20 +301,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 +375,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..6438248f1a1 100644 --- a/jans-cedarling/cedarling/src/init/service_factory.rs +++ b/jans-cedarling/cedarling/src/init/service_factory.rs @@ -1,12 +1,12 @@ -// 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; @@ -14,6 +14,7 @@ use crate::common::app_types; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{JwtService, JwtServiceInitError}; use crate::log; +use std::sync::Arc; #[derive(Clone)] pub(crate) struct ServiceFactory<'a> { @@ -71,20 +72,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 +94,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..de6ca520eeb 100644 --- a/jans-cedarling/cedarling/src/jwt/mod.rs +++ b/jans-cedarling/cedarling/src/jwt/mod.rs @@ -24,11 +24,11 @@ use std::sync::Arc; pub use jsonwebtoken::Algorithm; use key_service::{KeyService, KeyServiceError}; -pub use token::{Token, TokenClaim, TokenClaimTypeError, TokenClaims, TokenStr}; +pub use token::{Token, TokenClaimTypeError, TokenClaims, TokenStr}; use validator::{JwtValidator, JwtValidatorConfig, JwtValidatorError}; use crate::common::policy_store::TrustedIssuer; -use crate::{IdTokenTrustMode, JwtConfig}; +use crate::JwtConfig; /// Type alias for Trusted Issuers' ID. type TrustedIssuerId = Arc; @@ -92,14 +92,10 @@ pub struct JwtService { access_tkn_validator: JwtValidator, id_tkn_validator: JwtValidator, userinfo_tkn_validator: JwtValidator, - // TODO: implement the usage of this bootstrap property in - // the authz module. - #[allow(dead_code)] - id_token_trust_mode: IdTokenTrustMode, } impl JwtService { - pub fn new( + pub async fn new( config: &JwtConfig, trusted_issuers: Option>, ) -> Result { @@ -110,6 +106,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 @@ -173,11 +170,10 @@ impl JwtService { access_tkn_validator, id_tkn_validator, userinfo_tkn_validator, - id_token_trust_mode: config.id_token_trust_mode, }) } - pub fn process_token<'a>( + pub async fn process_token<'a>( &'a self, token: TokenStr<'a>, ) -> Result, JwtProcessingError> { @@ -217,13 +213,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}; + use crate::{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!({ @@ -262,7 +259,6 @@ mod test { jwks: Some(local_jwks), jwt_sig_validation: true, jwt_status_validation: false, - id_token_trust_mode: IdTokenTrustMode::Strict, signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]), access_token_config: TokenValidationConfig::access_token(), id_token_config: TokenValidationConfig::id_token(), @@ -270,11 +266,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 +281,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 +290,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/token.rs b/jans-cedarling/cedarling/src/jwt/token.rs index 58a28f31b57..47e5ab3b7a0 100644 --- a/jans-cedarling/cedarling/src/jwt/token.rs +++ b/jans-cedarling/cedarling/src/jwt/token.rs @@ -81,13 +81,13 @@ impl<'a> Token<'a> { self.claims.logging_info(claim) } - pub fn claims(&self) -> &TokenClaims { - &self.claims + pub fn claims_value(&self) -> &HashMap { + &self.claims.claims } } /// A struct holding information on a decoded JWT. -#[derive(Debug, PartialEq, Default, Deserialize)] +#[derive(Debug, PartialEq, Default, Deserialize, Clone)] pub struct TokenClaims { #[serde(flatten)] claims: HashMap, @@ -100,14 +100,11 @@ impl From> for TokenClaims { } impl TokenClaims { + #[cfg(test)] pub fn new(claims: HashMap) -> Self { Self { claims } } - pub fn from_json_map(map: serde_json::Map) -> Self { - Self::new(HashMap::from_iter(map)) - } - pub fn get_claim(&self, name: &str) -> Option { self.claims.get(name).map(|value| TokenClaim { key: name.to_string(), @@ -132,22 +129,10 @@ pub struct TokenClaim<'a> { } impl TokenClaim<'_> { - pub fn key(&self) -> &str { - &self.key - } - pub fn value(&self) -> &serde_json::Value { self.value } - pub fn as_i64(&self) -> Result { - self.value - .as_i64() - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "i64", self.value, - )) - } - pub fn as_str(&self) -> Result<&str, TokenClaimTypeError> { self.value .as_str() @@ -155,39 +140,10 @@ impl TokenClaim<'_> { &self.key, "String", self.value, )) } - - pub fn as_bool(&self) -> Result { - self.value - .as_bool() - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "bool", self.value, - )) - } - - pub fn as_array(&self) -> Result, TokenClaimTypeError> { - self.value - .as_array() - .map(|array| { - array - .iter() - .enumerate() - .map(|(i, v)| { - TokenClaim { - // show current key and index in array - key: format!("{}[{}]", self.key, i), - value: v, - } - }) - .collect() - }) - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "Array", self.value, - )) - } } -#[derive(Debug, thiserror::Error)] -#[error("type mismatch for key '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] +#[derive(Debug, thiserror::Error, PartialEq)] +#[error("type mismatch for token claim '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] pub struct TokenClaimTypeError { pub key: String, pub expected_type: String, @@ -208,7 +164,7 @@ impl TokenClaimTypeError { } /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. - fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { + pub fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { let got_value_type_name = Self::json_value_type_name(got_value); Self { 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..389b4ab177f 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -21,28 +21,30 @@ 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}; -pub use crate::authz::entities::CreateCedarEntityError; - #[doc(hidden)] pub mod bindings { pub use cedar_policy; @@ -62,6 +64,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 +83,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,24 +109,24 @@ 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 build_entities( &self, request: &Request, ) -> Result { - let tokens = self.authz.decode_tokens(request)?; + let tokens = self.authz.decode_tokens(request).await?; self.authz.build_entities(request, &tokens) } } @@ -124,11 +134,11 @@ impl Cedarling { // 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..be322c50285 100644 --- a/jans-cedarling/cedarling/src/log/interface.rs +++ b/jans-cedarling/cedarling/src/log/interface.rs @@ -25,6 +25,7 @@ pub(crate) trait LogWriter { pub(crate) trait Loggable: serde::Serialize { /// get unique request ID fn get_request_id(&self) -> Uuid; + /// get log level for entity /// not all log entities have log level, only when `log_kind` == `System` fn get_log_level(&self) -> Option; @@ -34,13 +35,8 @@ pub(crate) trait Loggable: serde::Serialize { // is used to avoid boilerplate code fn can_log(&self, logger_level: LogLevel) -> bool { if let Some(entry_log_level) = self.get_log_level() { - if entry_log_level < logger_level { - // entry log level lower than logger level - false - } else { - // entry log higher or equal than logger level - true - } + // higher level is more important, ie closer to fatal + logger_level <= entry_log_level } else { // if `.get_log_level` return None // it means that `log_kind` != `System` and we should log it @@ -53,10 +49,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..46add8d97fc 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. +pub 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..2fc944b40c9 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -3,29 +3,31 @@ // // 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"; -const STORAGE_JSON_PARSE_EXPECT_MESSAGE: &str = - "In MemoryLogger storage value should be valid LogEntry json string"; /// A logger that store logs in-memory. pub(crate) struct MemoryLogger { - storage: Mutex, + storage: Mutex>, log_level: LogLevel, } 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() }; @@ -36,6 +38,44 @@ impl MemoryLogger { } } +/// In case of failure in MemoryLogger, log to stderr where supported. +/// On WASM, stderr is not supported, so log to whatever the wasm logger uses. +mod fallback { + use crate::LogLevel; + + /// conform to Loggable requirement imposed by LogStrategy + #[derive(serde::Serialize)] + struct StrWrap<'a>(&'a str); + + impl crate::log::interface::Loggable for StrWrap<'_> { + fn get_request_id(&self) -> uuid7::Uuid { + crate::log::log_entry::gen_uuid7() + } + + fn get_log_level(&self) -> Option { + // These must always be logged. + Some(LogLevel::TRACE) + } + } + + /// Fetch the correct logger. That takes some work, and it's done on every + /// call. But this is a fallback logger, so it is not intended to be used + /// often, and in this case correctness and non-fallibility are far more + /// important than performance. + pub fn log(msg: &str) { + let log_config = crate::bootstrap_config::LogConfig { + log_type: crate::bootstrap_config::log_config::LogTypeConfig::StdOut, + // level is so that all messages passed here are logged. + log_level: LogLevel::TRACE, + }; + // This should always be a LogStrategy::StdOut(StdOutLogger) + let log_strategy = crate::log::LogStrategy::new(&log_config); + use crate::log::interface::LogWriter; + // a string is always serializable + log_strategy.log_any(StrWrap(msg)) + } +} + // Implementation of LogWriter impl LogWriter for MemoryLogger { fn log_any(&self, entry: T) { @@ -44,43 +84,45 @@ impl LogWriter for MemoryLogger { return; } - let json_string = serde_json::json!(entry).to_string(); + let json = match serde_json::to_value(&entry) { + Ok(json) => json, + Err(err) => { + fallback::log(&format!( + "could not serialize LogEntry to serde_json::Value: {err:?}" + )); + return; + }, + }; - let result = self + let set_result = self .storage .lock() .expect(STORAGE_MUTEX_EXPECT_MESSAGE) - .set(entry.get_request_id().to_string().as_str(), &json_string); + .set(&entry.get_request_id().to_string(), json); - if let Err(err) = result { - // log error to stderr - eprintln!("could not store LogEntry to memory: {err:?}"); + if let Err(err) = set_result { + fallback::log(&format!("could not store LogEntry to memory: {err:?}")); }; } } // Implementation of LogStorage impl LogStorage for MemoryLogger { - fn pop_logs(&self) -> Vec { - // TODO: implement more efficient implementation - - let mut storage_guard = self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE); - - let keys = storage_guard.get_keys(); - - 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()) - .expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) + fn pop_logs(&self) -> Vec { + self.storage + .lock() + .expect(STORAGE_MUTEX_EXPECT_MESSAGE) + .drain() + .map(|(_k, value)| value) .collect() } - fn get_log_by_id(&self, id: &str) -> Option { - self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE) + 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)) + .cloned() } fn get_log_ids(&self) -> Vec { @@ -130,32 +172,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 +234,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!( @@ -196,4 +249,41 @@ mod tests { "Logs were not fully popped" ); } + + #[test] + fn fallback_logger() { + struct FailSerialize; + + impl serde::Serialize for FailSerialize { + fn serialize(&self, _serializer: S) -> Result + where + S: serde::Serializer, + { + Err(serde::ser::Error::custom("this always fails")) + } + } + + impl crate::log::interface::Loggable for FailSerialize { + fn get_request_id(&self) -> uuid7::Uuid { + crate::log::log_entry::gen_uuid7() + } + + fn get_log_level(&self) -> Option { + // These must always be logged. + Some(LogLevel::TRACE) + } + } + + let logger = create_memory_logger(); + logger.log_any(FailSerialize); + + // There isn't a good way, in unit tests, to verify the output was + // actually written to stderr/json console. + // + // To eyeball-verify it: + // cargo test -- --nocapture fall + // and look in the output for + // "could not serialize LogEntry to serde_json::Value: Error(\"this always fails\", line: 0, column: 0)" + assert!(logger.pop_logs().is_empty(), "logger should be empty"); + } } 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..c4b19069df3 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, authorization_config::IdTokenTrustMode, 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,27 +94,30 @@ 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 { use_user_principal: false, use_workload_principal: true, user_workload_operator: Default::default(), + id_token_trust_mode: IdTokenTrustMode::None, ..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,27 +133,30 @@ 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 { use_user_principal: true, use_workload_principal: false, user_workload_operator: Default::default(), + id_token_trust_mode: IdTokenTrustMode::None, ..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,28 +175,31 @@ 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 { use_user_principal: true, use_workload_principal: false, user_workload_operator: Default::default(), + id_token_trust_mode: IdTokenTrustMode::None, ..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,27 +219,30 @@ 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 { use_user_principal: true, use_workload_principal: true, user_workload_operator: WorkloadBoolOp::And, + id_token_trust_mode: IdTokenTrustMode::None, ..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,28 +267,31 @@ 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 { use_user_principal: true, use_workload_principal: true, user_workload_operator: WorkloadBoolOp::Or, + id_token_trust_mode: IdTokenTrustMode::None, ..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,28 +316,31 @@ 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 { use_user_principal: true, use_workload_principal: true, user_workload_operator: WorkloadBoolOp::Or, + id_token_trust_mode: IdTokenTrustMode::None, ..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,28 +365,31 @@ 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 { use_user_principal: true, use_workload_principal: true, user_workload_operator: WorkloadBoolOp::Or, + id_token_trust_mode: IdTokenTrustMode::None, ..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,27 +414,30 @@ 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 { use_user_principal: true, use_workload_principal: true, user_workload_operator: Default::default(), + id_token_trust_mode: IdTokenTrustMode::None, ..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,22 +448,25 @@ 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 { use_user_principal: true, use_workload_principal: false, user_workload_operator: Default::default(), + id_token_trust_mode: IdTokenTrustMode::None, ..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..05dde523b33 100644 --- a/jans-cedarling/cedarling/src/tests/mapping_entities.rs +++ b/jans-cedarling/cedarling/src/tests/mapping_entities.rs @@ -10,15 +10,17 @@ //! CEDARLING_MAPPING_ACCESS_TOKEN //! CEDARLING_MAPPING_USERINFO_TOKEN +use super::utils::*; +use crate::authz::entity_builder::{ + BuildCedarlingEntityError, BuildEntityError, BuildTokenEntityError, +}; +use crate::common::policy_store::TokenKind; +use crate::{AuthorizeError, Cedarling, cmp_decision, cmp_policy}; +use cedarling_util::get_raw_config; use std::collections::HashSet; use std::sync::LazyLock; - -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 tokio::test; static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_entity_mapping.yaml"); @@ -66,16 +68,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,18 +106,16 @@ 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. /// This function validates the mapping of users and workloads using the defined `cedar` schema. /// For other entities, currently, it is not possible to automatically validate the mapping. /// -/// TODO: Add validation for `IdToken`, `Access_token`, and `Userinfo_token` once they are added to the context. -/// /// 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 +126,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 +162,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,23 +176,26 @@ 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 { - AuthorizeError::CreateUserEntity(error) => { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::User(error)) => { assert_eq!(error.errors.len(), 2, "there should be 2 errors"); let (token_kind, err) = &error.errors[0]; assert_eq!(token_kind, &TokenKind::Userinfo); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), - "expected CouldNotFindEntity({}), got: {:?}", + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), + "expected EntityNotInSchema({}), got: {:?}", &entity_type, err, ); @@ -194,43 +203,47 @@ fn test_failed_user_mapping() { let (token_kind, err) = &error.errors[1]; assert_eq!(token_kind, &TokenKind::Id); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), - "expected CouldNotFindEntity({}), got: {:?}", + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), + "expected EntityNotInSchema({}), got: {:?}", &entity_type, err, ); }, - _ => panic!("expected error CreateWorkloadEntity"), + _ => panic!("expected error BuildCedarlingEntityError::User"), } } /// 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 { - AuthorizeError::CreateWorkloadEntity(error) => { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::Workload(error)) => { assert_eq!(error.errors.len(), 2, "there should be 2 errors"); // check for access token error let (token_kind, err) = &error.errors[0]; assert_eq!(token_kind, &TokenKind::Access); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), "expected CouldNotFindEntity(\"{}\"), got: {:?}", &entity_type, err, @@ -240,19 +253,22 @@ fn test_failed_workload_mapping() { let (token_kind, err) = &error.errors[1]; assert_eq!(token_kind, &TokenKind::Id); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), "expected CouldNotFindEntity(\"{}\"), got: {:?}", &entity_type, err, ); }, - _ => panic!("expected error CreateWorkloadEntity"), + _ => panic!( + "expected BuildEntity(BuildCedarlingEntityError::Workload(_))) error, got: {:?}", + err + ), } } /// 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,26 +276,38 @@ 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!( - matches!( - err, - AuthorizeError::CreateIdTokenEntity(CreateCedarEntityError::CouldNotFindEntity(_)) + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::IdToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Id); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedIdTokenNotExist"), + "expected EntityNotInSchema(\"MappedIdTokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!( + "expected BuildEntity(BuildCedarlingEntityError::IdToken(_)) error, got: {:?}", + err ), - "should be error CouldNotFindEntity, got: {err:?}" - ); + } } /// 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,26 +315,35 @@ 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!( - matches!( - err, - AuthorizeError::CreateAccessTokenEntity(CreateCedarEntityError::CouldNotFindEntity(_)) - ), - "should be error CouldNotFindEntity" - ); + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::AccessToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Access); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedAccess_tokenNotExist"), + "expected EntityNotInSchema(\"MappedAccess_tokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!("expected BuildEntity error, got: {:?}", err), + } } /// 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,36 +351,45 @@ 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!( - matches!( - err, - AuthorizeError::CreateUserinfoTokenEntity(CreateCedarEntityError::CouldNotFindEntity( - _ - )) - ), - "should be error CouldNotFindEntity" - ); + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::UserinfoToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Userinfo); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedUserinfo_tokenNotExist"), + "expected EntityNotInSchema(\"MappedUserinfo_tokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!("expected BuildEntity error, got: {:?}", err), + } } /// Check if we get roles mapping from all tokens. /// 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!( @@ -391,7 +437,8 @@ fn test_role_many_tokens_mapping() { // iterate over roles that created and filter expected roles let roles_left = cedarling - .authorize_entities_data(&request) + .build_entities(&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..f1bdb7e9be7 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` @@ -109,7 +110,8 @@ fn check_mapping_tokens_data() { .expect("Request should be deserialized from json"); let entities = cedarling - .authorize_entities_data(&request) + .build_entities(&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..aaf87e8edd5 100644 --- a/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs +++ b/jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs @@ -3,7 +3,9 @@ // // Copyright (c) 2024, Gluu, Inc. -use crate::{AuthorizationConfig, JwtConfig, WorkloadBoolOp}; +use crate::{ + authorization_config::IdTokenTrustMode, AuthorizationConfig, JwtConfig, WorkloadBoolOp, +}; pub use crate::{ BootstrapConfig, BootstrapConfigRaw, Cedarling, FeatureToggle, LogConfig, LogTypeConfig, PolicyStoreConfig, PolicyStoreSource, @@ -23,7 +25,7 @@ pub fn get_raw_config(local_policy_store: &str) -> BootstrapConfigRaw { log_type: crate::LoggerType::StdOut, local_policy_store: Some(local_policy_store_json.to_string()), jwt_status_validation: FeatureToggle::Disabled, - id_token_trust_mode: crate::IdTokenTrustMode::None, + id_token_trust_mode: IdTokenTrustMode::None, ..Default::default() } } @@ -44,19 +46,21 @@ pub fn get_config(policy_source: PolicyStoreSource) -> BootstrapConfig { use_user_principal: true, use_workload_principal: true, user_workload_operator: WorkloadBoolOp::And, + id_token_trust_mode: IdTokenTrustMode::None, ..Default::default() }, } } /// 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 +76,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 1396235729c..eef4c3b8d60 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=9610bc15908331e8344dfaed16ee8a397bd999d5 +ENV JANS_SOURCE_VERSION=040ff17942019bc10433ce17d819b8d8474f13c8 COPY docker-entrypoint.sh / RUN chmod +x /docker-entrypoint.sh @@ -51,11 +51,11 @@ RUN git clone --filter blob:none --no-checkout https://github.com/JanssenProject && git sparse-checkout set jans-cedarling \ && cd jans-cedarling/bindings/cedarling_python \ && cp -r ../../flask-sidecar/* /api \ - && version=$(sed -n 's/.*version = "\(.*\)"/\1/p' pyproject.toml) \ + && version=$(sed -n 's/.*version = "\([0-9].[0-9].[0-9]\)"/\1/p' Cargo.toml) \ && echo "${version}" > /api/cedarling_version \ && release_version="${version}" \ && if [ "$version" = "0.0.0" ]; then release_version="nightly"; fi \ - && wget -q https://github.com/JanssenProject/jans/releases/download/"${release_version}"/cedarling_python-"${version}"-cp310-cp310-manylinux_2_34_x86_64.whl -O /api/cedarling_python-"${version}"-cp310-cp310-manylinux_2_34_x86_64.whl \ + && wget -q https://github.com/JanssenProject/jans/releases/download/"${release_version}"/cedarling_python-"${version}"-cp310-cp310-manylinux_2_31_x86_64.whl -O /api/cedarling_python-"${version}"-cp310-cp310-manylinux_2_31_x86_64.whl \ && rm -rf /tmp/jans # Setting up proper permissions: @@ -63,20 +63,21 @@ RUN chmod -R g=u /api \ && chown -R 1000:1000 /api # Project initialization: -RUN poetry add /api/cedarling_python-$(cat /api/cedarling_version)-cp310-cp310-manylinux_2_34_x86_64.whl \ +RUN poetry add /api/cedarling_python-$(cat /api/cedarling_version)-cp310-cp310-manylinux_2_31_x86_64.whl \ && poetry install --no-dev --no-root --no-interaction --no-ansi \ # Cleaning poetry installation's cache for production: && rm -rf "$POETRY_CACHE_DIR" ENV FLASK_APP=main.core:app \ GUNICORN_LOG_LEVEL=${GUNICORN_LOG_LEVEL:-debug} \ - CEDARLING_BOOTSTRAP_CONFIG_FILE=${CEDARLING_BOOTSTRAP_CONFIG_FILE:-/api/bootstrap.json} + CEDARLING_BOOTSTRAP_CONFIG_FILE=${CEDARLING_BOOTSTRAP_CONFIG_FILE:-/api/bootstrap.json} \ + SIDECAR_DEBUG_RESPOSE=${SIDECAR_DEBUG_RESPONSE:-False} EXPOSE 5000 LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/cedarling-flask-sidecar" \ org.opencontainers.image.authors="Janssen Project " \ org.opencontainers.image.vendor="Janssen Project" \ - org.opencontainers.image.version="0.0.0-nightly" \ + org.opencontainers.image.version="1.3.0-1" \ org.opencontainers.image.title="AuthZen Flask API" \ org.opencontainers.image.description="Flask API that implements the [AuthZen](https://openid.github.io/authzen/) specification with the [cedarling](../) python binding." diff --git a/jans-cedarling/flask-sidecar/README.md b/jans-cedarling/flask-sidecar/README.md index f373e5d3377..617fdf318d1 100644 --- a/jans-cedarling/flask-sidecar/README.md +++ b/jans-cedarling/flask-sidecar/README.md @@ -7,27 +7,83 @@ This is a Flask API that implements the [AuthZen](https://openid.github.io/authz To run the API: - Install [poetry](https://python-poetry.org/docs/#installation) -- Clone the [Janssen](https://github.com/JanssenProject/jans) repository +- Clone the [Janssen](https://github.com/JanssenProject/jans) repository: + ``` + git clone --filter blob:none --no-checkout https://github.com/JanssenProject/jans + ``` + ``` + cd jans + ``` + ``` + git sparse-checkout init --cone ``` - git clone --filter blob:none --no-checkout https://github.com/JanssenProject/jans /tmp/jans \ - && cd /tmp/jans \ - && git sparse-checkout init --cone \ - && git checkout main \ - && git sparse-checkout set jans-cedarling ``` -- Navigate to `jans/jans-cedarling/flask-sidecar/main` + git checkout main + ``` + + ``` + git sparse-checkout set jans-cedarling + ``` +- Navigate to `jans-cedarling/flask-sidecar` - Run `poetry install` to install dependencies +- Navigate to `main/` - Run `poetry run flask run` to run the API on `http://127.0.0.1:5000` +## Configuration + +For running via poetry, the sidecar supports the following environment variables: + +| Variable name | Default value | Supported value(s) | +| ------------- | ------------- | ------------------ | +| APP_MODE | testing | development, testing, production | +| CEDARLING_BOOTSTRAP_CONFIG_FILE | None | Path to your configuration | +| SIDECAR_DEBUG_RESPONSE | False | True, False | + +- Navigate to `jans/jans-cedarling/flask-sidecar/main` and create a file named `.env` +- Set environment variables like so: +``` +APP_MODE=development +CEDARLING_BOOTSTRAP_CONFIG_FILE=/path/to/bootstrap.json +SIDECAR_DEBUG_RESPONSE=False +``` + ## Tests Not yet implemented -## Docker Instructions +# Docker Instructions + +- Create a file called `bootstrap.json`. You may use this [sample](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/flask-sidecar/secrets/bootstrap.json) file. +- Modify the file to your specifications. Configuration values are described [here](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi). +- Pull the docker image: + ``` + docker pull ghcr.io/janssenproject/jans/cedarling-flask-sidecar:1.3.0-1 + ``` +- Run the docker image, replacing `` with the absolute path to your bootstrap file: + + ```bash + docker run -d \ + -e APP_MODE='development' \ + -e CEDARLING_BOOTSTRAP_CONFIG_FILE=/bootstrap.json \ + -e SIDECAR_DEBUG_RESPONSE=False \ + --mount type=bind,src=,dst=/bootstrap.json \ + -p 5000:5000\ + ghcr.io/janssenproject/jans/cedarling-flask-sidecar:1.3.0-1 + ``` +- The service is running on `http://0.0.0.0:5000`. OpenAPI documentation is available at `/swagger-ui` + +## Docker Compose Instructions (for development) - Clone the [Janssen](https://github.com/JanssenProject/jans) repository - Navigate to `jans/jans-cedarling/flask-sidecar/` - Modify the `secrets/bootstrap.json` file to your specifications. Configuration values are described [here](https://github.com/JanssenProject/jans/blob/main/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi). + - The default configuration expects you to provide a URL to a policy store file via `CEDARLING_POLICY_STORE_URI`. If you want to use a local policy store via `CEDARLING_POLICY_STORE_FN`, you need to mount it inside the docker image. Place your policy store file in the `secrets` folder and edit the Dockerfile at line 46 to add this line: + + ``` + ... + COPY --chown=1000:1000 ./secrets/.json /api/ + ... + ``` - Run `docker compose up` - The service is running on `http://0.0.0.0:5000`. OpenAPI documentation is available at `/swagger-ui` diff --git a/jans-cedarling/flask-sidecar/docker-compose.yml b/jans-cedarling/flask-sidecar/docker-compose.yml index aa5040ca56c..6c93d08202c 100644 --- a/jans-cedarling/flask-sidecar/docker-compose.yml +++ b/jans-cedarling/flask-sidecar/docker-compose.yml @@ -9,6 +9,7 @@ services: - FLASK_APP=main.core:app - APP_MODE=${APP_MODE:-development} - CEDARLING_BOOTSTRAP_CONFIG_FILE=/run/secrets/cedarling_bootstrap_config_file + - SIDECAR_DEBUG_RESPOSE={SIDECAR_DEBUG_RESPOSE:-False} secrets: - cedarling_bootstrap_config_file secrets: diff --git a/jans-cedarling/flask-sidecar/main/app.py b/jans-cedarling/flask-sidecar/main/app.py index 42b61c0bd56..7343d508d09 100644 --- a/jans-cedarling/flask-sidecar/main/app.py +++ b/jans-cedarling/flask-sidecar/main/app.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from flask import Flask from main.config import ConfigLoader, get_instance_path from main.extensions import api, cors, cedarling diff --git a/jans-cedarling/flask-sidecar/main/base/cedarling/cedarling.py b/jans-cedarling/flask-sidecar/main/base/cedarling/cedarling.py index 554fa02357d..7e15f38b113 100644 --- a/jans-cedarling/flask-sidecar/main/base/cedarling/cedarling.py +++ b/jans-cedarling/flask-sidecar/main/base/cedarling/cedarling.py @@ -1,6 +1,27 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from cedarling_python import BootstrapConfig from cedarling_python import Cedarling -from cedarling_python import ResourceData, Request, AuthorizeResult, AuthorizeResultResponse +from cedarling_python import ( + ResourceData, + Request, + AuthorizeResultResponse, + Tokens +) from main.logger import logger from flask import Flask import json @@ -19,6 +40,7 @@ def __init__(self, app=None): def init_app(self, app: Flask): self._bootstrap_config = app.config.get("CEDARLING_BOOTSTRAP_CONFIG", "{}") + self.debug_response: bool = app.config.get("SIDECAR_DEBUG_RESPONSE", False) app.extensions = getattr(app, "extensions", {}) app.extensions["cedarling_client"] = self self.initialize_cedarling() @@ -42,11 +64,17 @@ def generate_resource(self, resource: DictType) -> ResourceData: resource_entity = ResourceData.from_dict(resource_entity_dict) return resource_entity - def validate_context(self, context: DictType) -> bool: - for key in KEYS_LIST: - if context.get(key, None) is None: - return False - return True + def validate_subject(self, subject: DictType) -> bool: + if "properties" not in subject: + return False + count = 0 + i = 0 + while count == 0 and i < len(KEYS_LIST): + key = KEYS_LIST[i] + if subject["properties"].get(key, None) is not None: + count += 1 + i += 1 + return True if count > 0 else False def generate_report(self, authorize_response: AuthorizeResultResponse | None, report: str) -> _t.List[str]: result = [] @@ -59,6 +87,13 @@ def generate_report(self, authorize_response: AuthorizeResultResponse | None, re for error in diagnostic.errors: result.append(error.error) return result + + def get_reason(self, authorize_response: AuthorizeResultResponse | None) -> _t.List[str]: + result = [] + if authorize_response is not None: + for reason in authorize_response.diagnostics.reason: + result.append(reason) + return result def authorize(self, subject: DictType, @@ -68,7 +103,7 @@ def authorize(self, result_dict = {} action_entity = action.get("name", "") resource_entity = self.generate_resource(resource) - if not self.validate_context(context): + if not self.validate_subject(subject): result_dict["decision"] = False result_dict["context"] = { "id": "-1", @@ -77,13 +112,12 @@ def authorize(self, } } return result_dict - access_token = context.get("access_token", "") - id_token = context.get("id_token", "") - userinfo_token = context.get("userinfo_token", "") - for key in KEYS_LIST: - context.pop(key) + access_token = subject["properties"].get("access_token", None) + id_token = subject["properties"].get("id_token", None) + userinfo_token = subject["properties"].get("userinfo_token", None) try: - request = Request(access_token, id_token, userinfo_token, action_entity, resource_entity, context) + tokens = Tokens(access_token, id_token, userinfo_token) + request = Request(tokens, action_entity, resource_entity, context) authorize_result = self._cedarling.authorize(request) except Exception as e: result_dict["decision"] = False @@ -100,6 +134,7 @@ def authorize(self, result_dict["decision"] = True else: result_dict["decision"] = False + if self.debug_response: person_result = authorize_result.person() workload_result = authorize_result.workload() person_value = None @@ -108,18 +143,22 @@ def authorize(self, person_value = person_result.decision.value if workload_result is not None: workload_value = workload_result.decision.value - person_diagnostic = self.generate_report(person_result, "reason") - person_error = self.generate_report(person_result, "error") - workload_diagnostic = self.generate_report(workload_result, "reason") - workload_error = self.generate_report(workload_result, "error") - result_dict["context"] = { - "reason_admin": { - "person evaluation": person_value, - "person diagnostics": person_diagnostic, - "person error": person_error, - "workload evaluation": workload_value, - "workload diagnostics": workload_diagnostic, - "workload_error": workload_error + person_diagnostic = self.generate_report(person_result, "reason") + person_error = self.generate_report(person_result, "error") + person_reason = self.get_reason(person_result) + workload_diagnostic = self.generate_report(workload_result, "reason") + workload_error = self.generate_report(workload_result, "error") + workload_reason = self.get_reason(workload_result) + result_dict["context"] = { + "reason_admin": { + "person evaluation": person_value, + "person diagnostics": person_diagnostic, + "person error": person_error, + "person reason": person_reason, + "workload evaluation": workload_value, + "workload diagnostics": workload_diagnostic, + "workload error": workload_error, + "workload reason": workload_reason + } } - } return result_dict diff --git a/jans-cedarling/flask-sidecar/main/config.py b/jans-cedarling/flask-sidecar/main/config.py index d6a4cc23dc2..8c578fc502e 100644 --- a/jans-cedarling/flask-sidecar/main/config.py +++ b/jans-cedarling/flask-sidecar/main/config.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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 os from pathlib import Path from main.logger import logger @@ -25,6 +41,11 @@ class BaseConfig: exit() with open(CEDARLING_BOOTSTRAP_CONFIG_FILE, "r") as f: CEDARLING_BOOTSTRAP_CONFIG = f.read() + SIDECAR_DEBUG_RESPONSE = os.getenv("SIDECAR_DEBUG_RESPONSE", "False") + if SIDECAR_DEBUG_RESPONSE == "True": + SIDECAR_DEBUG_RESPONSE = True + else: + SIDECAR_DEBUG_RESPONSE = False class TestingConfig(BaseConfig): TESTING = True diff --git a/jans-cedarling/flask-sidecar/main/core.py b/jans-cedarling/flask-sidecar/main/core.py index ed940548dda..e26857ecaa8 100644 --- a/jans-cedarling/flask-sidecar/main/core.py +++ b/jans-cedarling/flask-sidecar/main/core.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from main.app import create_app app = create_app() diff --git a/jans-cedarling/flask-sidecar/main/extensions/__init__.py b/jans-cedarling/flask-sidecar/main/extensions/__init__.py index cae351c3c70..db04fdf2c9a 100644 --- a/jans-cedarling/flask-sidecar/main/extensions/__init__.py +++ b/jans-cedarling/flask-sidecar/main/extensions/__init__.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from flask_smorest import Api, Blueprint from flask_cors import CORS from main.base.cedarling.cedarling import CedarlingInstance diff --git a/jans-cedarling/flask-sidecar/main/extensions/routes_extension.py b/jans-cedarling/flask-sidecar/main/extensions/routes_extension.py index 707a4298383..b3a2793c298 100644 --- a/jans-cedarling/flask-sidecar/main/extensions/routes_extension.py +++ b/jans-cedarling/flask-sidecar/main/extensions/routes_extension.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from main.v1.resource import blp as evaluation_routes def register_routes(app): diff --git a/jans-cedarling/flask-sidecar/main/logger.py b/jans-cedarling/flask-sidecar/main/logger.py index 325696b40eb..73aceaa6bc6 100644 --- a/jans-cedarling/flask-sidecar/main/logger.py +++ b/jans-cedarling/flask-sidecar/main/logger.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from datetime import datetime from structlog import configure, stdlib, processors, get_logger diff --git a/jans-cedarling/flask-sidecar/main/v1/resource.py b/jans-cedarling/flask-sidecar/main/v1/resource.py index 402047f571a..d35a6a6b362 100644 --- a/jans-cedarling/flask-sidecar/main/v1/resource.py +++ b/jans-cedarling/flask-sidecar/main/v1/resource.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. +""" + from main.extensions import BlueprintApi from main.v1.schema import EvaluationRequestSchema, DecisionSchema, WellKnownSchema from flask.views import MethodView diff --git a/jans-cedarling/flask-sidecar/main/v1/schema.py b/jans-cedarling/flask-sidecar/main/v1/schema.py index cb59030e69c..c79812b1635 100644 --- a/jans-cedarling/flask-sidecar/main/v1/schema.py +++ b/jans-cedarling/flask-sidecar/main/v1/schema.py @@ -1,5 +1,21 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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 marshmallow as ma -from marshmallow import EXCLUDE, RAISE, ValidationError, validate, validates_schema +from marshmallow import EXCLUDE class BaseSchema(ma.Schema): class Meta(ma.Schema.Meta): diff --git a/jans-cedarling/flask-sidecar/poetry.lock b/jans-cedarling/flask-sidecar/poetry.lock index 58ed84d8303..b0508c6f1e4 100644 --- a/jans-cedarling/flask-sidecar/poetry.lock +++ b/jans-cedarling/flask-sidecar/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "apispec" -version = "6.7.1" +version = "6.8.1" description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)." optional = false python-versions = ">=3.9" files = [ - {file = "apispec-6.7.1-py3-none-any.whl", hash = "sha256:d99e7a564f3871327c17b3e43726cc1e6ade2c97aa05706644a48818fc37999e"}, - {file = "apispec-6.7.1.tar.gz", hash = "sha256:c01b8b6ff40ffedf55b79a67f9dd920e9b2fc3909aae116facf6c8372a08b933"}, + {file = "apispec-6.8.1-py3-none-any.whl", hash = "sha256:eacba00df745efc9adb2a45cf992300e87938582077e101fb26b78ecf4320beb"}, + {file = "apispec-6.8.1.tar.gz", hash = "sha256:f4916cbb7be156963b18f5929a0e42bd2349135834b680a81b12432bcfaa9a39"}, ] [package.dependencies] @@ -17,7 +17,7 @@ packaging = ">=21.3" [package.extras] dev = ["apispec[tests]", "pre-commit (>=3.5,<5.0)", "tox"] -docs = ["apispec[marshmallow]", "pyyaml (==6.0.2)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-rtd-theme (==3.0.1)"] +docs = ["apispec[marshmallow]", "pyyaml (==6.0.2)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-rtd-theme (==3.0.2)"] marshmallow = ["marshmallow (>=3.18.0)"] tests = ["apispec[marshmallow,yaml]", "openapi-spec-validator (==0.7.1)", "pytest"] yaml = ["PyYAML (>=3.10)"] @@ -35,13 +35,13 @@ files = [ [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -58,6 +58,97 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.6.10" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "flask" version = "3.1.0" @@ -117,6 +208,38 @@ dev = ["flask-smorest[tests]", "pre-commit (>=3.6,<5.0)", "tox"] docs = ["alabaster (==1.0.0)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)"] tests = ["PyYAML (==6.0.2)", "apispec (==6.7.0)", "coverage (==7.6.4)", "flask (==3.0.3)", "marshmallow (==3.23.0)", "pytest (==8.3.3)", "pytest-cov (==5.0.0)", "webargs (==8.6.0)", "werkzeug (==3.0.4)"] +[[package]] +name = "gunicorn" +version = "22.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, + {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -130,13 +253,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -217,13 +340,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.23.1" +version = "3.25.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" files = [ - {file = "marshmallow-3.23.1-py3-none-any.whl", hash = "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491"}, - {file = "marshmallow-3.23.1.tar.gz", hash = "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468"}, + {file = "marshmallow-3.25.1-py3-none-any.whl", hash = "sha256:ec5d00d873ce473b7f2ffcb7104286a376c354cab0c2fa12f5573dab03e87210"}, + {file = "marshmallow-3.25.1.tar.gz", hash = "sha256:f4debda3bb11153d81ac34b0d582bf23053055ee11e791b54b4b35493468040a"}, ] [package.dependencies] @@ -231,29 +354,29 @@ packaging = ">=17.0" [package.extras] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] -docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.14)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"] +docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] tests = ["pytest", "simplejson"] [[package]] name = "maturin" -version = "1.7.4" +version = "1.8.1" description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" optional = false python-versions = ">=3.7" files = [ - {file = "maturin-1.7.4-py3-none-linux_armv6l.whl", hash = "sha256:eb7b7753b733ae302c08f80bca7b0c3fda1eea665c2b1922c58795f35a54c833"}, - {file = "maturin-1.7.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0182a9638399c8835afd39d2aeacf56908e37cba3f7abb15816b9df6774fab81"}, - {file = "maturin-1.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:41a29c5b23f3ebdfe7633637e3de256579a1b2700c04cd68c16ed46934440c5a"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:23fae44e345a2da5cb391ae878726fb793394826e2f97febe41710bd4099460e"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:8b441521c151f0dbe70ed06fb1feb29b855d787bda038ff4330ca962e5d56641"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7ccb66d0c5297cf06652c5f72cb398f447d3a332eccf5d1e73b3fe14dbc9498c"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:71f668f19e719048605dbca6a1f4d0dc03b987c922ad9c4bf5be03b9b278e4c3"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:c179fcb2b494f19186781b667320e43d95b3e71fcb1c98fffad9ef6bd6e276b3"}, - {file = "maturin-1.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd5b4b95286f2f376437340f8a4908f4761587212170263084455be8099099a7"}, - {file = "maturin-1.7.4-py3-none-win32.whl", hash = "sha256:35487a424467d1fda4567cbb02d21f09febb10eda22f5fd647b130bc0767dc61"}, - {file = "maturin-1.7.4-py3-none-win_amd64.whl", hash = "sha256:f70c1c8ec9bd4749a53c0f3ae8fdbb326ce45be4f1c5551985ee25a6d7150328"}, - {file = "maturin-1.7.4-py3-none-win_arm64.whl", hash = "sha256:f3d38a6d0c7fd7b04bec30dd470b2173cf9bd184ab6220c1acaf49df6b48faf5"}, - {file = "maturin-1.7.4.tar.gz", hash = "sha256:2b349d742a07527d236f0b4b6cab26f53ebecad0ceabfc09ec4c6a396e3176f9"}, + {file = "maturin-1.8.1-py3-none-linux_armv6l.whl", hash = "sha256:7e590a23d9076b8a994f2e67bc63dc9a2d1c9a41b1e7b45ac354ba8275254e89"}, + {file = "maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d8251a95682c83ea60988c804b620c181911cd824aa107b4a49ac5333c92968"}, + {file = "maturin-1.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9fc1a4354cac5e32c190410208039812ea88c4a36bd2b6499268ec49ef5de00"}, + {file = "maturin-1.8.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:621e171c6b39f95f1d0df69a118416034fbd59c0f89dcaea8c2ea62019deecba"}, + {file = "maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:98f638739a5132962347871b85c91f525c9246ef4d99796ae98a2031e3df029f"}, + {file = "maturin-1.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f9f5c47521924b6e515cbc652a042fe5f17f8747445be9d931048e5d8ddb50a4"}, + {file = "maturin-1.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:0f4407c7353c31bfbb8cdeb82bc2170e474cbfb97b5ba27568f440c9d6c1fdd4"}, + {file = "maturin-1.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ec49cd70cad3c389946c6e2bc0bd50772a7fcb463040dd800720345897eec9bf"}, + {file = "maturin-1.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08767d794de8f8a11c5c8b1b47a4ff9fb6ae2d2d97679e27030f2f509c8c2a0"}, + {file = "maturin-1.8.1-py3-none-win32.whl", hash = "sha256:d678407713f3e10df33c5b3d7a343ec0551eb7f14d8ad9ba6febeb96f4e4c75c"}, + {file = "maturin-1.8.1-py3-none-win_amd64.whl", hash = "sha256:a526f90fe0e5cb59ffb81f4ff547ddc42e823bbdeae4a31012c0893ca6dcaf46"}, + {file = "maturin-1.8.1-py3-none-win_arm64.whl", hash = "sha256:e95f077fd2ddd2f048182880eed458c308571a534be3eb2add4d3dac55bf57f4"}, + {file = "maturin-1.8.1.tar.gz", hash = "sha256:49cd964aabf59f8b0a6969f9860d2cdf194ac331529caae14c884f5659568857"}, ] [package.dependencies] @@ -274,6 +397,61 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -380,13 +558,43 @@ typing = ["mypy (>=1.4)", "rich", "twisted"] [[package]] name = "tomli" -version = "2.1.0" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -430,4 +638,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "0675d7de7297345d80644938832646b44bb54db5f19b1932c1272f96c77e680d" +content-hash = "e2d07778e7aa9b1e8e5e3c76bb0ef0b9f9af86249da8655d985a7b5848f5180d" diff --git a/jans-cedarling/flask-sidecar/pyproject.toml b/jans-cedarling/flask-sidecar/pyproject.toml index 0c30b0be9ea..10b45db64b0 100644 --- a/jans-cedarling/flask-sidecar/pyproject.toml +++ b/jans-cedarling/flask-sidecar/pyproject.toml @@ -1,10 +1,11 @@ [tool.poetry] name = "flask-sidecar" -version = "0.0.0" +version = "1.3.0" description = "Sidecar for cedarling" authors = ["SafinWasi <6601566+SafinWasi@users.noreply.github.com>"] license = "Apache-2.0" readme = "README.md" +packages = [{ include = "main" }] [tool.poetry.dependencies] python = "^3.10" @@ -16,9 +17,13 @@ flask-cors = "^5.0.0" python-dotenv = "^1.0.1" structlog = "^24.4.0" python-json-logger = "^2.0.7" -gunicorn = "^20.1.0" +gunicorn = "^22.0.0" +[tool.poetry.group.test.dependencies] +pytest = "^8.3.4" +pytest-cov = "^6.0.0" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/jans-cedarling/flask-sidecar/tests/conftest.py b/jans-cedarling/flask-sidecar/tests/conftest.py new file mode 100644 index 00000000000..c2f9f9db2d8 --- /dev/null +++ b/jans-cedarling/flask-sidecar/tests/conftest.py @@ -0,0 +1,33 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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 os + +import pytest +from main.extensions import cedarling + +current_path = os.path.dirname(os.path.realpath(__file__)) + + +@pytest.fixture(autouse=True) +def env_setup(monkeypatch): + monkeypatch.setenv("APP_MODE", "testing") + monkeypatch.setenv("CEDARLING_BOOTSTRAP_CONFIG_FILE", os.path.join(current_path, "test_secrets", "cedarling_test_config.json")) + monkeypatch.setattr(cedarling, "initialize_cedarling", lambda: None) + +@pytest.fixture(scope="function") +def mock_cedarling(monkeypatch): + pass diff --git a/jans-cedarling/flask-sidecar/tests/test_app.py b/jans-cedarling/flask-sidecar/tests/test_app.py new file mode 100644 index 00000000000..867c9d7477f --- /dev/null +++ b/jans-cedarling/flask-sidecar/tests/test_app.py @@ -0,0 +1,60 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. + +test_app +~~~~~~~~ +This module consists of testcases for :module:`main.app` module. +""" +import os + +import pytest + + +@pytest.mark.parametrize( + "mode", + [ + ("development"), + ("testing"), + ("production"), + ("dummy"), + ], +) +def test_create_app(mode): + """Test factory function that creates Flask's app.""" + from main.app import create_app + os.environ["APP_MODE"] = mode + config = { + "testing": { + "debug": True, + "testing": True + }, + "development": { + "debug": True, + "testing": False + }, + "production": { + "debug": False, + "testing": False + } + } + loaded_config = config.get(mode, config.get("testing")) + app = create_app() + assert loaded_config is not None + assert loaded_config.get("debug", None) is not None + assert loaded_config.get("testing", None) is not None + assert app.config["DEBUG"] is loaded_config["debug"] + assert app.config["TESTING"] is loaded_config["testing"] + # Flask app has ``app_context`` attr + assert hasattr(app, "app_context") diff --git a/jans-cedarling/flask-sidecar/tests/test_core.py b/jans-cedarling/flask-sidecar/tests/test_core.py new file mode 100644 index 00000000000..ec41bf68100 --- /dev/null +++ b/jans-cedarling/flask-sidecar/tests/test_core.py @@ -0,0 +1,26 @@ +""" +Copyright (c) 2025, Gluu, Inc. + +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. + +test_core +~~~~~~~~~ +This module consists of testcases for :module:`core` module. +""" + + +def test_core_app(): + """Test core app is a Flask app.""" + from main.core import app + + assert hasattr(app, "app_context") diff --git a/jans-cedarling/flask-sidecar/tests/test_secrets/cedarling_test_config.json b/jans-cedarling/flask-sidecar/tests/test_secrets/cedarling_test_config.json new file mode 100644 index 00000000000..fb5d711a0fa --- /dev/null +++ b/jans-cedarling/flask-sidecar/tests/test_secrets/cedarling_test_config.json @@ -0,0 +1,41 @@ +{ + "CEDARLING_APPLICATION_NAME": "My App", + "CEDARLING_POLICY_STORE_URI": "https://gluu.org", + "CEDARLING_POLICY_STORE_ID": "gICAgcHJpbmNpcGFsIGlz", + "CEDARLING_LOG_TYPE": "std_out", + "CEDARLING_LOG_LEVEL": "INFO", + "CEDARLING_LOG_TTL": null, + "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" +} diff --git a/jans-cedarling/flask-sidecar/tox.ini b/jans-cedarling/flask-sidecar/tox.ini index e69de29bb2d..343b7139804 100644 --- a/jans-cedarling/flask-sidecar/tox.ini +++ b/jans-cedarling/flask-sidecar/tox.ini @@ -0,0 +1,20 @@ +[tox] +envlist = py310 +skip_missing_interpreters = true +# align with pyproject.toml +isolated_build = true + +[testenv] +passenv = * +deps = + pytest + pytest-cov + pytest-env + pytest-structlog +allowlist_externals = poetry +skip_install = true +commands_pre = + poetry install +commands = + poetry install -v + poetry run pytest -v --cov-config=.coveragerc --cov=main --cov-report=term-missing:skip-covered --cov-report=xml tests/ 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..4347be0c19d 100644 --- a/jans-cedarling/sparkv/Cargo.toml +++ b/jans-cedarling/sparkv/Cargo.toml @@ -13,3 +13,7 @@ homepage = "https://crates.io/crates/sparkv" [dependencies] thiserror = { workspace = true } +chrono = { workspace = true } + +[dev-dependencies] +serde_json = "*" diff --git a/jans-cedarling/sparkv/README.md b/jans-cedarling/sparkv/README.md index ab4aa01dc7e..ba6c66a180a 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::seconds(60)); ``` 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..0e3899a615c 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::seconds(60 * 60), + default_ttl: Duration::seconds(5 * 60), // 5 minutes auto_clear_expired: true, } } @@ -41,8 +43,8 @@ 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::seconds(60 * 60)); + assert_eq!(config.default_ttl, Duration::seconds(5 * 60)); assert!(config.auto_clear_expired); } } diff --git a/jans-cedarling/sparkv/src/expentry.rs b/jans-cedarling/sparkv/src/expentry.rs index 014c7f98226..a4a1cd2e008 100644 --- a/jans-cedarling/sparkv/src/expentry.rs +++ b/jans-cedarling/sparkv/src/expentry.rs @@ -6,23 +6,25 @@ */ 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: S, expiration: Duration) -> Self { + let expired_at: DateTime = Utc::now() + expiration; Self { - key: String::from(key), + key: key.as_ref().into(), expired_at, } } - pub fn from_kv_entry(kv_entry: &KvEntry) -> Self { + pub fn from_kv_entry(kv_entry: &KvEntry) -> Self { Self { key: kv_entry.key.clone(), expired_at: kv_entry.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,19 +59,15 @@ mod tests { #[test] fn test_new() { - let item = ExpEntry::new("key", std::time::Duration::from_secs(10)); + let item = ExpEntry::new("key", Duration::seconds(10)); 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::seconds(9)); + assert!(item.expired_at <= Utc::now() + Duration::seconds(10)); } #[test] fn test_from_kventry() { - let kv_entry = KvEntry::new( - "keyFromKV", - "value from KV", - std::time::Duration::from_secs(10), - ); + let kv_entry = KvEntry::new("keyFromKV", "value from KV", Duration::seconds(10)); let exp_item = ExpEntry::from_kv_entry(&kv_entry); assert_eq!(exp_item.key, "keyFromKV"); assert_eq!(exp_item.expired_at, kv_entry.expired_at); @@ -77,17 +75,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::seconds(10)); + let item_big = ExpEntry::new("k2", Duration::seconds(8000)); 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::seconds(0)); + 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..c8e38c62ee5 100644 --- a/jans-cedarling/sparkv/src/kventry.rs +++ b/jans-cedarling/sparkv/src/kventry.rs @@ -4,20 +4,22 @@ * * Copyright (c) 2024 U-Zyn Chua */ +use chrono::Duration; +use chrono::prelude::*; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct KvEntry { +pub struct KvEntry { pub key: String, - pub value: String, - pub expired_at: std::time::Instant, + pub value: T, + 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; +impl KvEntry { + pub fn new>(key: S, value: T, expiration: Duration) -> Self { + let expired_at: DateTime = Utc::now() + expiration; Self { - key: String::from(key), - value: String::from(value), + key: key.as_ref().into(), + value, expired_at, } } @@ -29,10 +31,10 @@ mod tests { #[test] fn test_new() { - let item = KvEntry::new("key", "value", std::time::Duration::from_secs(10)); + let item = KvEntry::::new("key", "value".into(), Duration::seconds(10)); 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::seconds(9)); + assert!(item.expired_at <= Utc::now() + Duration::seconds(10)); } } diff --git a/jans-cedarling/sparkv/src/lib.rs b/jans-cedarling/sparkv/src/lib.rs index 8c76171f013..c44c5750f2e 100644 --- a/jans-cedarling/sparkv/src/lib.rs +++ b/jans-cedarling/sparkv/src/lib.rs @@ -15,13 +15,62 @@ pub use error::Error; pub use expentry::ExpEntry; pub use kventry::KvEntry; -pub struct SparKV { +use chrono::Duration; +use chrono::prelude::*; + +pub struct SparKV { pub config: Config, - data: std::collections::BTreeMap, + data: std::collections::BTreeMap>, expiries: std::collections::BinaryHeap, + /// An optional function that calculates the memory size of a value. + /// + /// Used by `ensure_item_size`. + /// + /// If this function is not provided, the container will enforce + /// `Config.max_item_size` on the basis of `std::mem::size_of_val` which + /// probably won't be what you expect. + size_calculator: Option usize>, +} + +/// See the SparKV::iter function +pub struct Iter<'a, T: 'a> { + btree_value_iter: std::collections::btree_map::Values<'a, String, KvEntry>, +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = (&'a String, &'a T); + + fn next(&mut self) -> Option { + self.btree_value_iter + .next() + .map(|kventry| (&kventry.key, &kventry.value)) + } + + fn size_hint(&self) -> (usize, Option) { + self.btree_value_iter.size_hint() + } } -impl SparKV { +/// See the SparKV::drain function +pub struct DrainIter { + value_iter: std::collections::btree_map::IntoValues>, +} + +impl Iterator for DrainIter { + type Item = (String, T); + + fn next(&mut self) -> Option { + self.value_iter + .next() + .map(|kventry| (kventry.key, kventry.value)) + } + + fn size_hint(&self) -> (usize, Option) { + self.value_iter.size_hint() + } +} + +impl SparKV { pub fn new() -> Self { let config = Config::new(); SparKV::with_config(config) @@ -32,58 +81,76 @@ impl SparKV { config, data: std::collections::BTreeMap::new(), expiries: std::collections::BinaryHeap::new(), + // This will underestimate the size of most things. + size_calculator: Some(|v| std::mem::size_of_val(v)), } } - pub fn set(&mut self, key: &str, value: &str) -> Result<(), Error> { + /// Provide optional size function. See SparKV.size_calculator comments. + pub fn with_config_and_sizer(config: Config, sizer: Option usize>) -> Self { + SparKV { + config, + data: std::collections::BTreeMap::new(), + expiries: std::collections::BinaryHeap::new(), + size_calculator: sizer, + } + } + + pub fn set(&mut self, key: &str, value: T) -> Result<(), Error> { 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: T, ttl: Duration) -> Result<(), Error> { self.clear_expired_if_auto(); self.ensure_capacity_ignore_key(key)?; - self.ensure_item_size(value)?; + self.ensure_item_size(&value)?; self.ensure_max_ttl(ttl)?; - let item: KvEntry = KvEntry::new(key, value, ttl); + let item: KvEntry = KvEntry::new(key, value, ttl); let exp_item: ExpEntry = ExpEntry::from_kv_entry(&item); self.expiries.push(exp_item); - self.data.insert(item.key.clone(), item); + self.data.insert(key.into(), item); Ok(()) } - pub fn get(&self, key: &str) -> Option { - let item = self.get_item(key)?; - Some(item.value.clone()) + pub fn get(&self, key: &str) -> Option<&T> { + Some(&self.get_item(key)?.value) } // Only returns if it is not yet expired - pub fn get_item(&self, key: &str) -> Option<&KvEntry> { + pub fn get_item(&self, key: &str) -> Option<&KvEntry> { let item = self.data.get(key)?; - if item.expired_at > std::time::Instant::now() { - Some(item) - } else { - None - } + (item.expired_at > Utc::now()).then_some(item) } pub fn get_keys(&self) -> Vec { - self.data - .keys() - .map(|key| key.to_string())// it clone the string - .collect() + self.data.keys().cloned().collect() } - pub fn pop(&mut self, key: &str) -> Option { + /// Return an iterator of (key,value) : (&String,&T). + pub fn iter(&self) -> Iter { + Iter { + btree_value_iter: self.data.values(), + } + } + + /// Return an iterator of (key,value) : (String,T) which empties the container. + /// All entries will be owned by the iterator, and yielded entries will not be checked against expiry. + /// All entries and expiries will be cleared. + pub fn drain(&mut self) -> DrainIter { + // assume that slightly-expired entries should be returned. + self.expiries.clear(); + let data_only = std::mem::take(&mut self.data); + DrainIter { + value_iter: data_only.into_values(), + } + } + + pub fn pop(&mut self, key: &str) -> Option { self.clear_expired_if_auto(); let item = self.data.remove(key)?; - // Does not delete from BinaryHeap as it's expensive. + // Does not delete expiry entry from BinaryHeap as it's expensive. Some(item.value) } @@ -92,7 +159,7 @@ impl SparKV { } pub fn is_empty(&self) -> bool { - self.data.len() == 0 + self.data.is_empty() } pub fn contains_key(&self, key: &str) -> bool { @@ -101,29 +168,27 @@ impl SparKV { pub fn clear_expired(&mut self) -> usize { let mut cleared_count: usize = 0; - loop { - let peeked = self.expiries.peek().cloned(); - match peeked { - Some(exp_item) => { - if exp_item.is_expired() { - let kv_entry = self.data.get(&exp_item.key).unwrap(); - if kv_entry.key == exp_item.key - && kv_entry.expired_at == exp_item.expired_at - { - cleared_count += 1; - self.pop(&exp_item.key); - } - self.expiries.pop(); - } else { - break; - } - }, - None => break, + while let Some(exp_item) = self.expiries.peek().cloned() { + if exp_item.is_expired() { + let kv_entry = self.data.get(&exp_item.key).unwrap(); + if kv_entry.key == exp_item.key && kv_entry.expired_at == exp_item.expired_at { + cleared_count += 1; + self.pop(&exp_item.key); + } + self.expiries.pop(); + } else { + break; } } cleared_count } + /// Empty the container. That is, remove all key-values and expiries. + pub fn clear(&mut self) { + self.data.clear(); + self.expiries.clear(); + } + fn clear_expired_if_auto(&mut self) { if self.config.auto_clear_expired { self.clear_expired(); @@ -144,14 +209,16 @@ impl SparKV { self.ensure_capacity() } - fn ensure_item_size(&self, value: &str) -> Result<(), Error> { - if value.len() > self.config.max_item_size { - return Err(Error::ItemSizeExceeded); + fn ensure_item_size(&self, value: &T) -> Result<(), Error> { + if let Some(calc) = self.size_calculator { + if calc(value) > self.config.max_item_size { + return Err(Error::ItemSizeExceeded); + } } 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); } @@ -159,214 +226,14 @@ impl SparKV { } } -impl Default for SparKV { +impl Default for SparKV { fn default() -> Self { Self::new() } } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sparkv_config() { - 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)); - } +mod tests; - #[test] - fn test_sparkv_new_with_config() { - let config: Config = Config::new(); - let sparkv = SparKV::with_config(config); - assert_eq!(sparkv.config, config); - } - - #[test] - fn test_len_is_empty() { - let mut sparkv = SparKV::new(); - assert_eq!(sparkv.len(), 0); - assert!(sparkv.is_empty()); - - _ = sparkv.set("keyA", "value"); - assert_eq!(sparkv.len(), 1); - assert!(!sparkv.is_empty()); - } - - #[test] - fn test_set_get() { - let mut sparkv = SparKV::new(); - _ = sparkv.set("keyA", "value"); - assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - assert_eq!(sparkv.expiries.len(), 1); - - // Overwrite the value - _ = sparkv.set("keyA", "value2"); - assert_eq!(sparkv.get("keyA"), Some(String::from("value2"))); - assert_eq!(sparkv.expiries.len(), 2); - - assert!(sparkv.get("non-existent").is_none()); - } - - #[test] - fn test_get_item() { - let mut sparkv = SparKV::new(); - let item = KvEntry::new("keyARaw", "value99", std::time::Duration::from_secs(1)); - sparkv.data.insert(item.key.clone(), item); - let get_result = sparkv.get_item("keyARaw"); - let unwrapped = get_result.unwrap(); - - assert!(get_result.is_some()); - assert_eq!(unwrapped.key, "keyARaw"); - assert_eq!(unwrapped.value, "value99"); - - assert!(sparkv.get_item("non-existent").is_none()); - } - - #[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"))); - - std::thread::sleep(std::time::Duration::from_millis(60)); - assert_eq!(sparkv.get("kkk"), None); - } - - #[test] - fn test_set_should_fail_if_capacity_exceeded() { - let mut config: Config = Config::new(); - config.max_items = 2; - - let mut sparkv = SparKV::with_config(config); - let mut set_result = sparkv.set("keyA", "value"); - assert!(set_result.is_ok()); - assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - - set_result = sparkv.set("keyB", "value2"); - assert!(set_result.is_ok()); - - set_result = sparkv.set("keyC", "value3"); - assert!(set_result.is_err()); - assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded); - assert!(sparkv.get("keyC").is_none()); - - // Overwrite existing key should not err - set_result = sparkv.set("keyB", "newValue1234"); - assert!(set_result.is_ok()); - assert_eq!(sparkv.get("keyB"), Some(String::from("newValue1234"))); - } - - #[test] - 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)); - - assert_eq!(sparkv.get("longer"), Some(String::from("value"))); - assert_eq!(sparkv.get("shorter"), Some(String::from("value"))); - assert!( - sparkv.get_item("longer").unwrap().expired_at - > sparkv.get_item("shorter").unwrap().expired_at - ); - assert!( - sparkv.get_item("longest").unwrap().expired_at - > sparkv.get_item("longer").unwrap().expired_at - ); - } - - #[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); - 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)); - assert!(set_result_ok.is_ok()); - - let set_result_ok_2 = - sparkv.set_with_ttl("exact", "ok", std::time::Duration::from_secs(3600)); - 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)); - assert!(set_result_not_ok.is_err()); - assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); - } - - #[test] - fn test_delete() { - let mut sparkv = SparKV::new(); - _ = sparkv.set("keyA", "value"); - assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - assert_eq!(sparkv.expiries.len(), 1); - - let deleted_value = sparkv.pop("keyA"); - assert_eq!(deleted_value, Some(String::from("value"))); - assert!(sparkv.get("keyA").is_none()); - assert_eq!(sparkv.expiries.len(), 1); // it does not delete - } - - #[test] - fn test_clear_expired() { - 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); - } - - #[test] - fn test_clear_expired_with_overwritten_key() { - 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)); - assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries - assert_eq!(sparkv.len(), 2); - - let cleared_count = sparkv.clear_expired(); - assert_eq!(cleared_count, 0); // no longer expiring - assert_eq!(sparkv.expiries.len(), 2); // should have cleared the expiries - assert_eq!(sparkv.len(), 2); // but not actually deleting - } - - #[test] - fn test_clear_expired_with_auto_clear_expired_enabled() { - 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)); - 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)); - assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries - assert_eq!(sparkv.len(), 3); // but not actually deleting - } -} +#[cfg(test)] +mod test_json_value; diff --git a/jans-cedarling/sparkv/src/test_json_value.rs b/jans-cedarling/sparkv/src/test_json_value.rs new file mode 100644 index 00000000000..f9e0afc1712 --- /dev/null +++ b/jans-cedarling/sparkv/src/test_json_value.rs @@ -0,0 +1,177 @@ +// 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::Config; +use crate::SparKV; +use serde_json; + +#[cfg(test)] +fn json_value_size(value: &serde_json::Value) -> usize { + std::mem::size_of::() + + match value { + serde_json::Value::Null => 0, + serde_json::Value::Bool(_) => 0, + serde_json::Value::Number(_) => 0, // Incorrect if arbitrary_precision is enabled. oh well + serde_json::Value::String(s) => s.capacity(), + serde_json::Value::Array(a) => { + a.iter().map(json_value_size).sum::() + + a.capacity() * std::mem::size_of::() + }, + serde_json::Value::Object(o) => o + .iter() + .map(|(k, v)| { + std::mem::size_of::() + + k.capacity() + + json_value_size(v) + + std::mem::size_of::() * 3 // As a crude approximation, I pretend each map entry has 3 words of overhead + }) + .sum(), + } +} + +fn first_json() -> serde_json::Value { + serde_json::json!({ + "name" : "first_json", + "compile_kind": 0, + "config": 3355035640151825893usize, + "declared_features": ["bstr", "bytes", "default", "inline", "serde", "text", "unicode", "unicode-segmentation"], + "deps": [], + "features": ["default", "text"], + "local": [ + { + "CheckDepInfo": { + "checksum": false, + "dep_info": "debug/.fingerprint/similar-056a66f4ad898c88/dep-lib-similar" + } + } + ], + "metadata": 943206097653546126i64, + "path": 7620609427446831929u64, + "profile": 10243973527296709326usize, + "rustc": 11594289678289209806usize, + "rustflags": [ + "-C", + "link-arg=-fuse-ld=/usr/bin/mold" + ], + "target": 15605724903113465739u64 + }) +} + +fn second_json() -> serde_json::Value { + serde_json::json!({ + "name" : "second_json", + "compile_kind": 0, + "config": 5533035641051825893usize, + "declared_features": ["bstr", "bytes", "default", "inline", "serde", "text", "unicode", "unicode-segmentation"], + "deps": [], + "features": ["default", "text"], + "local": [ + { + "CheckDepInfo": { + "checksum": false, + "dep_info": "debug/.fingerprint/utterly-different-0a6664d898c8f8a5/dep-lib-utterly-different" + } + } + ], + "metadata": 943206097653546126i64, + "path": 7620609427446831929u64, + "profile": 10243973527296709326usize, + "rustc": 11594289678289209806usize, + "rustflags": [ + "-C", + "link-arg=-fuse-ld=/usr/bin/mold" + ], + "target": 15605724903113465739u64 + }) +} + +#[test] +fn simple_serde_json() { + let config: Config = Config::new(); + let mut sparkv = + SparKV::::with_config_and_sizer(config, Some(json_value_size)); + let json = first_json(); + sparkv.set("first", json.clone()).unwrap(); + let stored_first = sparkv.get("first").unwrap(); + assert_eq!(&json, stored_first); +} + +#[test] +fn type_serde_json() { + let config: Config = Config::new(); + let mut sparkv = + SparKV::::with_config_and_sizer(config, Some(json_value_size)); + let json = first_json(); + sparkv.set("first", json.clone()).unwrap(); + + // now make sure it's actually stored as the value, not as a String + let kv = sparkv.get_item("first").unwrap(); + use std::any::{Any, TypeId}; + assert_eq!(kv.value.type_id(), TypeId::of::()); +} + +#[test] +fn fails_size_calculator() { + // create this first, so we know what item size is too large + let json = first_json(); + + let mut config: Config = Config::new(); + // set item size to something smaller than item + config.max_item_size = json_value_size(&json) / 2; + let mut sparkv = + SparKV::::with_config_and_sizer(config, Some(json_value_size)); + + let should_be_error = sparkv.set("first", json.clone()); + assert_eq!(should_be_error, Err(crate::Error::ItemSizeExceeded)); +} + +#[test] +fn two_json_items() { + let mut sparkv = SparKV::::new(); + sparkv.set("first", first_json()).unwrap(); + sparkv.set("second", second_json()).unwrap(); + + let fj = sparkv.get("first").unwrap(); + assert_eq!( + fj.pointer("/name").unwrap(), + &serde_json::Value::String("first_json".into()) + ); + + let sj = sparkv.get("second").unwrap(); + assert_eq!( + sj.pointer("/name").unwrap(), + &serde_json::Value::String("second_json".into()) + ); +} + +#[test] +fn drain_all_json_items() { + let mut sparkv = SparKV::::new(); + sparkv.set("first", first_json()).unwrap(); + sparkv.set("second", second_json()).unwrap(); + + let all_items = sparkv.drain(); + let all_values = all_items.map(|(_, v)| v).collect::>(); + assert_eq!(all_values, vec![first_json(), second_json()]); + + assert!(sparkv.is_empty(), "sparkv not empty"); +} + +#[test] +fn rc_json_items() { + use std::rc::Rc; + let mut sparkv = SparKV::>::new(); + sparkv.set("first", Rc::new(first_json())).unwrap(); + sparkv.set("second", Rc::new(second_json())).unwrap(); + + let all_items = sparkv.drain(); + let all_values = all_items.map(|(_, v)| v).collect::>(); + assert_eq!(all_values, vec![ + Rc::new(first_json()), + Rc::new(second_json()) + ]); + + assert!(sparkv.is_empty(), "sparkv not empty"); +} diff --git a/jans-cedarling/sparkv/src/tests.rs b/jans-cedarling/sparkv/src/tests.rs new file mode 100644 index 00000000000..848fdd9db98 --- /dev/null +++ b/jans-cedarling/sparkv/src/tests.rs @@ -0,0 +1,266 @@ +// 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::*; + +#[test] +fn test_sparkv_config() { + 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, Duration::seconds(60 * 60)); +} + +#[test] +fn test_sparkv_new_with_config() { + let config: Config = Config::new(); + let sparkv = SparKV::::with_config(config); + assert_eq!(sparkv.config, config); +} + +#[test] +fn test_len_is_empty() { + let mut sparkv = SparKV::::new(); + assert_eq!(sparkv.len(), 0); + assert!(sparkv.is_empty()); + + _ = sparkv.set("keyA", "value".to_string()); + assert_eq!(sparkv.len(), 1); + assert!(!sparkv.is_empty()); +} + +#[test] +fn test_set_get() { + let mut sparkv = SparKV::::new(); + _ = sparkv.set("keyA", "value".into()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value"))); + assert_eq!(sparkv.expiries.len(), 1); + + // Overwrite the value + _ = sparkv.set("keyA", "value2".into()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value2"))); + assert_eq!(sparkv.expiries.len(), 2); + + assert!(sparkv.get("non-existent").is_none()); +} + +#[test] +fn test_get_item() { + let mut sparkv = SparKV::new(); + let item = KvEntry::new("keyARaw", "value99", Duration::seconds(1)); + sparkv.data.insert(item.key.clone(), item); + let get_result = sparkv.get_item("keyARaw"); + let unwrapped = get_result.unwrap(); + + assert!(get_result.is_some()); + assert_eq!(unwrapped.key, "keyARaw"); + assert_eq!(unwrapped.value, "value99"); + + assert!(sparkv.get_item("non-existent").is_none()); +} + +#[test] +fn test_get_item_return_none_if_expired() { + let mut sparkv = SparKV::new(); + _ = sparkv.set_with_ttl("key", "value", Duration::microseconds(40)); + assert_eq!(sparkv.get("key"), Some(&"value")); + + std::thread::sleep(std::time::Duration::from_micros(80)); + assert_eq!(sparkv.get("key"), None); +} + +#[test] +fn test_set_should_fail_if_capacity_exceeded() { + let mut config: Config = Config::new(); + config.max_items = 2; + + let mut sparkv = SparKV::::with_config(config); + let mut set_result = sparkv.set("keyA", "value".to_string()); + assert!(set_result.is_ok()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value"))); + + set_result = sparkv.set("keyB", "value2".to_string()); + assert!(set_result.is_ok()); + + set_result = sparkv.set("keyC", "value3".to_string()); + assert!(set_result.is_err()); + assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded); + assert!(sparkv.get("keyC").is_none()); + + // Overwrite existing key should not err + set_result = sparkv.set("keyB", "newValue1234".to_string()); + assert!(set_result.is_ok()); + assert_eq!(sparkv.get("keyB"), Some(&String::from("newValue1234"))); +} + +#[test] +fn memsize_item_capacity_exceeded() { + let value: String = "jay".into(); + + let mut config: Config = Config::new(); + config.max_item_size = std::mem::size_of_val(&value) / 2; + let mut sparkv = SparKV::::with_config(config); + + let error = sparkv.set("blue", value); + assert_eq!(error, Err(crate::Error::ItemSizeExceeded)); +} + +#[test] +fn custom_item_capacity_exceeded() { + let mut config: Config = Config::new(); + config.max_item_size = 20; + let mut sparkv = SparKV::<&str>::with_config_and_sizer(config, Some(|s| s.len())); + + assert_eq!(Ok(()), sparkv.set("short", "value")); + assert_eq!( + Err(crate::Error::ItemSizeExceeded), + sparkv.set("long", "This is a value that exceeds 20 characters") + ); +} + +#[test] +fn test_set_with_ttl() { + let mut sparkv = SparKV::::new(); + _ = sparkv.set("longest", "value".into()); + _ = sparkv.set_with_ttl("longer", "value".into(), Duration::seconds(2)); + _ = sparkv.set_with_ttl("shorter", "value".into(), Duration::seconds(1)); + + assert_eq!(sparkv.get("longer"), Some(&String::from("value"))); + assert_eq!(sparkv.get("shorter"), Some(&String::from("value"))); + assert!( + sparkv.get_item("longer").unwrap().expired_at + > sparkv.get_item("shorter").unwrap().expired_at + ); + assert!( + sparkv.get_item("longest").unwrap().expired_at + > sparkv.get_item("longer").unwrap().expired_at + ); +} + +#[test] +fn test_ensure_max_ttl() { + let mut config: Config = Config::new(); + config.max_ttl = Duration::seconds(3600); + config.default_ttl = Duration::seconds(5000); + let mut sparkv = SparKV::::with_config(config); + + let set_result_long_def = sparkv.set("default is longer than max", "should fail".to_string()); + 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".into(), Duration::seconds(3599)); + assert!(set_result_ok.is_ok()); + + let set_result_ok_2 = sparkv.set_with_ttl("exact", "ok".into(), Duration::seconds(3600)); + assert!(set_result_ok_2.is_ok()); + + let set_result_not_ok = sparkv.set_with_ttl("not", "not ok".into(), Duration::seconds(33601)); + assert!(set_result_not_ok.is_err()); + assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); +} + +#[test] +fn test_delete() { + let mut sparkv = SparKV::::new(); + _ = sparkv.set("keyA", "value".to_string()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value"))); + assert_eq!(sparkv.expiries.len(), 1); + + let deleted_value = sparkv.pop("keyA"); + assert_eq!(deleted_value, Some(String::from("value"))); + assert!(sparkv.get("keyA").is_none()); + assert_eq!(sparkv.expiries.len(), 1); // it does not delete +} + +#[test] +fn test_clear_expired() { + 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", Duration::seconds(90)); + _ = sparkv.set_with_ttl("expiring", "value", Duration::milliseconds(1)); + _ = sparkv.set_with_ttl("not-expired", "value", Duration::seconds(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); +} + +#[test] +fn test_clear_expired_with_overwritten_key() { + 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", Duration::milliseconds(1)); + _ = sparkv.set_with_ttl("no-longer", "v", Duration::seconds(90)); + _ = sparkv.set_with_ttl("not-expired", "value", Duration::seconds(60)); + std::thread::sleep(std::time::Duration::from_millis(2)); + assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries + assert_eq!(sparkv.len(), 2); + + let cleared_count = sparkv.clear_expired(); + assert_eq!(cleared_count, 0); // no longer expiring + assert_eq!(sparkv.expiries.len(), 2); // should have cleared the expiries + assert_eq!(sparkv.len(), 2); // but not actually deleting +} + +#[test] +fn test_clear_expired_with_auto_clear_expired_enabled() { + 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".into(), Duration::milliseconds(1)); + _ = sparkv.set_with_ttl("no-longer", "v".into(), Duration::seconds(90)); + std::thread::sleep(std::time::Duration::from_millis(2)); + _ = sparkv.set_with_ttl("not-expired", "value".into(), Duration::seconds(60)); + assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear + assert_eq!(sparkv.len(), 2); + + // auto clear 2 + _ = sparkv.set_with_ttl("new-", "value".into(), Duration::seconds(60)); + assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries + assert_eq!(sparkv.len(), 3); // but not actually deleting +} + +#[test] +fn iterator() { + let mut sparkv = SparKV::::new(); + sparkv.set("this", "town".into()).unwrap(); + sparkv.set("woo", "oooo".into()).unwrap(); + sparkv.set("is", "coming".into()).unwrap(); + sparkv.set("like", "a".into()).unwrap(); + sparkv.set("ghost", "town".into()).unwrap(); + sparkv.set("oh", "yeah".into()).unwrap(); + + let iter = sparkv.iter(); + assert!(!sparkv.is_empty(), "sparkv should be not empty"); + assert_eq!(sparkv.get("ghost").unwrap(), "town"); + + let (keys, values): (Vec<_>, Vec<_>) = iter.unzip(); + assert_eq!(keys, vec!["ghost", "is", "like", "oh", "this", "woo"]); + assert_eq!(values, vec!["town", "coming", "a", "yeah", "town", "oooo"]); +} + +#[test] +fn drain() { + let mut sparkv = SparKV::::new(); + sparkv.set("this", "town".into()).unwrap(); + sparkv.set("woo", "oooo".into()).unwrap(); + sparkv.set("is", "coming".into()).unwrap(); + sparkv.set("like", "a".into()).unwrap(); + sparkv.set("ghost", "town".into()).unwrap(); + sparkv.set("oh", "yeah".into()).unwrap(); + + let iter = sparkv.drain(); + assert!(sparkv.is_empty(), "sparkv should be empty"); + + let (keys, values): (Vec<_>, Vec<_>) = iter.unzip(); + assert_eq!(keys, vec!["ghost", "is", "like", "oh", "this", "woo"]); + assert_eq!(values, vec!["town", "coming", "a", "yeah", "town", "oooo"]); +} diff --git a/jans-cedarling/test_utils/Cargo.toml b/jans-cedarling/test_utils/Cargo.toml index b47c9faae77..41b6014e6dc 100644 --- a/jans-cedarling/test_utils/Cargo.toml +++ b/jans-cedarling/test_utils/Cargo.toml @@ -1,8 +1,11 @@ [package] name = "test_utils" -version = "0.0.0-nightly" +version = "1.3.0" 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-cli-tui/cli_tui/version.py b/jans-cli-tui/cli_tui/version.py index b915669433e..4cedf2c31d2 100644 --- a/jans-cli-tui/cli_tui/version.py +++ b/jans-cli-tui/cli_tui/version.py @@ -3,4 +3,4 @@ https://www.apache.org/licenses/LICENSE-2.0 """ -__version__ = "0.0.0" +__version__ = "1.3.0" diff --git a/jans-config-api/common/pom.xml b/jans-config-api/common/pom.xml index a33c08a4ec7..0a183376bcf 100644 --- a/jans-config-api/common/pom.xml +++ b/jans-config-api/common/pom.xml @@ -9,7 +9,7 @@ io.jans jans-config-api-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index 69e873ce172..d46e4c60864 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: + userCanAccess: type: boolean - userCanView: + adminCanAccess: type: boolean - userCanEdit: + whitePagesCanView: type: boolean - userCanAccess: + adminCanView: type: boolean - adminCanAccess: + userCanEdit: type: boolean adminCanEdit: type: boolean - adminCanView: + userCanView: 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 @@ -11767,10 +11767,10 @@ components: ttl: type: integer format: int32 - persisted: - type: boolean opbrowserState: type: string + persisted: + type: boolean SessionIdAccessMap: type: object properties: diff --git a/jans-config-api/plugins/admin-ui-plugin/pom.xml b/jans-config-api/plugins/admin-ui-plugin/pom.xml index c47467970a1..0c68946b8ee 100644 --- a/jans-config-api/plugins/admin-ui-plugin/pom.xml +++ b/jans-config-api/plugins/admin-ui-plugin/pom.xml @@ -3,7 +3,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 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/docs/lock-plugin-swagger.yaml b/jans-config-api/plugins/docs/lock-plugin-swagger.yaml index 2d846528f26..b75b56adade 100644 --- a/jans-config-api/plugins/docs/lock-plugin-swagger.yaml +++ b/jans-config-api/plugins/docs/lock-plugin-swagger.yaml @@ -15,6 +15,7 @@ servers: tags: - name: Lock - Configuration - name: Lock - Audit +- name: Lock - Statistics paths: /lock/audit/health/search: get: @@ -468,6 +469,57 @@ paths: security: - oauth2: - https://jans.io/oauth/lock-config.write + /lock/stat: + get: + tags: + - Statistics + summary: Provides basic statistic + description: Provides basic statistic + operationId: get-lock-stat + parameters: + - name: Authorization + in: header + description: Authorization code + schema: + type: string + - name: month + in: query + description: Month for which the stat report is to be fetched. The parameter + is mandatory if start_month and end_month parameters are not present. + schema: + type: string + - name: start_month + in: query + description: Start-Month for which the stat report is to be fetched + schema: + type: string + - name: end_month + in: query + description: End-Month for which the stat report is to be fetched + schema: + type: string + - name: format + in: query + description: Report format + schema: + type: string + responses: + "200": + description: Stats + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/JsonNode' + "401": + description: Unauthorized + "500": + description: InternalServerError + security: + - oauth2: + - https://jans.io/oauth/lock/read-all + - jans_stat components: schemas: HealthEntry: @@ -597,6 +649,13 @@ components: openIdIssuer: type: string description: OpenID issuer URL + statEnabled: + type: boolean + description: Active stat enabled + statTimerIntervalInSeconds: + type: integer + description: Statistical data capture time interval + format: int32 tokenChannels: type: array description: List of token channel names @@ -689,6 +748,8 @@ components: items: type: string description: List of Zip Uris with policies + errorReasonEnabled: + type: boolean OpaConfiguration: type: object properties: @@ -701,6 +762,8 @@ components: description: Opa Configuration JsonPatch: type: object + JsonNode: + type: object securitySchemes: oauth2: type: oauth2 diff --git a/jans-config-api/plugins/fido2-plugin/pom.xml b/jans-config-api/plugins/fido2-plugin/pom.xml index 08eeea05b3d..d9f362ad066 100644 --- a/jans-config-api/plugins/fido2-plugin/pom.xml +++ b/jans-config-api/plugins/fido2-plugin/pom.xml @@ -3,7 +3,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 fido2-plugin diff --git a/jans-config-api/plugins/jans-link-plugin/pom.xml b/jans-config-api/plugins/jans-link-plugin/pom.xml index 1b65f2ded34..c72d51ec294 100644 --- a/jans-config-api/plugins/jans-link-plugin/pom.xml +++ b/jans-config-api/plugins/jans-link-plugin/pom.xml @@ -3,7 +3,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 jans-link-plugin diff --git a/jans-config-api/plugins/kc-link-plugin/pom.xml b/jans-config-api/plugins/kc-link-plugin/pom.xml index d92ec7aeba1..d4ddc55630a 100644 --- a/jans-config-api/plugins/kc-link-plugin/pom.xml +++ b/jans-config-api/plugins/kc-link-plugin/pom.xml @@ -5,7 +5,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-config-api/plugins/kc-saml-plugin/pom.xml b/jans-config-api/plugins/kc-saml-plugin/pom.xml index 29feff15e1e..d9b89dcf2f7 100644 --- a/jans-config-api/plugins/kc-saml-plugin/pom.xml +++ b/jans-config-api/plugins/kc-saml-plugin/pom.xml @@ -5,7 +5,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-config-api/plugins/lock-plugin/pom.xml b/jans-config-api/plugins/lock-plugin/pom.xml index cace2d322a5..0a9554455bc 100644 --- a/jans-config-api/plugins/lock-plugin/pom.xml +++ b/jans-config-api/plugins/lock-plugin/pom.xml @@ -5,7 +5,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java index 5432433ade7..fec8a36af05 100644 --- a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/ApiApplication.java @@ -18,7 +18,10 @@ license = @License(name = "Apache 2.0", url = "https://github.com/JanssenProject/jans/blob/main/LICENSE")), - tags = { @Tag(name = "Lock - Configuration"), @Tag(name = "Lock - Audit") }, + tags = { @Tag(name = "Lock - Configuration"), + @Tag(name = "Lock - Audit"), + @Tag(name = "Lock - Statistics") + }, servers = { @Server(url = "https://jans.io/", description = "The Jans server") }) diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockStatResource.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockStatResource.java new file mode 100644 index 00000000000..86223909970 --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/rest/LockStatResource.java @@ -0,0 +1,96 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.configapi.plugin.lock.rest; + + + +import com.fasterxml.jackson.databind.JsonNode; + +import static io.jans.as.model.util.Util.escapeLog; +import io.jans.configapi.core.rest.BaseResource; +import io.jans.configapi.core.rest.ProtectedApi; +import io.jans.configapi.plugin.lock.service.LockService; +import io.jans.configapi.plugin.lock.util.Constants; +import io.jans.configapi.service.auth.ConfigurationService; +import io.jans.configapi.util.ApiAccessConstants; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.*; + +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +@Path(Constants.LOCK_STAT) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class LockStatResource extends BaseResource { + + private static final String STAT_URL = "/jans-lock/v1/internal/stat"; + + @Inject + Logger logger; + + @Inject + ConfigurationService configurationService; + + @Inject + LockService lockService; + + @Operation(summary = "Provides basic statistic", description = "Provides basic statistic", operationId = "get-lock-stat", tags = { + "Statistics" }, security = @SecurityRequirement(name = "oauth2", scopes = { Constants.LOCK_READ_ACCESS, + ApiAccessConstants.JANS_STAT })) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stats", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = JsonNode.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "500", description = "InternalServerError") }) + @GET + @ProtectedApi(scopes = { Constants.LOCK_READ_ACCESS, + ApiAccessConstants.JANS_STAT }, groupScopes = {}, superScopes = { + ApiAccessConstants.SUPER_ADMIN_READ_ACCESS }) + @Produces(MediaType.APPLICATION_JSON) + public Response getStatistics( + @Parameter(description = "Authorization code") @HeaderParam("Authorization") String authorization, + @Parameter(description = "Month for which the stat report is to be fetched. The parameter is mandatory if start_month and end_month parameters are not present.") @QueryParam(value = "month") String month, + @Parameter(description = "Start-Month for which the stat report is to be fetched") @QueryParam(value = "start_month") String startMonth, + @Parameter(description = "End-Month for which the stat report is to be fetched") @QueryParam(value = "end_month") String endMonth, + @Parameter(description = "Report format") @QueryParam(value = "format") String format) { + if (StringUtils.isBlank(format)) { + format = ""; + } + JsonNode jsonNode = null; + try { + if (logger.isInfoEnabled()) { + logger.info( + "LockStatResource::getStatistics() - authorization:{}, month:{}, startMonth:{}, endMonth:{}, format:{}", + escapeLog(authorization), escapeLog(month), escapeLog(startMonth), escapeLog(endMonth), escapeLog(format)); + } + String url = getIssuer() + STAT_URL; + jsonNode = this.lockService.getStat(url, authorization, month, startMonth, endMonth, format); + logger.info("StatResource::getUserStatistics() - jsonNode:{} ", jsonNode); + } catch (Exception ex) { + logger.error(" Error while fetching lock stat is", ex); + throwBadRequestException(ex); + } + return Response.ok(jsonNode).build(); + } + + private String getIssuer() { + return configurationService.find().getIssuer(); + } + +} \ No newline at end of file diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/LockService.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/LockService.java new file mode 100644 index 00000000000..3293d795b67 --- /dev/null +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/service/LockService.java @@ -0,0 +1,103 @@ +package io.jans.configapi.plugin.lock.service; + +import io.jans.configapi.core.util.Jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import static io.jans.as.model.util.Util.escapeLog; +import io.jans.configapi.core.service.ConfigHttpService; +import io.jans.model.net.HttpServiceResponse; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response.Status; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; +import org.slf4j.Logger; + +@ApplicationScoped +public class LockService { + private static final String CONTENT_TYPE = "Content-Type"; + private static final String AUTHORIZATION = "Authorization"; + + @Inject + Logger logger; + + @Inject + ConfigHttpService configHttpService; + + public JsonNode getStat(String url, String token, String month, String startMonth, String endMonth, String format) + throws JsonProcessingException { + if (logger.isInfoEnabled()) { + logger.info( + "LockStatResource::getStatistics() - url:{}, token:{}, month:{}, startMonth:{}, endMonth:{}, format:{}", + escapeLog(url), escapeLog(token), escapeLog(month), escapeLog(startMonth), escapeLog(endMonth), escapeLog(format)); + } + JsonNode jsonNode = null; + Map headers = new HashMap<>(); + headers.put(CONTENT_TYPE, MediaType.APPLICATION_JSON); + if (StringUtils.isNotBlank(token)) { + headers.put(AUTHORIZATION, token); + } + + // Query Param + Map data = new HashMap<>(); + data.put("month", month); + data.put("start-month", startMonth); + data.put("end-month", endMonth); + data.put("format", format); + HttpServiceResponse httpServiceResponse = configHttpService.executeGet(url, headers, data); + logger.info(" stat httpServiceResponse:{}", httpServiceResponse); + if (httpServiceResponse != null) { + jsonNode = getResponseJsonNode(httpServiceResponse, Status.OK); + } + logger.info(" stat jsonNode:{}", jsonNode); + return jsonNode; + } + + public String getResponseEntityString(HttpServiceResponse serviceResponse, Status status) { + String jsonString = null; + + if (serviceResponse == null) { + return jsonString; + } + + if (serviceResponse.getHttpResponse() != null && serviceResponse.getHttpResponse().getStatusLine() != null + && serviceResponse.getHttpResponse().getStatusLine().getStatusCode() == status.getStatusCode()) { + HttpEntity entity = serviceResponse.getHttpResponse().getEntity(); + if (entity == null) { + return jsonString; + } + jsonString = entity.toString(); + + } + logger.info(" stat jsonString:{}", jsonString); + return jsonString; + } + + public JsonNode getResponseJsonNode(HttpServiceResponse serviceResponse, Status status) + throws JsonProcessingException { + JsonNode jsonNode = null; + + if (serviceResponse == null) { + return jsonNode; + } + + return getResponseJsonNode(getResponseEntityString(serviceResponse, status)); + } + + public JsonNode getResponseJsonNode(String jsonSring) throws JsonProcessingException { + JsonNode jsonNode = null; + + if (StringUtils.isNotBlank(jsonSring)) { + return jsonNode; + } + + return Jackson.asJsonNode(jsonSring); + } + +} diff --git a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java index 221bd3ece54..c3f50d4e373 100644 --- a/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java +++ b/jans-config-api/plugins/lock-plugin/src/main/java/io/jans/configapi/plugin/lock/util/Constants.java @@ -14,6 +14,7 @@ private Constants() { public static final String LOCK = "/lock"; public static final String LOCK_CONFIG = "/lockConfig"; public static final String AUDIT = "/audit"; + public static final String LOCK_STAT = "/stat"; public static final String HEALTH = "/health"; public static final String LOG = "/log"; public static final String TELEMETRY = "/telemetry"; diff --git a/jans-config-api/plugins/pom.xml b/jans-config-api/plugins/pom.xml index e845759a7a8..b3d5f3fce0c 100644 --- a/jans-config-api/plugins/pom.xml +++ b/jans-config-api/plugins/pom.xml @@ -4,14 +4,14 @@ io.jans jans-config-api-parent - 0.0.0-nightly + 1.3.0 4.0.0 io.jans.jans-config-api.plugins plugins - 0.0.0-nightly + 1.3.0 pom diff --git a/jans-config-api/plugins/sample/demo/pom.xml b/jans-config-api/plugins/sample/demo/pom.xml index e1ad30f926b..344bdb02c91 100644 --- a/jans-config-api/plugins/sample/demo/pom.xml +++ b/jans-config-api/plugins/sample/demo/pom.xml @@ -3,7 +3,7 @@ io.jans.jans-config-api.plugins ${plugin.id} - 0.0.0-nightly + 1.3.0 jar demo diff --git a/jans-config-api/plugins/sample/helloworld/pom.xml b/jans-config-api/plugins/sample/helloworld/pom.xml index ad4ed9ee899..e7c9b0a512a 100644 --- a/jans-config-api/plugins/sample/helloworld/pom.xml +++ b/jans-config-api/plugins/sample/helloworld/pom.xml @@ -3,7 +3,7 @@ com.spl ${plugin.id} - 0.0.0-nightly + 1.3.0 jar helloworld diff --git a/jans-config-api/plugins/scim-plugin/pom.xml b/jans-config-api/plugins/scim-plugin/pom.xml index 7d460c68a29..0150eaaf30a 100644 --- a/jans-config-api/plugins/scim-plugin/pom.xml +++ b/jans-config-api/plugins/scim-plugin/pom.xml @@ -3,7 +3,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 scim-plugin diff --git a/jans-config-api/plugins/user-mgt-plugin/pom.xml b/jans-config-api/plugins/user-mgt-plugin/pom.xml index 27c77ea328b..7aa98b2dd99 100644 --- a/jans-config-api/plugins/user-mgt-plugin/pom.xml +++ b/jans-config-api/plugins/user-mgt-plugin/pom.xml @@ -3,7 +3,7 @@ plugins io.jans.jans-config-api.plugins - 0.0.0-nightly + 1.3.0 4.0.0 user-mgt-plugin 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/pom.xml b/jans-config-api/pom.xml index efb8f25fd64..500c88059d5 100644 --- a/jans-config-api/pom.xml +++ b/jans-config-api/pom.xml @@ -5,7 +5,7 @@ io.jans jans-config-api-parent pom - 0.0.0-nightly + 1.3.0 jans-config-api-parent diff --git a/jans-config-api/server-fips/pom.xml b/jans-config-api/server-fips/pom.xml index cd312af0803..16ffdd144cd 100644 --- a/jans-config-api/server-fips/pom.xml +++ b/jans-config-api/server-fips/pom.xml @@ -9,7 +9,7 @@ io.jans jans-config-api-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-config-api/server/pom.xml b/jans-config-api/server/pom.xml index b0d052e9f61..976035f12aa 100644 --- a/jans-config-api/server/pom.xml +++ b/jans-config-api/server/pom.xml @@ -9,7 +9,7 @@ io.jans jans-config-api-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-config-api/server/src/test/resources/feature/openid/scopes/scopes.feature b/jans-config-api/server/src/test/resources/feature/openid/scopes/scopes.feature index cc93861fea4..4ab40d28a80 100644 --- a/jans-config-api/server/src/test/resources/feature/openid/scopes/scopes.feature +++ b/jans-config-api/server/src/test/resources/feature/openid/scopes/scopes.feature @@ -122,6 +122,7 @@ And print response Scenario: Get an openid connect scopes by inum Given url mainUrl And header Authorization = 'Bearer ' + accessToken +And param pattern = '1800' When method GET Then status 200 And print response 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-config-api/shared/pom.xml b/jans-config-api/shared/pom.xml index 15efc9f1800..53a9d5596de 100644 --- a/jans-config-api/shared/pom.xml +++ b/jans-config-api/shared/pom.xml @@ -7,7 +7,7 @@ io.jans jans-config-api-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-config-api/shared/src/main/java/io/jans/configapi/core/service/ConfigHttpService.java b/jans-config-api/shared/src/main/java/io/jans/configapi/core/service/ConfigHttpService.java new file mode 100644 index 00000000000..ada7abc8609 --- /dev/null +++ b/jans-config-api/shared/src/main/java/io/jans/configapi/core/service/ConfigHttpService.java @@ -0,0 +1,375 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.configapi.core.service; + +import io.jans.model.net.HttpServiceResponse; +import io.jans.util.StringHelper; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.SSLContext; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.ssl.TrustStrategy; +import org.apache.http.util.EntityUtils; + +@ApplicationScoped +public class ConfigHttpService implements Serializable { + + private static final long serialVersionUID = -2398422090669045605L; + protected transient Logger log = LogManager.getLogger(getClass()); + private static final String CON_STATS_STR = "Connection manager stats: {}"; + private transient Base64 base64; + + private transient PoolingHttpClientConnectionManager connectionManager; + + @PostConstruct + public void init() { + connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(200); // Increase max total connection to 200 + connectionManager.setDefaultMaxPerRoute(50); // Increase default max connection per route to 50 + + this.base64 = new Base64(); + } + + public CloseableHttpClient getHttpsClientTrustAll() + throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { + log.trace(CON_STATS_STR, connectionManager.getTotalStats()); + + TrustStrategy acceptingTrustStrategy = (cert, authType) -> true; + SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + SSLConnectionSocketFactory sslConSocFactory = new SSLConnectionSocketFactory(sslContext, + NoopHostnameVerifier.INSTANCE); + + return HttpClients.custom().setSSLSocketFactory(sslConSocFactory) + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(connectionManager).build(); + } + + public CloseableHttpClient getHttpsClient() { + return getHttpsClient(RequestConfig.custom().build()); + } + + public CloseableHttpClient getHttpsClient(RequestConfig requestConfig) { + log.trace(CON_STATS_STR, connectionManager.getTotalStats()); + + return HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.copy(requestConfig).setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(connectionManager).build(); + } + + public CloseableHttpClient getHttpsClient(HttpRoutePlanner routerPlanner) { + log.trace(CON_STATS_STR, connectionManager.getTotalStats()); + + return getHttpsClient(RequestConfig.custom().build(), routerPlanner); + } + + public CloseableHttpClient getHttpsClient(RequestConfig requestConfig, HttpRoutePlanner routerPlanner) { + log.trace(CON_STATS_STR, connectionManager.getTotalStats()); + + return HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.copy(requestConfig).setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(connectionManager).setRoutePlanner(routerPlanner).build(); + } + + public CloseableHttpClient getHttpsClient(String trustStorePath, String trustStorePassword) + throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, CertificateException, + IOException { + log.trace(CON_STATS_STR, connectionManager.getTotalStats()); + + SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(new File(trustStorePath), trustStorePassword.toCharArray()).build(); + SSLConnectionSocketFactory sslConSocFactory = new SSLConnectionSocketFactory(sslContext); + + return HttpClients.custom().setSSLSocketFactory(sslConSocFactory) + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(connectionManager).build(); + } + + public CloseableHttpClient getHttpsClient(String trustStorePath, String trustStorePassword, String keyStorePath, + String keyStorePassword) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, + CertificateException, IOException, UnrecoverableKeyException { + log.trace(CON_STATS_STR, connectionManager.getTotalStats()); + + SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(new File(trustStorePath), trustStorePassword.toCharArray()) + .loadKeyMaterial(new File(keyStorePath), keyStorePassword.toCharArray(), keyStorePassword.toCharArray()) + .build(); + SSLConnectionSocketFactory sslConSocFactory = new SSLConnectionSocketFactory(sslContext); + + return HttpClients.custom().setSSLSocketFactory(sslConSocFactory) + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(connectionManager).build(); + } + + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authCode, + Map headers, String postData, ContentType contentType, String authType) { + + HttpPost httpPost = new HttpPost(uri); + + if (StringHelper.isEmpty(authType)) { + authType = "Basic "; + } else { + authType = authType + " "; + } + if (StringHelper.isNotEmpty(authCode)) { + httpPost.setHeader("Authorization", authType + authCode); + } + + if (contentType == null) { + contentType = ContentType.APPLICATION_JSON; + } + + if (headers != null) { + for (Entry headerEntry : headers.entrySet()) { + httpPost.setHeader(headerEntry.getKey(), headerEntry.getValue()); + } + } + + StringEntity stringEntity = new StringEntity(postData, contentType); + httpPost.setEntity(stringEntity); + + try { + HttpResponse httpResponse = httpClient.execute(httpPost); + + return new HttpServiceResponse(httpPost, httpResponse); + } catch (IOException ex) { + log.error("Failed to execute post request", ex); + } + + return null; + } + + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authCode, + Map headers, String postData) { + return executePost(httpClient, uri, authCode, headers, postData, null, null); + } + + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authCode, String postData, + ContentType contentType) { + return executePost(httpClient, uri, authCode, null, postData, contentType, null); + } + + public HttpServiceResponse executePost(String uri, String authCode, String postData, + ContentType contentType, String authType) { + return executePost(this.getHttpsClient(), uri, authCode, null, postData, contentType, authType); + } + + public String encodeBase64(String value) { + return new String(base64.encode((value).getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); + } + + public String encodeUrl(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + public HttpServiceResponse executeGet(HttpClient httpClient, String requestUri, Map headers, + Map parameters) { + HttpGet httpGet = new HttpGet(requestUri); + + if (headers != null) { + for (Entry headerEntry : headers.entrySet()) { + httpGet.setHeader(headerEntry.getKey(), headerEntry.getValue()); + } + } + + if (parameters != null && !parameters.isEmpty()) { + StringBuilder query = new StringBuilder(""); + for (String key : parameters.keySet()) { + + String value = parameters.get(key); + if (value != null && value.length() > 0) { + + String delim = "&" + URLEncoder.encode(key, StandardCharsets.UTF_8) + "="; + query.append(delim.substring(1)); + query.append(URLEncoder.encode(value, StandardCharsets.UTF_8)); + } + } + httpGet = new HttpGet(requestUri + query.toString()); + } + + try { + HttpResponse httpResponse = httpClient.execute(httpGet); + + return new HttpServiceResponse(httpGet, httpResponse); + } catch (IOException ex) { + log.error("Failed to execute get request", ex); + } + + return null; + } + + public HttpServiceResponse executeGet(String requestUri, Map headers, Map data) { + HttpClient httpClient = this.getHttpsClient(); + return executeGet(httpClient, requestUri, headers, data); + } + + public HttpServiceResponse executeGet(String requestUri, Map headers) { + HttpClient httpClient = this.getHttpsClient(); + return executeGet(httpClient, requestUri, headers, null); + } + + public HttpServiceResponse executeGet(HttpClient httpClient, String requestUri) { + return executeGet(httpClient, requestUri, null, null); + } + + public byte[] getResponseContent(HttpResponse httpResponse) throws IOException { + if ((httpResponse == null) || !isResponseStastusCodeOk(httpResponse)) { + return null; + } + + HttpEntity entity = httpResponse.getEntity(); + byte[] responseBytes = new byte[0]; + if (entity != null) { + responseBytes = EntityUtils.toByteArray(entity); + } + + // Consume response content + if (entity != null) { + EntityUtils.consume(entity); + } + + return responseBytes; + } + + public void consume(HttpResponse httpResponse) throws IOException { + if ((httpResponse == null) || !isResponseStastusCodeOk(httpResponse)) { + return; + } + + // Consume response content + HttpEntity entity = httpResponse.getEntity(); + if (entity != null) { + EntityUtils.consume(entity); + } + } + + public String convertEntityToString(byte[] responseBytes) { + if (responseBytes == null) { + return null; + } + + return new String(responseBytes); + } + + public String convertEntityToString(byte[] responseBytes, Charset charset) { + if (responseBytes == null) { + return null; + } + + return new String(responseBytes, charset); + } + + public String convertEntityToString(byte[] responseBytes, String charsetName) throws UnsupportedEncodingException { + if (responseBytes == null) { + return null; + } + + return new String(responseBytes, charsetName); + } + + public boolean isResponseStastusCodeOk(HttpResponse httpResponse) { + int responseStastusCode = httpResponse.getStatusLine().getStatusCode(); + if ((responseStastusCode == HttpStatus.SC_OK) || (responseStastusCode == HttpStatus.SC_CREATED) + || (responseStastusCode == HttpStatus.SC_ACCEPTED) + || (responseStastusCode == HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION) + || (responseStastusCode == HttpStatus.SC_NO_CONTENT) + || (responseStastusCode == HttpStatus.SC_RESET_CONTENT) + || (responseStastusCode == HttpStatus.SC_PARTIAL_CONTENT) + || (responseStastusCode == HttpStatus.SC_MULTI_STATUS)) { + return true; + } else { + return false; + } + } + + public boolean isResponseStatusCodeOk(HttpResponse httpResponse) { + return isResponseStastusCodeOk(httpResponse); + } + + public boolean isContentTypeXml(HttpResponse httpResponse) { + Header contentType = httpResponse.getEntity().getContentType(); + if (contentType == null) { + return false; + } + + String contentTypeValue = contentType.getValue(); + if (StringHelper.equals(contentTypeValue, ContentType.APPLICATION_XML.getMimeType()) + || StringHelper.equals(contentTypeValue, ContentType.TEXT_XML.getMimeType())) { + return true; + } else { + return false; + } + } + + public String constructServerUrl(final HttpServletRequest request) { + int serverPort = request.getServerPort(); + + String redirectUrl; + if ((serverPort == 80) || (serverPort == 443)) { + redirectUrl = String.format("%s://%s%s", request.getScheme(), request.getServerName(), + request.getContextPath()); + } else { + redirectUrl = String.format("%s://%s:%s%s", request.getScheme(), request.getServerName(), + request.getServerPort(), request.getContextPath()); + } + + return redirectUrl.toLowerCase(); + } + + public HttpRoutePlanner buildDefaultRoutePlanner(final String hostname, final int port, final String scheme) { + // Creating an HttpHost object for proxy + HttpHost proxyHost = new HttpHost(hostname, port, scheme); + + return new DefaultProxyRoutePlanner(proxyHost); + } + + public HttpRoutePlanner buildDefaultRoutePlanner(final String proxy) { + return buildDefaultRoutePlanner(proxy, -1, null); + } + +} diff --git a/jans-core/cache/pom.xml b/jans-core/cache/pom.xml index 00e847e0563..a3d2d9d4a87 100644 --- a/jans-core/cache/pom.xml +++ b/jans-core/cache/pom.xml @@ -7,7 +7,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/cdi/pom.xml b/jans-core/cdi/pom.xml index ac52625aca5..e848a96da97 100644 --- a/jans-core/cdi/pom.xml +++ b/jans-core/cdi/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/demo-cdi/pom.xml b/jans-core/demo-cdi/pom.xml index d0b66175ad2..b4a60ae0b83 100644 --- a/jans-core/demo-cdi/pom.xml +++ b/jans-core/demo-cdi/pom.xml @@ -7,7 +7,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/doc/pom.xml b/jans-core/doc/pom.xml index 5745756dfab..46a1fe76f73 100644 --- a/jans-core/doc/pom.xml +++ b/jans-core/doc/pom.xml @@ -3,7 +3,7 @@ jans-core-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-core/document-store/pom.xml b/jans-core/document-store/pom.xml index 56f84ac019d..6669c7040f4 100644 --- a/jans-core/document-store/pom.xml +++ b/jans-core/document-store/pom.xml @@ -7,7 +7,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/exception-extension-cdi/pom.xml b/jans-core/exception-extension-cdi/pom.xml index f987522a154..0b24d7153ad 100644 --- a/jans-core/exception-extension-cdi/pom.xml +++ b/jans-core/exception-extension-cdi/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/java-ext/pom.xml b/jans-core/java-ext/pom.xml index 0615bd52db7..7d30d4c2710 100644 --- a/jans-core/java-ext/pom.xml +++ b/jans-core/java-ext/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/jsf-util/pom.xml b/jans-core/jsf-util/pom.xml index a078aea54db..d8ee7f92b36 100644 --- a/jans-core/jsf-util/pom.xml +++ b/jans-core/jsf-util/pom.xml @@ -9,7 +9,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/message/pom.xml b/jans-core/message/pom.xml index 520aff88ac4..7ba372a05eb 100644 --- a/jans-core/message/pom.xml +++ b/jans-core/message/pom.xml @@ -7,7 +7,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/model/pom.xml b/jans-core/model/pom.xml index 8674b055597..9e80fc69049 100644 --- a/jans-core/model/pom.xml +++ b/jans-core/model/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/notify-client/pom.xml b/jans-core/notify-client/pom.xml index c1e90ac5a92..a9982f95fba 100644 --- a/jans-core/notify-client/pom.xml +++ b/jans-core/notify-client/pom.xml @@ -9,7 +9,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/pom.xml b/jans-core/pom.xml index de11e79b5fe..70f8ed86cc9 100644 --- a/jans-core/pom.xml +++ b/jans-core/pom.xml @@ -3,7 +3,7 @@ io.jans jans-core-parent pom - 0.0.0-nightly + 1.3.0 jans-core diff --git a/jans-core/radius/pom.xml b/jans-core/radius/pom.xml index 86716d3420a..33e44fbd978 100644 --- a/jans-core/radius/pom.xml +++ b/jans-core/radius/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/saml/pom.xml b/jans-core/saml/pom.xml index 2f44cf5fa78..76a95fdc567 100644 --- a/jans-core/saml/pom.xml +++ b/jans-core/saml/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/saml/src/pom.xml b/jans-core/saml/src/pom.xml index 2b51deeb5fd..b049f86cdaf 100644 --- a/jans-core/saml/src/pom.xml +++ b/jans-core/saml/src/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/script/pom.xml b/jans-core/script/pom.xml index 30ab009c0df..618ecf67d8c 100644 --- a/jans-core/script/pom.xml +++ b/jans-core/script/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 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-core/security-extension-cdi/pom.xml b/jans-core/security-extension-cdi/pom.xml index 367eb5ca661..07c9b53128d 100644 --- a/jans-core/security-extension-cdi/pom.xml +++ b/jans-core/security-extension-cdi/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/server/pom.xml b/jans-core/server/pom.xml index 5f5d0d12bdd..683066f5c93 100644 --- a/jans-core/server/pom.xml +++ b/jans-core/server/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/service/pom.xml b/jans-core/service/pom.xml index 2e72b28bbdb..2e5ca187fa8 100644 --- a/jans-core/service/pom.xml +++ b/jans-core/service/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/standalone/pom.xml b/jans-core/standalone/pom.xml index 403797d90b4..f409a67e3e2 100644 --- a/jans-core/standalone/pom.xml +++ b/jans-core/standalone/pom.xml @@ -7,7 +7,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/timer-weld/pom.xml b/jans-core/timer-weld/pom.xml index d336feccceb..ff5e157b75a 100644 --- a/jans-core/timer-weld/pom.xml +++ b/jans-core/timer-weld/pom.xml @@ -7,7 +7,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-core/uma-rs-core/pom.xml b/jans-core/uma-rs-core/pom.xml index 041954a9e64..2596fa9c058 100644 --- a/jans-core/uma-rs-core/pom.xml +++ b/jans-core/uma-rs-core/pom.xml @@ -9,7 +9,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 true diff --git a/jans-core/util/pom.xml b/jans-core/util/pom.xml index 64acdedd18f..931cc00f360 100644 --- a/jans-core/util/pom.xml +++ b/jans-core/util/pom.xml @@ -8,7 +8,7 @@ io.jans jans-core-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-fido2/client/pom.xml b/jans-fido2/client/pom.xml index 87fa2fe23c4..125332152cb 100644 --- a/jans-fido2/client/pom.xml +++ b/jans-fido2/client/pom.xml @@ -9,7 +9,7 @@ io.jans jans-fido2-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-fido2/model/pom.xml b/jans-fido2/model/pom.xml index 28744e700a1..5678828c3e0 100644 --- a/jans-fido2/model/pom.xml +++ b/jans-fido2/model/pom.xml @@ -10,7 +10,7 @@ io.jans jans-fido2-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-fido2/pom.xml b/jans-fido2/pom.xml index d67fdad2bdc..5073833ce4b 100644 --- a/jans-fido2/pom.xml +++ b/jans-fido2/pom.xml @@ -5,7 +5,7 @@ io.jans jans-fido2-parent pom - 0.0.0-nightly + 1.3.0 Fido2 API diff --git a/jans-fido2/server-fips/pom.xml b/jans-fido2/server-fips/pom.xml index 94b80f112c5..3a374743b25 100644 --- a/jans-fido2/server-fips/pom.xml +++ b/jans-fido2/server-fips/pom.xml @@ -9,7 +9,7 @@ io.jans jans-fido2-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-fido2/server/pom.xml b/jans-fido2/server/pom.xml index 2c3398985f5..7c13926146a 100644 --- a/jans-fido2/server/pom.xml +++ b/jans-fido2/server/pom.xml @@ -9,7 +9,7 @@ io.jans jans-fido2-parent - 0.0.0-nightly + 1.3.0 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-integration/job-scheduler/pom.xml b/jans-keycloak-integration/job-scheduler/pom.xml index 9735209945b..ef73e6c5663 100644 --- a/jans-keycloak-integration/job-scheduler/pom.xml +++ b/jans-keycloak-integration/job-scheduler/pom.xml @@ -17,7 +17,7 @@ io.jans jans-kc-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index 56ea4beef87..d7c1b2c18e3 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -4,7 +4,7 @@ io.jans jans-kc-parent pom - 0.0.0-nightly + 1.3.0 keycloak-integration-parent diff --git a/jans-keycloak-integration/spi/pom.xml b/jans-keycloak-integration/spi/pom.xml index 65ce4066eb7..0e7150caa70 100644 --- a/jans-keycloak-integration/spi/pom.xml +++ b/jans-keycloak-integration/spi/pom.xml @@ -9,7 +9,7 @@ io.jans jans-kc-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-keycloak-link/model/pom.xml b/jans-keycloak-link/model/pom.xml index b0e6c6ae469..13b586b2ad0 100644 --- a/jans-keycloak-link/model/pom.xml +++ b/jans-keycloak-link/model/pom.xml @@ -2,7 +2,7 @@ jans-keycloak-link-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 jans-keycloak-link-model diff --git a/jans-keycloak-link/model/src/main/resources/archetype-resources/pom.xml b/jans-keycloak-link/model/src/main/resources/archetype-resources/pom.xml index fed7c7d1183..3d1dfe16882 100644 --- a/jans-keycloak-link/model/src/main/resources/archetype-resources/pom.xml +++ b/jans-keycloak-link/model/src/main/resources/archetype-resources/pom.xml @@ -2,7 +2,7 @@ 4.0.0 $io.jans $model - 0.0.0-nightly + 1.3.0 junit diff --git a/jans-keycloak-link/pom.xml b/jans-keycloak-link/pom.xml index 108b7cc578b..18738872388 100644 --- a/jans-keycloak-link/pom.xml +++ b/jans-keycloak-link/pom.xml @@ -2,7 +2,7 @@ 4.0.0 io.jans jans-keycloak-link-parent - 0.0.0-nightly + 1.3.0 model service diff --git a/jans-keycloak-link/server/pom.xml b/jans-keycloak-link/server/pom.xml index 75a754de219..98b1a17c2e1 100644 --- a/jans-keycloak-link/server/pom.xml +++ b/jans-keycloak-link/server/pom.xml @@ -2,7 +2,7 @@ jans-keycloak-link-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 jans-keycloak-link-server @@ -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 jans-keycloak-link-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-link/model/pom.xml b/jans-link/model/pom.xml index a979f6df6bf..f2d4f8389c8 100644 --- a/jans-link/model/pom.xml +++ b/jans-link/model/pom.xml @@ -2,7 +2,7 @@ jans-link-server-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 jans-link-model diff --git a/jans-link/pom.xml b/jans-link/pom.xml index 954fca152c8..d032b7c064a 100644 --- a/jans-link/pom.xml +++ b/jans-link/pom.xml @@ -2,12 +2,12 @@ 4.0.0 io.jans jans-link-server-parent - 0.0.0-nightly + 1.3.0 io.jans jans-bom - 0.0.0-nightly + 1.3.0 diff --git a/jans-link/server-fips/pom.xml b/jans-link/server-fips/pom.xml index c1b7e2261af..03197a38db5 100644 --- a/jans-link/server-fips/pom.xml +++ b/jans-link/server-fips/pom.xml @@ -17,7 +17,7 @@ jans-link-server-parent io.jans - 0.0.0-nightly + 1.3.0 diff --git a/jans-link/server/pom.xml b/jans-link/server/pom.xml index 3f223c23209..e5723b1c5c1 100644 --- a/jans-link/server/pom.xml +++ b/jans-link/server/pom.xml @@ -2,7 +2,7 @@ jans-link-server-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 jans-link-server diff --git a/jans-link/service/pom.xml b/jans-link/service/pom.xml index f55a8f80fc4..6e7d17262a4 100644 --- a/jans-link/service/pom.xml +++ b/jans-link/service/pom.xml @@ -2,7 +2,7 @@ jans-link-server-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 jans-link-service diff --git a/jans-linux-setup/jans_setup/app_info.json b/jans-linux-setup/jans_setup/app_info.json index caf5ed20004..0fdc4286520 100644 --- a/jans-linux-setup/jans_setup/app_info.json +++ b/jans-linux-setup/jans_setup/app_info.json @@ -1,6 +1,6 @@ { - "JANS_APP_VERSION": "0.0.0", - "JANS_BUILD": "-nightly", + "JANS_APP_VERSION": "1.3.0", + "JANS_BUILD": "", "JETTY_VERSION": "11.0.15", "AMAZON_CORRETTO_VERSION": "17", "JYTHON_VERSION": "2.7.3", diff --git a/jans-linux-setup/jans_setup/setup_app/test_data_loader.py b/jans-linux-setup/jans_setup/setup_app/test_data_loader.py index a44ebdb7a55..896bd80eed2 100644 --- a/jans-linux-setup/jans_setup/setup_app/test_data_loader.py +++ b/jans-linux-setup/jans_setup/setup_app/test_data_loader.py @@ -273,11 +273,12 @@ def load_test_data(self): 'forceIdTokenHintPresence': False, 'introspectionScriptBackwardCompatibility': False, 'allowSpontaneousScopes': True, + 'accessEvaluationAllowBasicClientAuthorization': True, 'spontaneousScopeLifetime': 0, 'tokenEndpointAuthMethodsSupported': [ 'client_secret_basic', 'client_secret_post', 'client_secret_jwt', 'private_key_jwt', 'tls_client_auth', 'self_signed_tls_client_auth', 'none' ], 'sessionIdRequestParameterEnabled': True, 'skipRefreshTokenDuringRefreshing': False, - 'featureFlags': ['unknown', 'health_check', 'userinfo', 'clientinfo', 'id_generation', 'registration', 'introspection', 'revoke_token', 'revoke_session', 'global_token_revocation', 'end_session', 'status_session', 'jans_configuration', 'ciba', 'uma', 'u2f', 'device_authz', 'stat', 'par', 'ssa', 'status_list'], + 'featureFlags': ['unknown', 'health_check', 'userinfo', 'clientinfo', 'id_generation', 'registration', 'introspection', 'revoke_token', 'revoke_session', 'global_token_revocation', 'end_session', 'status_session', 'jans_configuration', 'ciba', 'uma', 'u2f', 'device_authz', 'stat', 'par', 'ssa', 'status_list', 'access_evaluation'], 'cleanServiceInterval':7200, 'loggingLevel': 'TRACE', } diff --git a/jans-linux-setup/jans_setup/setup_app/version.py b/jans-linux-setup/jans_setup/setup_app/version.py index 6c8e6b979c5..67bc602abf0 100644 --- a/jans-linux-setup/jans_setup/setup_app/version.py +++ b/jans-linux-setup/jans_setup/setup_app/version.py @@ -1 +1 @@ -__version__ = "0.0.0" +__version__ = "1.3.0" diff --git a/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py b/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py index 40576be19b1..f0a48a0a49d 100644 --- a/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py +++ b/jans-linux-setup/jans_setup/static/extension/consent_gathering/ConsentGatheringSample.py @@ -30,7 +30,7 @@ def destroy(self, configurationAttributes): return True def getApiVersion(self): - return 1 + return 11 # Main consent-gather method. Must return True (if gathering performed successfully) or False (if fail). # All user entered values can be access via Map context.getPageAttributes() diff --git a/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json b/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json index ecfd1981a09..b79e5cccf5e 100644 --- a/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json +++ b/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json @@ -73,13 +73,13 @@ "snapshotFolder": "%(snapshots_dir)s", "snapshotMaxCount": 10, "keycloakConfiguration": { - "serverUrl": "http://localhost:8180", - "realm": "master", - "clientId": "clientserviceaccount", - "clientSecret": "cbyFHt3MMU2vNluAmzXopl9SHx9CUmfC", - "grantType": "client_credentials", - "username": "admin", - "password": "keycloak" + "serverUrl": "", + "realm": "", + "clientId": "", + "clientSecret": "", + "grantType": "", + "username": "", + "password": "" }, "baseDN": null, "personObjectClassTypes": null, diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index 5a4defd2202..a3c926ae360 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -564,6 +564,21 @@ jansRevision: 1 jansScr::%(authorization_challenge_authorizationchallenge)s jansScrTyp: authorization_challenge +dn: inum=BADA-B000,ou=scripts,o=jans +objectClass: jansCustomScr +objectClass: top +description: Agama Authorization Challenge Script +displayName: agama_challenge +inum: BADA-B000 +jansEnabled: true +jansLevel: 1 +jansModuleProperty: {"value1":"location_type","value2":"db","description":""} +jansConfProperty: {"value1":"finish_userid_db_attribute","value2":"uid","description":""} +jansProgLng: java +jansRevision: 1 +jansScrTyp: authorization_challenge +jansScr::%(authorization_challenge_agamachallenge)s + dn: inum=0300-BB00,ou=scripts,o=jans objectClass: jansCustomScr objectClass: top diff --git a/jans-lock/lock-server/client/pom.xml b/jans-lock/lock-server/client/pom.xml index 6fa0e1f7a48..db5a8a31ebb 100644 --- a/jans-lock/lock-server/client/pom.xml +++ b/jans-lock/lock-server/client/pom.xml @@ -11,7 +11,7 @@ jans-lock-server-parent io.jans - 0.0.0-nightly + 1.3.0 diff --git a/jans-lock/lock-server/model/pom.xml b/jans-lock/lock-server/model/pom.xml index a0044861c1c..0c9ade44bd0 100644 --- a/jans-lock/lock-server/model/pom.xml +++ b/jans-lock/lock-server/model/pom.xml @@ -10,7 +10,7 @@ jans-lock-server-parent io.jans - 0.0.0-nightly + 1.3.0 diff --git a/jans-lock/lock-server/pom.xml b/jans-lock/lock-server/pom.xml index 5e926cea610..52d831900c3 100644 --- a/jans-lock/lock-server/pom.xml +++ b/jans-lock/lock-server/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jans jans-lock-server-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-lock/lock-server/server-fips/pom.xml b/jans-lock/lock-server/server-fips/pom.xml index 50b480d1f66..3b0fcdecfb5 100644 --- a/jans-lock/lock-server/server-fips/pom.xml +++ b/jans-lock/lock-server/server-fips/pom.xml @@ -9,7 +9,7 @@ jans-lock-server-parent io.jans - 0.0.0-nightly + 1.3.0 diff --git a/jans-lock/lock-server/server/pom.xml b/jans-lock/lock-server/server/pom.xml index 2782ecd2200..b371b0afb2a 100644 --- a/jans-lock/lock-server/server/pom.xml +++ b/jans-lock/lock-server/server/pom.xml @@ -6,7 +6,7 @@ jans-lock-server-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-lock/lock-server/service/pom.xml b/jans-lock/lock-server/service/pom.xml index 5a35cea34df..66e333ec584 100644 --- a/jans-lock/lock-server/service/pom.xml +++ b/jans-lock/lock-server/service/pom.xml @@ -5,7 +5,7 @@ jans-lock-server-parent io.jans - 0.0.0-nightly + 1.3.0 4.0.0 diff --git a/jans-orm/annotation/pom.xml b/jans-orm/annotation/pom.xml index d90d742c1d2..92ebcc9485e 100644 --- a/jans-orm/annotation/pom.xml +++ b/jans-orm/annotation/pom.xml @@ -8,6 +8,6 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 \ No newline at end of file diff --git a/jans-orm/cdi/pom.xml b/jans-orm/cdi/pom.xml index 73b4126f83b..687fb577a36 100644 --- a/jans-orm/cdi/pom.xml +++ b/jans-orm/cdi/pom.xml @@ -7,7 +7,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/core/pom.xml b/jans-orm/core/pom.xml index a91d65c4c9b..84555866aa6 100644 --- a/jans-orm/core/pom.xml +++ b/jans-orm/core/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/couchbase-libs/pom.xml b/jans-orm/couchbase-libs/pom.xml index 88361ac0f35..22242f90157 100644 --- a/jans-orm/couchbase-libs/pom.xml +++ b/jans-orm/couchbase-libs/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/couchbase-sample/pom.xml b/jans-orm/couchbase-sample/pom.xml index 427414ba04f..1fbe8c17ef3 100644 --- a/jans-orm/couchbase-sample/pom.xml +++ b/jans-orm/couchbase-sample/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/couchbase/pom.xml b/jans-orm/couchbase/pom.xml index 17001811d60..b9b82800959 100644 --- a/jans-orm/couchbase/pom.xml +++ b/jans-orm/couchbase/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/filter/pom.xml b/jans-orm/filter/pom.xml index de2d5a01a2e..9de5ec8023e 100644 --- a/jans-orm/filter/pom.xml +++ b/jans-orm/filter/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/hybrid/pom.xml b/jans-orm/hybrid/pom.xml index 7cf9d17cde0..7b06f729e1a 100644 --- a/jans-orm/hybrid/pom.xml +++ b/jans-orm/hybrid/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/ldap-sample/pom.xml b/jans-orm/ldap-sample/pom.xml index 7716be8e0a4..446cfaa71db 100644 --- a/jans-orm/ldap-sample/pom.xml +++ b/jans-orm/ldap-sample/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/ldap/pom.xml b/jans-orm/ldap/pom.xml index 46e74184aa8..f92ac7269a2 100644 --- a/jans-orm/ldap/pom.xml +++ b/jans-orm/ldap/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/model/pom.xml b/jans-orm/model/pom.xml index 9c703401d13..fbe817933c2 100644 --- a/jans-orm/model/pom.xml +++ b/jans-orm/model/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/model/src/main/java/io/jans/orm/model/base/CustomObjectAttribute.java b/jans-orm/model/src/main/java/io/jans/orm/model/base/CustomObjectAttribute.java index c12ad7e64cb..a95d12eacab 100644 --- a/jans-orm/model/src/main/java/io/jans/orm/model/base/CustomObjectAttribute.java +++ b/jans-orm/model/src/main/java/io/jans/orm/model/base/CustomObjectAttribute.java @@ -8,6 +8,7 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -55,7 +56,11 @@ public Object getValue() { public void setValue(Object value) { this.values = new ArrayList<>(); - this.values.add(value); + if (value instanceof List) { + this.values.addAll((Collection) value); + } else { + this.values.add(value); + } this.multiValued = false; } diff --git a/jans-orm/pom.xml b/jans-orm/pom.xml index 4d5ef7027d6..c06b4c64e56 100644 --- a/jans-orm/pom.xml +++ b/jans-orm/pom.xml @@ -3,7 +3,7 @@ io.jans jans-orm-parent pom - 0.0.0-nightly + 1.3.0 orm diff --git a/jans-orm/spanner-libs/pom.xml b/jans-orm/spanner-libs/pom.xml index 141da99833e..631b4002e19 100644 --- a/jans-orm/spanner-libs/pom.xml +++ b/jans-orm/spanner-libs/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/spanner-sample/pom.xml b/jans-orm/spanner-sample/pom.xml index 0b2c33b2f02..1792854bfe4 100644 --- a/jans-orm/spanner-sample/pom.xml +++ b/jans-orm/spanner-sample/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/spanner/pom.xml b/jans-orm/spanner/pom.xml index 76dc2b7b74a..5e4575a3ac7 100644 --- a/jans-orm/spanner/pom.xml +++ b/jans-orm/spanner/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/sql-sample/pom.xml b/jans-orm/sql-sample/pom.xml index 9ef118bb0d4..454c2c93682 100644 --- a/jans-orm/sql-sample/pom.xml +++ b/jans-orm/sql-sample/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/sql/pom.xml b/jans-orm/sql/pom.xml index b40e48cc865..0c9cbe76f17 100644 --- a/jans-orm/sql/pom.xml +++ b/jans-orm/sql/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/standalone/pom.xml b/jans-orm/standalone/pom.xml index ba2eafc2e3e..e71aeb88f4d 100644 --- a/jans-orm/standalone/pom.xml +++ b/jans-orm/standalone/pom.xml @@ -7,7 +7,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/util/pom.xml b/jans-orm/util/pom.xml index 841cce5e1c1..af9bf8edb51 100644 --- a/jans-orm/util/pom.xml +++ b/jans-orm/util/pom.xml @@ -8,7 +8,7 @@ io.jans jans-orm-parent - 0.0.0-nightly + 1.3.0 diff --git a/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java b/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java index 0a61b86aa10..5f0cf479081 100644 --- a/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java +++ b/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java @@ -6,6 +6,7 @@ package io.jans.orm.util.properties; +import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -77,9 +78,9 @@ protected void loadProperties() { this.loaded = true; } catch (ConfigurationException ex) { - LOG.error(String.format("Failed to load '%s' configuration file from config folder", this.fileName)); + LOG.error(String.format("Failed to load '%s' configuration file from config folder. Current folder: '%s'", this.fileName, (new File(".").getAbsolutePath()))); } catch (Exception e) { - LOG.error(String.format("Failed to load '%s' configuration file from config folder", this.fileName)); + LOG.error(String.format("Failed to load '%s' configuration file from config folder. Current folder: '%s'", this.fileName, (new File(".").getAbsolutePath()))); LOG.error(e.getMessage(), e); } } diff --git a/jans-pycloudlib/jans/pycloudlib/config/file_config.py b/jans-pycloudlib/jans/pycloudlib/config/file_config.py index 9d9603eda40..3869fab4fad 100644 --- a/jans-pycloudlib/jans/pycloudlib/config/file_config.py +++ b/jans-pycloudlib/jans/pycloudlib/config/file_config.py @@ -12,8 +12,9 @@ class FileConfig(BaseConfig): 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_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/jans/pycloudlib/version.py b/jans-pycloudlib/jans/pycloudlib/version.py index 231faa30226..bd559f84965 100644 --- a/jans-pycloudlib/jans/pycloudlib/version.py +++ b/jans-pycloudlib/jans/pycloudlib/version.py @@ -1,3 +1,3 @@ """This module contains version-related info.""" -__version__ = "0.0.0" # pragma: no cover +__version__ = "1.3.0" # pragma: no cover 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/jans-scim/client/pom.xml b/jans-scim/client/pom.xml index 08ba0c5d9d1..c3074e521a6 100644 --- a/jans-scim/client/pom.xml +++ b/jans-scim/client/pom.xml @@ -10,7 +10,7 @@ io.jans jans-scim - 0.0.0-nightly + 1.3.0 diff --git a/jans-scim/model/pom.xml b/jans-scim/model/pom.xml index cb5678ca729..00beea3d104 100644 --- a/jans-scim/model/pom.xml +++ b/jans-scim/model/pom.xml @@ -10,7 +10,7 @@ io.jans jans-scim - 0.0.0-nightly + 1.3.0 diff --git a/jans-scim/pom.xml b/jans-scim/pom.xml index ccbaf0506bb..7d86259320e 100644 --- a/jans-scim/pom.xml +++ b/jans-scim/pom.xml @@ -5,7 +5,7 @@ io.jans jans-scim pom - 0.0.0-nightly + 1.3.0 SCIM API http://www.gluu.org diff --git a/jans-scim/server-fips/pom.xml b/jans-scim/server-fips/pom.xml index f38c8cd4c7e..7f855f637fb 100644 --- a/jans-scim/server-fips/pom.xml +++ b/jans-scim/server-fips/pom.xml @@ -9,7 +9,7 @@ io.jans jans-scim - 0.0.0-nightly + 1.3.0 diff --git a/jans-scim/server/pom.xml b/jans-scim/server/pom.xml index 6bed3919ff3..b4a761ebd1f 100644 --- a/jans-scim/server/pom.xml +++ b/jans-scim/server/pom.xml @@ -10,7 +10,7 @@ io.jans jans-scim - 0.0.0-nightly + 1.3.0 diff --git a/jans-scim/service/pom.xml b/jans-scim/service/pom.xml index 86e289a1747..243e423d0df 100644 --- a/jans-scim/service/pom.xml +++ b/jans-scim/service/pom.xml @@ -8,7 +8,7 @@ io.jans jans-scim - 0.0.0-nightly + 1.3.0 diff --git a/mkdocs.yml b/mkdocs.yml index 05159ef1d84..1f91f74db38 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -109,6 +109,7 @@ nav: - Restarting Services: janssen-server/vm-ops/restarting-services.md - Managing Key Rotation: janssen-server/vm-ops/managing-key-rotation.md - Certificates: janssen-server/vm-ops/certificates.md + - Jans Command: janssen-server/vm-ops/jans-command.md - Terraform and IaC: - janssen-server/terraform/README.md - Configuration Guide: @@ -281,6 +282,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 @@ -410,10 +412,10 @@ nav: - JWT: 'cedarling/cedarling-jwt.md' - Logs: 'cedarling/cedarling-logs.md' - Properties: 'cedarling/cedarling-properties.md' + - Sidecar: 'cedarling/cedarling-sidecar.md' - Python: - cedarling/python/README.md - How to use: cedarling/python/usage.md - - Sidecar: cedarling/python/sidecar.md - WASM: 'cedarling/cedarling-wasm.md' - iOS: 'cedarling/cedarling-ios.md' - Android: 'cedarling/cedarling-android.md'