diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/Support_question.md index 3a2a5d3..dab7d18 100644 --- a/.github/ISSUE_TEMPLATE/Support_question.md +++ b/.github/ISSUE_TEMPLATE/Support_question.md @@ -9,9 +9,9 @@ We primarily use GitHub as an issue tracker. If however you're encountering an i --- -Please make sure you have read our [main Readme](https://github.com/numerique-gouv/oidc2fer). +Please make sure you have read our [main Readme](https://github.com/proconnect-gouv/oidc2fer). -Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/oidc2fer/issues). +Also make sure it was not already answered in [an open or close issue](https://github.com/proconnect-gouv/oidc2fer/issues). If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 0000000..7b45922 --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,67 @@ +name: 🐳 Build docker images + +on: + push: + branches: + - 'main' + tags: + - 'v*' + pull_request: + branches: + - 'main' + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=sha,prefix=sha- + # set latest tag for default branch + type=raw,value=latest,enable={{is_default_branch}} + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + notify-argocd: + runs-on: ubuntu-latest + if: | + github.event_name != 'pull_request' + needs: + - build-and-push + steps: + - uses: numerique-gouv/action-argocd-webhook-notification@main + id: notify + with: + deployment_repo_path: "${{ github.repository }}" + argocd_webhook_secret: "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" + argocd_url: "${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}" diff --git a/.github/workflows/oidc2fer.yml b/.github/workflows/ci-tests.yml similarity index 68% rename from .github/workflows/oidc2fer.yml rename to .github/workflows/ci-tests.yml index 65a4d1a..ee35419 100644 --- a/.github/workflows/oidc2fer.yml +++ b/.github/workflows/ci-tests.yml @@ -1,4 +1,4 @@ -name: OIDC2FER Workflow +name: 💚 CI Tests on: push: @@ -23,7 +23,7 @@ jobs: run: git log - name: Enforce absence of print statements in code run: | - ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/oidc2fer.yml' | grep "print(" + ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/**' | grep "print(" - name: Check absence of fixup commits run: | ! git log | grep 'fixup!' @@ -43,7 +43,7 @@ jobs: with: fetch-depth: 0 - name: Check that the CHANGELOG has been modified in the current branch - run: git whatchanged --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG + run: git log --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG lint-changelog: runs-on: ubuntu-latest @@ -101,3 +101,37 @@ jobs: pip install --user .[dev] - name: Run tests run: ~/.local/bin/pytest + + helmfile-lint: + runs-on: ubuntu-latest + container: + image: ghcr.io/helmfile/helmfile:v0.171.0 + steps: + - + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + owner: numerique-gouv + repositories: secrets + - + name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ steps.app-token.outputs.token }} + - + name: Helmfile lint + shell: bash + run: | + mkdir -p ~/.config/sops/age/ + echo ${{ secrets.SOPS_PRIVATE }} > ~/.config/sops/age/keys.txt + set -e + HELMFILE=src/helm/helmfile.yaml + environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=0}' "$HELMFILE" | grep -E '^[[:space:]]{2}[a-zA-Z]+' | sed 's/^[[:space:]]*//;s/:.*//') + for env in $environments; do + echo "################### $env lint ###################" + helmfile -e $env -f src/helm/helmfile.yaml lint || exit 1 + echo -e "\n" + done diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml deleted file mode 100644 index d77dd74..0000000 --- a/.github/workflows/docker-hub.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Docker Hub Workflow - -on: - workflow_dispatch: - push: - branches: - - 'main' - tags: - - 'v*' - pull_request: - branches: - - 'main' - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: "oidc2fer,secrets" - - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - token: ${{ steps.app-token.outputs.token }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: lasuite/oidc2fer - - - name: Load sops secrets - uses: rouja/actions-sops@main - with: - secret-file: secrets/numerique-gouv/oidc2fer/secrets.enc.env - age-key: ${{ secrets.SOPS_PRIVATE }} - - - name: Login to DockerHub - run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - target: production - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - notify-argocd: - runs-on: ubuntu-latest - if: | - github.event_name != 'pull_request' - needs: - - build-and-push - steps: - - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: "oidc2fer,secrets" - - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - token: ${{ steps.app-token.outputs.token }} - - - name: Load sops secrets - uses: rouja/actions-sops@main - with: - secret-file: secrets/numerique-gouv/oidc2fer/secrets.enc.env - age-key: ${{ secrets.SOPS_PRIVATE }} - - - name: Call argocd github webhook - run: | - data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}' - sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}') - curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL diff --git a/.github/workflows/helmfile-linter.yml b/.github/workflows/helmfile-linter.yml deleted file mode 100644 index 5e37fa3..0000000 --- a/.github/workflows/helmfile-linter.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Helmfile lint -run-name: Helmfile lint - -on: - pull_request: - branches: - - 'main' - -jobs: - helmfile-lint: - runs-on: ubuntu-latest - container: - image: ghcr.io/helmfile/helmfile:latest - steps: - - - uses: numerique-gouv/action-helmfile-lint@main - with: - app-id: ${{ secrets.APP_ID }} - age-key: ${{ secrets.SOPS_PRIVATE }} - private-key: ${{ secrets.PRIVATE_KEY }} - helmfile-src: "src/helm" - repositories: "oidc2fer,secrets" diff --git a/.gitmodules b/.gitmodules index 0846cc2..cb96d19 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "secrets"] path = secrets - url = ../secrets + url = ../../numerique-gouv/secrets diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cc0d0..f5a4f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.9] - 2025-10-03 +- map SAML entity IDs to SIRET (#34) +- include ACR claim in ID token only if requested (#32) + +## [1.0.8] - 2025-09-10 +- fix secrets submodule path following move to `proconnect-gouv` organization +- update other references to `numerique-gouv` organization +- publish Docker images at ghcr.io/proconnect-gouv/oidc2fer + +## [1.0.7] - 2025-05-07 +- remove unused APT dependencies +- upgrade to gunicorn 23.0.0 + ## [1.0.6] - 2025-03-11 - upgrade to SATOSA 8.5.1 - allow faculty, staff, employee, researcher, teacher affiliations diff --git a/Dockerfile b/Dockerfile index eed08b8..125974f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,11 @@ # ---- base image to inherit from ---- -FROM python:3.11-slim-bookworm as common +FROM python:3.11.12-slim-bookworm AS common # Install xmlsec1 dependencies required for xmlsec (for SAML) # Needs to be kept before the `pip install` RUN apt-get update && \ apt-get -y upgrade && \ - apt-get install -y \ - pkg-config \ - gcc \ - xmlsec1 \ - libxml2-dev \ - libxmlsec1-dev \ - libxmlsec1-openssl && \ + apt-get install -qy --no-install-recommends xmlsec1 && \ rm -rf /var/lib/apt/lists/* # We want the most up-to-date stable pip release @@ -19,7 +13,7 @@ RUN pip install --upgrade pip ENV PYTHONUNBUFFERED=1 -# Give the "root" group the same permissions as the "root" user on /etc/passwd +# Give the "root" group the same permissions AS the "root" user on /etc/passwd # to allow a user belonging to the root group to add new users; typically the # docker user (see entrypoint). RUN chmod g=u /etc/passwd @@ -41,7 +35,7 @@ COPY docker/files/usr/local/etc/gunicorn/satosa.py /usr/local/etc/gunicorn/satos CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/satosa.py"] # ---- Development image ---- -FROM common as development +FROM common AS development # Playwright browsers ENV PLAYWRIGHT_BROWSERS_PATH=/pw-browsers @@ -67,7 +61,7 @@ COPY ./src/satosa /app/ USER ${DOCKER_USER} # ---- Production image (keep last so it is the default target) ---- -FROM common as production +FROM common AS production # Copy oidc2fer application (see .dockerignore) COPY ./src/satosa /app/ diff --git a/README.md b/README.md index 4ac64a2..ea0548a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ number and date. ## Deploying a release to production (DINUM instance) 1. Make sure the release you want to deploy has been built and appears on - https://hub.docker.com/r/lasuite/oidc2fer/tags . + https://github.com/proconnect-gouv/oidc2fer/pkgs/container/oidc2fer 2. Edit `image/tag` at the top of `src/helm/env.d/outscale-production/values.oidc2fer.yaml.gotmpl`. 3. Commit and push to `main`. diff --git a/docker-compose.yml b/docker-compose.yml index ec1f90f..0a447ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,7 +66,7 @@ services: #OIDC_PROVIDER: https://oidc2fer-staging.beta.numerique.gouv.fr OIDC_CLIENT_ID: oidc-test-client OIDC_CLIENT_SECRET: oidc-test-secret - OIDC_SCOPES: openid,uid,given_name,usual_name,email + OIDC_SCOPES: openid,uid,given_name,usual_name,email,siret extra_hosts: - "oidc2fer.127.0.0.1.nip.io:host-gateway" diff --git a/docker/oidc-test-client/app.py b/docker/oidc-test-client/app.py index 201a4db..d6ac74c 100644 --- a/docker/oidc-test-client/app.py +++ b/docker/oidc-test-client/app.py @@ -2,13 +2,10 @@ from flask import Flask, jsonify, request, session from oic.oic import Client from oic.utils.authn.client import CLIENT_AUTHN_METHOD from oic import rndstr -from oic.oic.message import RegistrationResponse +from oic.oic.message import Claims, ClaimsRequest, RegistrationResponse from oic.utils.http_util import Redirect from oic.oic.message import AuthorizationResponse import secrets -import webbrowser -import threading -import time import logging import os @@ -52,6 +49,7 @@ def index(): "nonce": session["nonce"], "redirect_uri": client.registration_response["redirect_uris"][0], "state": session["state"], + "claims": ClaimsRequest(id_token=Claims(acr=None, amr=None)), } ) login_url = auth_req.request(client.authorization_endpoint) diff --git a/docker/oidc-test-client/requirements.txt b/docker/oidc-test-client/requirements.txt index 7ee47b1..9651550 100644 --- a/docker/oidc-test-client/requirements.txt +++ b/docker/oidc-test-client/requirements.txt @@ -1,2 +1,2 @@ Flask==3.0.3 -oic==1.6.1 +oic==1.7.0 diff --git a/src/helm/env.d/dev/values.oidc2fer.yaml.gotmpl b/src/helm/env.d/dev/values.oidc2fer.yaml.gotmpl index 7301ebe..2d38b2c 100644 --- a/src/helm/env.d/dev/values.oidc2fer.yaml.gotmpl +++ b/src/helm/env.d/dev/values.oidc2fer.yaml.gotmpl @@ -36,6 +36,10 @@ satosa: "token_endpoint_auth_method": "client_secret_post" } } + SIRET_MAP: |- + { + "https://test-idp.federation.renater.fr/idp/shibboleth": "12345678200010" + } ingress: enabled: true diff --git a/src/helm/env.d/outscale-production/values.oidc2fer.yaml.gotmpl b/src/helm/env.d/outscale-production/values.oidc2fer.yaml.gotmpl index 11ae56a..c556bc2 100644 --- a/src/helm/env.d/outscale-production/values.oidc2fer.yaml.gotmpl +++ b/src/helm/env.d/outscale-production/values.oidc2fer.yaml.gotmpl @@ -1,7 +1,7 @@ image: - repository: lasuite/oidc2fer + repository: ghcr.io/proconnect-gouv/oidc2fer pullPolicy: Always - tag: "v1.0.6" + tag: "v1.0.9" satosa: replicas: 2 @@ -24,6 +24,94 @@ satosa: OIDC_DB_URI: { secretKeyRef: { name: redis.redis.libre.sh, key: url } } + # As provided by RENATER on 2025-09-19 + SIRET_MAP: |- + { + "https://shibboleth.grenoble-inp.fr/idp/shibboleth": "19381912500017", + "https://identites.ensea.fr/idp/shibboleth": "19951376300011", + "https://idp.ent.dauphine.fr/idp/shibboleth": "19754692200018", + "https://idp.univ-lyon2.fr/idp/shibboleth": "19691775100014", + "https://shibboleth.insa-rouen.fr/idp/shibboleth": "19760165100023", + "https://idp1.univ-fcomte.fr/idp/shibboleth": "93810656400017", + "https://idp.univ-tours.fr/idp/shibboleth": "19370800500478", + "https://apps.univ-lr.fr/idp/shibboleth": "19170032700015", + "https://shib.mines-albi.fr/idp/shibboleth": "18009202500097", + "https://federation.upf.pf/idp/shibboleth": "19987001500013", + "https://federation.utbm.fr/idp/shibboleth": "19900356700013", + "urn:mace:cru.fr:federation:univ-paris1.fr": "19751717000019", + "https://idp.sciencespobordeaux.fr/idp/shibboleth": "19330192600039", + "https://vip.espci.fr/saml2/idp/metadata.php": "20000068500012", + "urn:mace:cru.fr:federation:univ-nantes.fr": "13002974700016", + "https://shibboleth.univ-corse.fr/idp/shibboleth": "19202664900264", + "https://srv-fii.insa-toulouse.fr/idp/shibboleth": "19310152400018", + "urn:mace:cru.fr:federation:univ-rouen.fr": "19761904200017", + "https://ident-shib.ensc-rennes.fr/idp/shibboleth": "19350077400016", + "urn:mace:cru.fr:federation:unilim.fr": "19870669900321", + "https://idp.inha.fr/idp/shibboleth": "19754688000018", + "https://federation.unimes.fr/idp/shibboleth": "93249157400012", + "urn:mace:cru.fr:federation:univ-rennes1.fr": "13003051300019", + "urn:mace:cru.fr:federation:univ-ubs.fr": "19561718800600", + "https://idp.univ-orleans.fr/idp/shibboleth": "19450855200016", + "https://shibboleth.univ-savoie.fr/idp/shibboleth": "19730858800015", + "https://idp.cirad.fr/idp/shibboleth": "33159627000016", + "https://sso.ird.fr/idp/shibboleth": "18000602500159", + "https://idp.ensma.fr/idp/shibboleth": "19860073600021", + "https://idp.renater.fr/idp/shibboleth": "18008947600055", + "https://idp.inp-toulouse.fr/idp/shibboleth": "19311381800127", + "https://idp.unistra.fr/idp/shibboleth": "13000545700010", + "https://idp1.agroparistech.fr/idp/shibboleth": "13000285000134", + "https://janus.cnrs.fr/idp": "18008901303720", + "https://federation.umontpellier.fr/idp/shibboleth": "13002979600013", + "https://idp.sciencespo-lyon.fr/idp/shibboleth": "19690173000024", + "https://shibboleth.univ-grenoble-alpes.fr/idp/shibboleth": "13002608100013", + "https://idp.bnu.fr/idp/shibboleth": "18004406700015", + "https://ruhnu.univ-tlse2.fr/idp/shibboleth": "19311383400017", + "https://idp2.amue.fr/idp/shibboleth": "18004312700091", + "https://idpv3.univ-amu.fr/idp/shibboleth": "13001533200013", + "https://idp-ng.univ-st-etienne.fr/idp/shibboleth": "93850168100010", + "https://identities.univ-jfc.fr/idp/prod": "19811201300018", + "https://idp3.univ-lorraine.fr/idp/shibboleth": "13001550600012", + "https://idp.univ-lemans.fr/idp/shibboleth": "19720916600010", + "https://idp.univ-lille.fr/idp/shibboleth": "13002975400012", + "https://idp.univ-tln.fr/idp/shibboleth": "19830766200017", + "https://idp.uca.fr/idp/shibboleth": "13002806100013", + "https://shibboleth3.utt.fr/idp/shibboleth": "19101060200032", + "https://idp3.ut-capitole.fr/idp/shibboleth": "13003061200019", + "https://idp.imt-atlantique.fr/idp/shibboleth": "18009202500121", + "https://shib.univ-lehavre.fr/idp/shibboleth": "19762762300097", + "https://idp2.univ-paris8.fr/idp/shibboleth": "19931827000014", + "https://shib2.unc.nc/idp/shibboleth": "13000322100012", + "https://upnidp2.parisnanterre.fr/idp/shibboleth": "19921204400010", + "https://idp.ensfea.fr/idp/shibboleth": "19310143300012", + "https://idp.univ-eiffel.fr/idp/shibboleth": "13002612300013", + "https://idp.insa-rennes.fr/idp/shibboleth": "19350097200016", + "https://idp.univ-tlse3.fr/idp/shibboleth": "93827139200012", + "https://idp.universite-paris-saclay.fr/idp": "13002602400054", + "https://shibboleth.ens2m.fr/idp/shibboleth": "19250082500026", + "https://sso-ciation-saclay.centralesupelec.fr/idp/shibboleth": "13002076100016", + "https://federation.ens.psl.eu/idp/shibboleth": "19753459700012", + "https://authentification.inrae.fr/saml/metadata/idp": "18007003901803", + "https://idp.ensam.eu/idp/shibboleth": "19753472000010", + "https://idp1.ens-paris-saclay.fr/idp/shibboleth": "19940607500036", + "https://idp2.emse.fr/idp/shibboleth": "18009202500105", + "https://idp-septa.onisep.fr/idp/shibboleth": "18004302800653", + "https://papangue.vetagro-sup.fr/idp/shibboleth": "13000858400018", + "https://multipass.imt-nord-europe.fr/idp/shibboleth": "18009202500139", + "https://idp4.unicaen.fr/idp/shibboleth": "19141408500016", + "https://sso.bordeaux-inp.fr/idp/shibboleth": "13000635600013", + "https://auth.uttop.fr/saml/metadata": "19650048200019", + "https://idp.univ-cotedazur.fr/idp/shibboleth": "13002566100013", + "https://idp2.brgm.fr/idp/shibboleth": "58205614900120", + "https://idp.crous-reims.fr/idp/shibboleth": "18510200100327", + "https://idp.univ-avignon.fr/idp/shibboleth": "19840685200204", + "https://idp-genci.renater.fr/idp/shibboleth": "49468697500041", + "https://auth.sso.vet-alfort.fr/saml/metadata": "19940608300014", + "https://idp.univ-perp.fr/idp/shibboleth": "19660437500010", + "https://idp.institut-agro.fr/idp": "13002622200013", + "https://prod-idp.inria.fr": "18008904700013", + "https://idp.ec-nantes.fr/idp/shibboleth": "19440100600011" + } + ingress: enabled: true host: renater.agentconnect.gouv.fr diff --git a/src/helm/env.d/staging/values.oidc2fer.yaml.gotmpl b/src/helm/env.d/staging/values.oidc2fer.yaml.gotmpl index 4b60cc3..c5821eb 100644 --- a/src/helm/env.d/staging/values.oidc2fer.yaml.gotmpl +++ b/src/helm/env.d/staging/values.oidc2fer.yaml.gotmpl @@ -1,7 +1,7 @@ image: - repository: lasuite/oidc2fer + repository: ghcr.io/proconnect-gouv/oidc2fer pullPolicy: Always - tag: "main" + tag: "latest" satosa: replicas: 2 @@ -24,6 +24,11 @@ satosa: OIDC_DB_URI: { secretKeyRef: { name: redis.redis.libre.sh, key: url } } + SIRET_MAP: |- + { + "https://test-idp.federation.renater.fr/idp/shibboleth": "12345678200010" + } + ingress: enabled: true host: oidc2fer-staging.beta.numerique.gouv.fr diff --git a/src/satosa/internal_attributes.yaml b/src/satosa/internal_attributes.yaml index 6a76b29..a3b6e7a 100644 --- a/src/satosa/internal_attributes.yaml +++ b/src/satosa/internal_attributes.yaml @@ -31,3 +31,6 @@ attributes: uid: openid: - uid + siret: + openid: + - siret diff --git a/src/satosa/oidc2fer/attribute_generators/__init__.py b/src/satosa/oidc2fer/attribute_generators/__init__.py new file mode 100644 index 0000000..73c690f --- /dev/null +++ b/src/satosa/oidc2fer/attribute_generators/__init__.py @@ -0,0 +1 @@ +from .entity_id_to_siret_mapper import EntityIdToSiretMapper diff --git a/src/satosa/oidc2fer/attribute_generators/entity_id_to_siret_mapper.py b/src/satosa/oidc2fer/attribute_generators/entity_id_to_siret_mapper.py new file mode 100644 index 0000000..0b05eac --- /dev/null +++ b/src/satosa/oidc2fer/attribute_generators/entity_id_to_siret_mapper.py @@ -0,0 +1,23 @@ +import json +import logging + +from satosa.micro_services.base import ResponseMicroService + +logger = logging.getLogger(__name__) + + +class EntityIdToSiretMapper(ResponseMicroService): + def __init__(self, config, *args, **kwargs): + super().__init__(*args, **kwargs) + self.attribute = config.get("attribute", "siret") + self.mapping = json.loads(config.get("mapping_json", "{}")) + + def process(self, context, data): + entity_id = data.auth_info.issuer + if entity_id in self.mapping: + siret = self.mapping[entity_id] + logger.info("Mapping entity ID %s to SIRET %s", entity_id, siret) + data.attributes[self.attribute] = siret + else: + logger.warning("No SIRET mapping found for entity ID %s", entity_id) + return super().process(context, data) diff --git a/src/satosa/plugins/frontends/openid_connect_frontend.yaml b/src/satosa/plugins/frontends/openid_connect_frontend.yaml index 8bf9d66..f14c718 100644 --- a/src/satosa/plugins/frontends/openid_connect_frontend.yaml +++ b/src/satosa/plugins/frontends/openid_connect_frontend.yaml @@ -43,6 +43,7 @@ config: - uid - given_name - usual_name + - siret # Set code/token lifetimes as short as possible authorization_code_lifetime: 60 @@ -56,8 +57,5 @@ config: - given_name usual_name: - usual_name - extra_id_token_claims: - oidc-test-client: - - acr - agent-connect: - - acr + siret: + - siret diff --git a/src/satosa/plugins/microservices/siret_mapping.yaml b/src/satosa/plugins/microservices/siret_mapping.yaml new file mode 100644 index 0000000..ef00bfc --- /dev/null +++ b/src/satosa/plugins/microservices/siret_mapping.yaml @@ -0,0 +1,5 @@ +module: oidc2fer.attribute_generators.EntityIdToSiretMapper +name: EntityIdToSiretMapper +config: + attribute: siret + mapping_json: !ENV SIRET_MAP diff --git a/src/satosa/proxy_conf.yaml b/src/satosa/proxy_conf.yaml index c6bcca1..c8127bd 100644 --- a/src/satosa/proxy_conf.yaml +++ b/src/satosa/proxy_conf.yaml @@ -17,6 +17,7 @@ MICRO_SERVICES: - plugins/microservices/primary_identifier.yaml - plugins/microservices/static_attributes.yaml - plugins/microservices/attribute_processor.yaml + - plugins/microservices/siret_mapping.yaml LOGGING: # All the logging configuration is done in the Gunicorn config, this is just # here to avoid overwriting it with the default SATOSA config diff --git a/src/satosa/pyproject.toml b/src/satosa/pyproject.toml index fabe804..593c0a0 100644 --- a/src/satosa/pyproject.toml +++ b/src/satosa/pyproject.toml @@ -31,10 +31,10 @@ dependencies = [ ] [project.urls] -"Bug Tracker" = "https://github.com/numerique-gouv/oidc2fer/issues/new" -"Changelog" = "https://github.com/numerique-gouv/oidc2fer/blob/main/CHANGELOG.md" -"Homepage" = "https://github.com/numerique-gouv/oidc2fer" -"Repository" = "https://github.com/numerique-gouv/oidc2fer" +"Bug Tracker" = "https://github.com/proconnect-gouv/oidc2fer/issues/new" +"Changelog" = "https://github.com/proconnect-gouv/oidc2fer/blob/main/CHANGELOG.md" +"Homepage" = "https://github.com/proconnect-gouv/oidc2fer" +"Repository" = "https://github.com/proconnect-gouv/oidc2fer" [project.optional-dependencies] dev = [ diff --git a/src/satosa/tests/oidc2fer/attribute_generators/test_entity_id_to_siret_mapper.py b/src/satosa/tests/oidc2fer/attribute_generators/test_entity_id_to_siret_mapper.py new file mode 100644 index 0000000..2065f86 --- /dev/null +++ b/src/satosa/tests/oidc2fer/attribute_generators/test_entity_id_to_siret_mapper.py @@ -0,0 +1,42 @@ +import json + +from satosa.context import Context +from satosa.internal import AuthenticationInformation, InternalData + +from oidc2fer.attribute_generators import EntityIdToSiretMapper + + +class TestEntityIdToSiretMapper: + def create_mapper(self): + mapper = EntityIdToSiretMapper( + config={ + "attribute": "siret", + "mapping_json": json.dumps( + { + "https://idp.example.fr": "12345678200010", + } + ), + }, + name="siret_mapper", + base_url="https://satosa.example.com", + ) + mapper.next = lambda ctx, data: data + return mapper + + def test_sets_siret_for_known_entityid(self): + mapper = self.create_mapper() + resp = InternalData( + auth_info=AuthenticationInformation(issuer="https://idp.example.fr") + ) + ctx = Context() + mapper.process(ctx, resp) + assert resp.attributes["siret"] == "12345678200010" + + def test_does_not_set_siret_for_unknown_entityid(self): + mapper = self.create_mapper() + resp = InternalData( + auth_info=AuthenticationInformation(issuer="https://unknown.example.fr") + ) + ctx = Context() + mapper.process(ctx, resp) + assert "siret" not in resp.attributes diff --git a/src/satosa/tests/test_e2e.py b/src/satosa/tests/test_e2e.py index 20ab6fa..b208eff 100644 --- a/src/satosa/tests/test_e2e.py +++ b/src/satosa/tests/test_e2e.py @@ -20,6 +20,7 @@ def renater_test_idp(page, login): def renater_wayf(page): page.get_by_text("Veuillez sélectionner").click() + page.get_by_role("searchbox").fill("GIP RENATER - IdP de test") page.get_by_role("option", name="GIP RENATER - IdP de test", exact=True).click() page.get_by_role("button", name="Sélection").click() @@ -48,6 +49,7 @@ def oidc_to_renater( "email": expected_email, "given_name": expected_given_name, "usual_name": expected_usual_name, + "siret": "12345678200010", }.items() <= userinfo.items() return id_token @@ -101,6 +103,7 @@ def pro_connect_to_renater( "email": expected_email, "given_name": expected_given_name, "usual_name": expected_usual_name, + "siret": "12345678200010", }.items() <= result.items() return result @@ -125,7 +128,7 @@ def test_pro_connect_to_renater_student_not_allowed(page: Page): renater_wayf(page) renater_test_idp(page, login="etudiant1") - expect(page.locator("body")).to_contain_text("Une erreur technique est survenue.") + expect(page.locator("body")).to_contain_text("access_denied") @pytest.mark.skipif(