From ba27905677be9fda28b92fbf0721b9f1b8cad7a8 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:44:54 +0100 Subject: [PATCH 1/9] Convert the keycloak scripts --- tests/system_tests/compose.yaml | 18 +- .../keycloak_config/mappers-template.json | 32 -- .../service-account-beamline.json | 34 --- .../keycloak_config/service-account-user.json | 28 -- .../services/keycloak_config/startup.py | 274 ++++++++++++++++++ .../services/keycloak_config/startup.sh | 106 ------- 6 files changed, 288 insertions(+), 204 deletions(-) delete mode 100644 tests/system_tests/services/keycloak_config/mappers-template.json delete mode 100644 tests/system_tests/services/keycloak_config/service-account-beamline.json delete mode 100644 tests/system_tests/services/keycloak_config/service-account-user.json create mode 100644 tests/system_tests/services/keycloak_config/startup.py delete mode 100644 tests/system_tests/services/keycloak_config/startup.sh diff --git a/tests/system_tests/compose.yaml b/tests/system_tests/compose.yaml index 6082515434..52794086c3 100644 --- a/tests/system_tests/compose.yaml +++ b/tests/system_tests/compose.yaml @@ -35,10 +35,6 @@ services: - KC_BOOTSTRAP_ADMIN_USERNAME=admin - KC_HOSTNAME=http://localhost:8081 command: ["start-dev"] - volumes: - - ./services/keycloak_config/:/tmp/config/ - post_start: - - command: bash /tmp/config/startup.sh ports: - 8081:8080 healthcheck: @@ -50,6 +46,20 @@ services: security_opt: - label=disable + keycloak-config: + image: ghcr.io/astral-sh/uv:python3.13-alpine + network_mode: host + volumes: + - ./services/keycloak_config:/config:ro + environment: + - KEYCLOAK_SERVER=http://localhost:8081 + command: ["uv", "run", "/config/startup.py"] + depends_on: + keycloak: + condition: service_healthy + security_opt: + - label=disable + tiled: image: ghcr.io/bluesky/tiled:0.2.12 network_mode: host # Port 8407 diff --git a/tests/system_tests/services/keycloak_config/mappers-template.json b/tests/system_tests/services/keycloak_config/mappers-template.json deleted file mode 100644 index 27be3fb865..0000000000 --- a/tests/system_tests/services/keycloak_config/mappers-template.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "protocolMappers": [ - { - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "aggregate.attrs": "false", - "introspection.token.claim": "true", - "multivalued": "false", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "fedid", - "jsonType.label": "String" - } - }, - { - "name": "audience-mapper", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-mapper", - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true", - "included.custom.audience": "__AUDIENCE__" - } - } - ] -} diff --git a/tests/system_tests/services/keycloak_config/service-account-beamline.json b/tests/system_tests/services/keycloak_config/service-account-beamline.json deleted file mode 100644 index f736156f84..0000000000 --- a/tests/system_tests/services/keycloak_config/service-account-beamline.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "protocolMappers": [ - { - "name": "beamline", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "claim.value": "adsim", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "beamline", - "jsonType.label": "String", - "access.tokenResponse.claim": "false" - } - }, - { - "name": "tiled", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "false", - "lightweight.claim": "false", - "access.token.claim": "true", - "introspection.token.claim": "true", - "included.custom.audience": "tiled-writer" - } - } - ] -} diff --git a/tests/system_tests/services/keycloak_config/service-account-user.json b/tests/system_tests/services/keycloak_config/service-account-user.json deleted file mode 100644 index 6196af5f19..0000000000 --- a/tests/system_tests/services/keycloak_config/service-account-user.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "protocolMappers": [ - { - "name": "fedid", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "config": { - "introspection.token.claim": "true", - "claim.value": "__CLAIM_VALUE__", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "fedid", - "jsonType.label": "String" - } - }, - { - "name": "audience-mapper", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-mapper", - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true", - "included.custom.audience": "__AUDIENCE__" - } - } - ] -} diff --git a/tests/system_tests/services/keycloak_config/startup.py b/tests/system_tests/services/keycloak_config/startup.py new file mode 100644 index 0000000000..2c8a50714f --- /dev/null +++ b/tests/system_tests/services/keycloak_config/startup.py @@ -0,0 +1,274 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "mantelo==2.2.1", +# "pydantic>=2.0", +# ] +# /// +"""Configure the local Keycloak instance used by the system tests. + +Replaces the previous kcadm.sh/kcreg.sh shell script with calls to the +Keycloak Admin REST API via https://github.com/derlin/mantelo. Client +protocol mapper payloads (previously separate JSON template files) are +built here directly as pydantic models. + +Assumes Keycloak is already up and healthy (the container running this +script is only started once the `keycloak` compose service reports +healthy), so there is no need to wait/retry for availability here. +""" + +import os +from typing import Any + +from mantelo import KeycloakAdmin +from pydantic import BaseModel, ConfigDict, Field + +SERVER = os.environ.get("KEYCLOAK_SERVER", "http://localhost:8081") +REALM = os.environ.get("KEYCLOAK_REALM", "master") +ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "admin") +ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD", "admin") + +USERS = {"alice": "alice", "bob": "bob"} + +admin = KeycloakAdmin.from_username_password( + server_url=SERVER, + realm_name=REALM, + client_id="admin-cli", + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, +) + + +class ProtocolMapper(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + name: str + protocol: str = "openid-connect" + protocol_mapper: str = Field(alias="protocolMapper") + consent_required: bool = Field(default=False, alias="consentRequired") + config: dict[str, str] + + +class ClientProtocolMappers(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + protocol_mappers: list[ProtocolMapper] = Field(alias="protocolMappers") + + def payload(self) -> dict[str, Any]: + return self.model_dump(by_alias=True) + + +def general_mappers(audience: str) -> dict[str, Any]: + return ClientProtocolMappers( + protocol_mappers=[ + ProtocolMapper( + name="username", + protocol_mapper="oidc-usermodel-attribute-mapper", + config={ + "aggregate.attrs": "false", + "introspection.token.claim": "true", + "multivalued": "false", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "fedid", + "jsonType.label": "String", + }, + ), + ProtocolMapper( + name="audience-mapper", + protocol_mapper="oidc-audience-mapper", + config={ + "introspection.token.claim": "true", + "access.token.claim": "true", + "included.custom.audience": audience, + }, + ), + ] + ).payload() + + +def beamline_service_account_mappers() -> dict[str, Any]: + return ClientProtocolMappers( + protocol_mappers=[ + ProtocolMapper( + name="beamline", + protocol_mapper="oidc-hardcoded-claim-mapper", + config={ + "introspection.token.claim": "true", + "claim.value": "adsim", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "beamline", + "jsonType.label": "String", + "access.tokenResponse.claim": "false", + }, + ), + ProtocolMapper( + name="tiled", + protocol_mapper="oidc-audience-mapper", + config={ + "id.token.claim": "false", + "lightweight.claim": "false", + "access.token.claim": "true", + "introspection.token.claim": "true", + "included.custom.audience": "tiled-writer", + }, + ), + ] + ).payload() + + +def user_service_account_mappers(audience: str, fedid: str) -> dict[str, Any]: + return ClientProtocolMappers( + protocol_mappers=[ + ProtocolMapper( + name="fedid", + protocol_mapper="oidc-hardcoded-claim-mapper", + config={ + "introspection.token.claim": "true", + "claim.value": fedid, + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "fedid", + "jsonType.label": "String", + }, + ), + ProtocolMapper( + name="audience-mapper", + protocol_mapper="oidc-audience-mapper", + config={ + "introspection.token.claim": "true", + "access.token.claim": "true", + "included.custom.audience": audience, + }, + ), + ] + ).payload() + + +def cleanup_components() -> None: + for component_type in ("Allowed Protocol Mapper Types", "Allowed Client Scopes"): + for component in admin.components.get(name=component_type): + admin.components(component["id"]).delete() + + +def create_users() -> None: + for username, password in USERS.items(): + response, _ = admin.users.as_raw().post({"username": username, "enabled": True}) + user_id = response.headers["Location"].rsplit("/", 1)[-1] + admin.users(user_id).reset_password.put( + {"type": "password", "value": password, "temporary": False} + ) + print(f"User '{username}' created successfully.") + + +def create_client( + client_id: str, + aud: str = "", + account_type: str = "", + fedid: str = "", + **attrs: Any, +) -> None: + if admin.clients.get(clientId=client_id): + print(f">> Skipping {client_id} (exists)") + return + + print(f">> Creating {client_id}...") + + if account_type == "BEAMLINE_SERVICE_ACCOUNT": + payload = beamline_service_account_mappers() + elif account_type == "USER_SERVICE_ACCOUNT": + payload = user_service_account_mappers(aud, fedid) + else: + payload = general_mappers(aud) + + payload["clientId"] = client_id + payload.update(attrs) + admin.clients.post(payload) + + +# Shared by every public (no client-secret) CLI client. +CLI_ATTRIBUTES = { + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "true", + "use.refresh.tokens": "true", + "backchannel.logout.session.required": "true", +} + +CLIENT_DEFINITIONS: list[dict[str, Any]] = [ + { + "client_id": "ixx-cli-blueapi", + "aud": "ixx-blueapi", + "standardFlowEnabled": False, + "publicClient": True, + "redirectUris": ["/*"], + "attributes": CLI_ATTRIBUTES, + }, + { + "client_id": "ixx-blueapi", + "aud": "ixx-blueapi", + "standardFlowEnabled": True, + "secret": "blueapi-secret", + "rootUrl": "http://localhost:4180", + "redirectUris": ["http://localhost:4180/*"], + "attributes": { + "frontchannel.logout.session.required": "true", + "use.refresh.tokens": "true", + }, + }, + { + "client_id": "tiled", + "aud": "tiled", + "standardFlowEnabled": True, + "secret": "tiled-secret", + "rootUrl": "http://localhost:4181", + "redirectUris": ["http://localhost:4181/*"], + }, + { + "client_id": "tiled-cli", + "aud": "tiled", + "standardFlowEnabled": False, + "publicClient": True, + "redirectUris": ["/*"], + "attributes": CLI_ATTRIBUTES, + }, + { + "client_id": "tiled-writer", + "account_type": "BEAMLINE_SERVICE_ACCOUNT", + "secret": "secret", + "standardFlowEnabled": False, + "serviceAccountsEnabled": True, + "redirectUris": ["/*"], + }, + # A system-test service account for the admin user plus each of USERS. + *( + { + "client_id": f"system-test-blueapi-{fedid}", + "aud": "ixx-blueapi", + "account_type": "USER_SERVICE_ACCOUNT", + "fedid": fedid, + "secret": "secret", + "standardFlowEnabled": False, + "serviceAccountsEnabled": True, + "redirectUris": ["/*"], + } + for fedid in ("admin", *USERS) + ), +] + + +def create_clients() -> None: + for definition in CLIENT_DEFINITIONS: + create_client(**definition) + + +if __name__ == "__main__": + cleanup_components() + create_users() + create_clients() diff --git a/tests/system_tests/services/keycloak_config/startup.sh b/tests/system_tests/services/keycloak_config/startup.sh deleted file mode 100644 index 32b22f3e4d..0000000000 --- a/tests/system_tests/services/keycloak_config/startup.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -export PATH=$PATH:/opt/keycloak/bin - -# --- Config --- -export KC_CLI_PASSWORD="admin" -SERVER="http://localhost:8080" -GENERAL_TEMPLATE="/tmp/config/mappers-template.json" -BEAMLINE_SERVICE_TEMPLATE="/tmp/config/service-account-beamline.json" -USER_SERVICE_TEMPLATE="/tmp/config/service-account-user.json" -REALM="master" - -# Wait for Keycloak -sleep 5 -until kcadm.sh config credentials --server $SERVER --realm $REALM --user admin; do sleep 3; done - -# Cleanup logic -for type in "Allowed Protocol Mapper Types" "Allowed Client Scopes"; do - for id in $(kcadm.sh get components -q name="$type" --fields id --format csv --noquotes); do - kcadm.sh delete components/$id - done -done - - -USERS=("alice:alice" "bob:bob") - -for entry in "${USERS[@]}"; do - # Split the string into username and password - username="${entry%%:*}" - password="${entry##*:}" - # Create the user - kcadm.sh create users -r "$REALM" -s username="$username" -s enabled=true - # Set the password - kcadm.sh set-password -r "$REALM" --username "$username" --new-password "$password" - echo "User '$username' created successfully." -done - -kcreg.sh config credentials --server $SERVER --realm $REALM --user admin - -# --- Client Creation Function --- -# Args: client_id, audience, type, fedid, extra_flags -create_client() { - local client_id=$1 - local aud=$2 - local type=$3 - local fedid=$4 - shift 4 # The rest are Keycloak attributes (-s key=value) - - if kcreg.sh get "$client_id" >/dev/null 2>&1; then - echo ">> Skipping $client_id (exists)" - return - fi - - echo ">> Creating $client_id..." - local tmpfile=$(mktemp) - - if [[ "$type" == "BEAMLINE_SERVICE_ACCOUNT" ]]; then - cp $BEAMLINE_SERVICE_TEMPLATE "$tmpfile" - elif [[ "$type" == "USER_SERVICE_ACCOUNT" ]]; then - sed "s/__AUDIENCE__/$aud/g; s/__CLAIM_VALUE__/$fedid/g" "$USER_SERVICE_TEMPLATE" > "$tmpfile" - else - # Use sed to replace placeholders in the JSON template - sed "s/__AUDIENCE__/$aud/g;" "$GENERAL_TEMPLATE" > "$tmpfile" - fi - - kcreg.sh create -x -s clientId="$client_id" -f "$tmpfile" "$@" - rm "$tmpfile" -} - -# --- Create Clients --- - -# ixx CLI -create_client "ixx-cli-blueapi" "ixx-blueapi" "" "" \ - -s standardFlowEnabled=false -s publicClient=true -s 'redirectUris=["/*"]' \ - -s 'attributes={"frontchannel.logout.session.required":"true","oauth2.device.authorization.grant.enabled":"true","use.refresh.tokens":"true","backchannel.logout.session.required":"true"}' - -# ixx BlueAPI -create_client "ixx-blueapi" "ixx-blueapi" "" "" \ - -s standardFlowEnabled=true -s secret="blueapi-secret" -s rootUrl="http://localhost:4180" \ - -s 'redirectUris=["http://localhost:4180/*"]' \ - -s 'attributes={"frontchannel.logout.session.required":"true","use.refresh.tokens":"true"}' - -# Tiled -create_client "tiled" "tiled" "" "" \ - -s standardFlowEnabled=true -s secret="tiled-secret" -s rootUrl="http://localhost:4181" \ - -s 'redirectUris=["http://localhost:4181/*"]' - -# Tiled CLI -create_client "tiled-cli" "tiled" "" ""\ - -s standardFlowEnabled=false -s publicClient=true -s 'redirectUris=["/*"]' \ - -s 'attributes={"frontchannel.logout.session.required":"true","oauth2.device.authorization.grant.enabled":"true","use.refresh.tokens":"true","backchannel.logout.session.required":"true"}' - -# Service account tiled-writer -create_client "tiled-writer" "" "BEAMLINE_SERVICE_ACCOUNT" "" \ - -s secret="secret" -s standardFlowEnabled=false -s serviceAccountsEnabled=true -s 'redirectUris=["/*"]' - -# System Test admin -create_client "system-test-blueapi-admin" "ixx-blueapi" "USER_SERVICE_ACCOUNT" "admin" \ - -s secret="secret" -s standardFlowEnabled=false -s serviceAccountsEnabled=true -s 'redirectUris=["/*"]' - -# System Test alice -create_client "system-test-blueapi-alice" "ixx-blueapi" "USER_SERVICE_ACCOUNT" "alice"\ - -s secret="secret" -s standardFlowEnabled=false -s serviceAccountsEnabled=true -s 'redirectUris=["/*"]' - -# System Test bob -create_client "system-test-blueapi-bob" "ixx-blueapi" "USER_SERVICE_ACCOUNT" "bob"\ - -s secret="secret" -s standardFlowEnabled=false -s serviceAccountsEnabled=true -s 'redirectUris=["/*"]' From bd1be9e3bb434abce610b3c6ab368c964a805b3f Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:47:45 +0000 Subject: [PATCH 2/9] check without PR --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d77f9c5d92..cd825bd493 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,6 @@ concurrency: on: push: - branches: - - main - tags: - - "*" pull_request: merge_group: types: [checks_requested] From 6d61948ae2be3ab256f86cef7ec071f3e03ddd90 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:54:42 +0000 Subject: [PATCH 3/9] --- pyproject.toml | 4 +++- .../services/keycloak_config/startup.py | 24 ++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 790fff190a..00dff822b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,9 @@ reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = '--tb=native -vv --doctest-modules --doctest-glob="*.rst"' +# keycloak_config is a standalone script run inside its own uv/mantelo container, not +# importable by the test suite's environment, so it is excluded from doctest collection. +addopts = '--tb=native -vv --doctest-modules --doctest-glob="*.rst" --ignore=tests/system_tests/services/keycloak_config' # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = ["error", "ignore::DeprecationWarning"] # Doctest python code in docs, python code in src docstrings, test functions in tests diff --git a/tests/system_tests/services/keycloak_config/startup.py b/tests/system_tests/services/keycloak_config/startup.py index 2c8a50714f..88afee9525 100644 --- a/tests/system_tests/services/keycloak_config/startup.py +++ b/tests/system_tests/services/keycloak_config/startup.py @@ -21,7 +21,7 @@ from typing import Any from mantelo import KeycloakAdmin -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, Field SERVER = os.environ.get("KEYCLOAK_SERVER", "http://localhost:8081") REALM = os.environ.get("KEYCLOAK_REALM", "master") @@ -40,8 +40,6 @@ class ProtocolMapper(BaseModel): - model_config = ConfigDict(populate_by_name=True) - name: str protocol: str = "openid-connect" protocol_mapper: str = Field(alias="protocolMapper") @@ -50,8 +48,6 @@ class ProtocolMapper(BaseModel): class ClientProtocolMappers(BaseModel): - model_config = ConfigDict(populate_by_name=True) - protocol_mappers: list[ProtocolMapper] = Field(alias="protocolMappers") def payload(self) -> dict[str, Any]: @@ -60,10 +56,10 @@ def payload(self) -> dict[str, Any]: def general_mappers(audience: str) -> dict[str, Any]: return ClientProtocolMappers( - protocol_mappers=[ + protocolMappers=[ ProtocolMapper( name="username", - protocol_mapper="oidc-usermodel-attribute-mapper", + protocolMapper="oidc-usermodel-attribute-mapper", config={ "aggregate.attrs": "false", "introspection.token.claim": "true", @@ -79,7 +75,7 @@ def general_mappers(audience: str) -> dict[str, Any]: ), ProtocolMapper( name="audience-mapper", - protocol_mapper="oidc-audience-mapper", + protocolMapper="oidc-audience-mapper", config={ "introspection.token.claim": "true", "access.token.claim": "true", @@ -92,10 +88,10 @@ def general_mappers(audience: str) -> dict[str, Any]: def beamline_service_account_mappers() -> dict[str, Any]: return ClientProtocolMappers( - protocol_mappers=[ + protocolMappers=[ ProtocolMapper( name="beamline", - protocol_mapper="oidc-hardcoded-claim-mapper", + protocolMapper="oidc-hardcoded-claim-mapper", config={ "introspection.token.claim": "true", "claim.value": "adsim", @@ -110,7 +106,7 @@ def beamline_service_account_mappers() -> dict[str, Any]: ), ProtocolMapper( name="tiled", - protocol_mapper="oidc-audience-mapper", + protocolMapper="oidc-audience-mapper", config={ "id.token.claim": "false", "lightweight.claim": "false", @@ -125,10 +121,10 @@ def beamline_service_account_mappers() -> dict[str, Any]: def user_service_account_mappers(audience: str, fedid: str) -> dict[str, Any]: return ClientProtocolMappers( - protocol_mappers=[ + protocolMappers=[ ProtocolMapper( name="fedid", - protocol_mapper="oidc-hardcoded-claim-mapper", + protocolMapper="oidc-hardcoded-claim-mapper", config={ "introspection.token.claim": "true", "claim.value": fedid, @@ -141,7 +137,7 @@ def user_service_account_mappers(audience: str, fedid: str) -> dict[str, Any]: ), ProtocolMapper( name="audience-mapper", - protocol_mapper="oidc-audience-mapper", + protocolMapper="oidc-audience-mapper", config={ "introspection.token.claim": "true", "access.token.claim": "true", From a4f2646bec614be95b5f936598fd889290aef1b9 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:11:29 +0000 Subject: [PATCH 4/9] make it smaller --- .../services/keycloak_config/startup.py | 97 +++++++++---------- 1 file changed, 46 insertions(+), 51 deletions(-) diff --git a/tests/system_tests/services/keycloak_config/startup.py b/tests/system_tests/services/keycloak_config/startup.py index 88afee9525..1433a73361 100644 --- a/tests/system_tests/services/keycloak_config/startup.py +++ b/tests/system_tests/services/keycloak_config/startup.py @@ -54,6 +54,42 @@ def payload(self) -> dict[str, Any]: return self.model_dump(by_alias=True) +def audience_mapper( + audience: str, + name: str = "audience-mapper", + extra_config: dict[str, str] | None = None, +) -> ProtocolMapper: + return ProtocolMapper( + name=name, + protocolMapper="oidc-audience-mapper", + config={ + "introspection.token.claim": "true", + "access.token.claim": "true", + "included.custom.audience": audience, + **(extra_config or {}), + }, + ) + + +def hardcoded_claim_mapper( + name: str, claim_value: str, extra_config: dict[str, str] | None = None +) -> ProtocolMapper: + return ProtocolMapper( + name=name, + protocolMapper="oidc-hardcoded-claim-mapper", + config={ + "introspection.token.claim": "true", + "claim.value": claim_value, + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": name, + "jsonType.label": "String", + **(extra_config or {}), + }, + ) + + def general_mappers(audience: str) -> dict[str, Any]: return ClientProtocolMappers( protocolMappers=[ @@ -73,15 +109,7 @@ def general_mappers(audience: str) -> dict[str, Any]: "jsonType.label": "String", }, ), - ProtocolMapper( - name="audience-mapper", - protocolMapper="oidc-audience-mapper", - config={ - "introspection.token.claim": "true", - "access.token.claim": "true", - "included.custom.audience": audience, - }, - ), + audience_mapper(audience), ] ).payload() @@ -89,31 +117,18 @@ def general_mappers(audience: str) -> dict[str, Any]: def beamline_service_account_mappers() -> dict[str, Any]: return ClientProtocolMappers( protocolMappers=[ - ProtocolMapper( - name="beamline", - protocolMapper="oidc-hardcoded-claim-mapper", - config={ - "introspection.token.claim": "true", - "claim.value": "adsim", - "userinfo.token.claim": "true", - "id.token.claim": "true", + hardcoded_claim_mapper( + "beamline", + "adsim", + extra_config={ "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "beamline", - "jsonType.label": "String", "access.tokenResponse.claim": "false", }, ), - ProtocolMapper( + audience_mapper( + "tiled-writer", name="tiled", - protocolMapper="oidc-audience-mapper", - config={ - "id.token.claim": "false", - "lightweight.claim": "false", - "access.token.claim": "true", - "introspection.token.claim": "true", - "included.custom.audience": "tiled-writer", - }, + extra_config={"id.token.claim": "false", "lightweight.claim": "false"}, ), ] ).payload() @@ -122,28 +137,8 @@ def beamline_service_account_mappers() -> dict[str, Any]: def user_service_account_mappers(audience: str, fedid: str) -> dict[str, Any]: return ClientProtocolMappers( protocolMappers=[ - ProtocolMapper( - name="fedid", - protocolMapper="oidc-hardcoded-claim-mapper", - config={ - "introspection.token.claim": "true", - "claim.value": fedid, - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "fedid", - "jsonType.label": "String", - }, - ), - ProtocolMapper( - name="audience-mapper", - protocolMapper="oidc-audience-mapper", - config={ - "introspection.token.claim": "true", - "access.token.claim": "true", - "included.custom.audience": audience, - }, - ), + hardcoded_claim_mapper("fedid", fedid), + audience_mapper(audience), ] ).payload() From f16d3b5c3cc214c2d05128e20a94ef2ce44e5cdd Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:29:10 +0000 Subject: [PATCH 5/9] add stuff --- tests/system_tests/compose.yaml | 9 +- .../services/keycloak_config/startup.py | 265 ------------------ tests/system_tests/services/startup.py | 262 +++++++++++++++++ 3 files changed, 268 insertions(+), 268 deletions(-) delete mode 100644 tests/system_tests/services/keycloak_config/startup.py create mode 100644 tests/system_tests/services/startup.py diff --git a/tests/system_tests/compose.yaml b/tests/system_tests/compose.yaml index 52794086c3..3b116938f5 100644 --- a/tests/system_tests/compose.yaml +++ b/tests/system_tests/compose.yaml @@ -46,14 +46,17 @@ services: security_opt: - label=disable - keycloak-config: + configure-services: image: ghcr.io/astral-sh/uv:python3.13-alpine network_mode: host volumes: - - ./services/keycloak_config:/config:ro + - ./services/startup.py:/startup.py:ro environment: - KEYCLOAK_SERVER=http://localhost:8081 - command: ["uv", "run", "/config/startup.py"] + - KC_BOOTSTRAP_ADMIN_PASSWORD=admin + - KC_BOOTSTRAP_ADMIN_USERNAME=admin + - KEYCLOAK_REALM=master + command: ["uv", "run", "/startup.py"] depends_on: keycloak: condition: service_healthy diff --git a/tests/system_tests/services/keycloak_config/startup.py b/tests/system_tests/services/keycloak_config/startup.py deleted file mode 100644 index 1433a73361..0000000000 --- a/tests/system_tests/services/keycloak_config/startup.py +++ /dev/null @@ -1,265 +0,0 @@ -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# "mantelo==2.2.1", -# "pydantic>=2.0", -# ] -# /// -"""Configure the local Keycloak instance used by the system tests. - -Replaces the previous kcadm.sh/kcreg.sh shell script with calls to the -Keycloak Admin REST API via https://github.com/derlin/mantelo. Client -protocol mapper payloads (previously separate JSON template files) are -built here directly as pydantic models. - -Assumes Keycloak is already up and healthy (the container running this -script is only started once the `keycloak` compose service reports -healthy), so there is no need to wait/retry for availability here. -""" - -import os -from typing import Any - -from mantelo import KeycloakAdmin -from pydantic import BaseModel, Field - -SERVER = os.environ.get("KEYCLOAK_SERVER", "http://localhost:8081") -REALM = os.environ.get("KEYCLOAK_REALM", "master") -ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "admin") -ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD", "admin") - -USERS = {"alice": "alice", "bob": "bob"} - -admin = KeycloakAdmin.from_username_password( - server_url=SERVER, - realm_name=REALM, - client_id="admin-cli", - username=ADMIN_USERNAME, - password=ADMIN_PASSWORD, -) - - -class ProtocolMapper(BaseModel): - name: str - protocol: str = "openid-connect" - protocol_mapper: str = Field(alias="protocolMapper") - consent_required: bool = Field(default=False, alias="consentRequired") - config: dict[str, str] - - -class ClientProtocolMappers(BaseModel): - protocol_mappers: list[ProtocolMapper] = Field(alias="protocolMappers") - - def payload(self) -> dict[str, Any]: - return self.model_dump(by_alias=True) - - -def audience_mapper( - audience: str, - name: str = "audience-mapper", - extra_config: dict[str, str] | None = None, -) -> ProtocolMapper: - return ProtocolMapper( - name=name, - protocolMapper="oidc-audience-mapper", - config={ - "introspection.token.claim": "true", - "access.token.claim": "true", - "included.custom.audience": audience, - **(extra_config or {}), - }, - ) - - -def hardcoded_claim_mapper( - name: str, claim_value: str, extra_config: dict[str, str] | None = None -) -> ProtocolMapper: - return ProtocolMapper( - name=name, - protocolMapper="oidc-hardcoded-claim-mapper", - config={ - "introspection.token.claim": "true", - "claim.value": claim_value, - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": name, - "jsonType.label": "String", - **(extra_config or {}), - }, - ) - - -def general_mappers(audience: str) -> dict[str, Any]: - return ClientProtocolMappers( - protocolMappers=[ - ProtocolMapper( - name="username", - protocolMapper="oidc-usermodel-attribute-mapper", - config={ - "aggregate.attrs": "false", - "introspection.token.claim": "true", - "multivalued": "false", - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "fedid", - "jsonType.label": "String", - }, - ), - audience_mapper(audience), - ] - ).payload() - - -def beamline_service_account_mappers() -> dict[str, Any]: - return ClientProtocolMappers( - protocolMappers=[ - hardcoded_claim_mapper( - "beamline", - "adsim", - extra_config={ - "lightweight.claim": "false", - "access.tokenResponse.claim": "false", - }, - ), - audience_mapper( - "tiled-writer", - name="tiled", - extra_config={"id.token.claim": "false", "lightweight.claim": "false"}, - ), - ] - ).payload() - - -def user_service_account_mappers(audience: str, fedid: str) -> dict[str, Any]: - return ClientProtocolMappers( - protocolMappers=[ - hardcoded_claim_mapper("fedid", fedid), - audience_mapper(audience), - ] - ).payload() - - -def cleanup_components() -> None: - for component_type in ("Allowed Protocol Mapper Types", "Allowed Client Scopes"): - for component in admin.components.get(name=component_type): - admin.components(component["id"]).delete() - - -def create_users() -> None: - for username, password in USERS.items(): - response, _ = admin.users.as_raw().post({"username": username, "enabled": True}) - user_id = response.headers["Location"].rsplit("/", 1)[-1] - admin.users(user_id).reset_password.put( - {"type": "password", "value": password, "temporary": False} - ) - print(f"User '{username}' created successfully.") - - -def create_client( - client_id: str, - aud: str = "", - account_type: str = "", - fedid: str = "", - **attrs: Any, -) -> None: - if admin.clients.get(clientId=client_id): - print(f">> Skipping {client_id} (exists)") - return - - print(f">> Creating {client_id}...") - - if account_type == "BEAMLINE_SERVICE_ACCOUNT": - payload = beamline_service_account_mappers() - elif account_type == "USER_SERVICE_ACCOUNT": - payload = user_service_account_mappers(aud, fedid) - else: - payload = general_mappers(aud) - - payload["clientId"] = client_id - payload.update(attrs) - admin.clients.post(payload) - - -# Shared by every public (no client-secret) CLI client. -CLI_ATTRIBUTES = { - "frontchannel.logout.session.required": "true", - "oauth2.device.authorization.grant.enabled": "true", - "use.refresh.tokens": "true", - "backchannel.logout.session.required": "true", -} - -CLIENT_DEFINITIONS: list[dict[str, Any]] = [ - { - "client_id": "ixx-cli-blueapi", - "aud": "ixx-blueapi", - "standardFlowEnabled": False, - "publicClient": True, - "redirectUris": ["/*"], - "attributes": CLI_ATTRIBUTES, - }, - { - "client_id": "ixx-blueapi", - "aud": "ixx-blueapi", - "standardFlowEnabled": True, - "secret": "blueapi-secret", - "rootUrl": "http://localhost:4180", - "redirectUris": ["http://localhost:4180/*"], - "attributes": { - "frontchannel.logout.session.required": "true", - "use.refresh.tokens": "true", - }, - }, - { - "client_id": "tiled", - "aud": "tiled", - "standardFlowEnabled": True, - "secret": "tiled-secret", - "rootUrl": "http://localhost:4181", - "redirectUris": ["http://localhost:4181/*"], - }, - { - "client_id": "tiled-cli", - "aud": "tiled", - "standardFlowEnabled": False, - "publicClient": True, - "redirectUris": ["/*"], - "attributes": CLI_ATTRIBUTES, - }, - { - "client_id": "tiled-writer", - "account_type": "BEAMLINE_SERVICE_ACCOUNT", - "secret": "secret", - "standardFlowEnabled": False, - "serviceAccountsEnabled": True, - "redirectUris": ["/*"], - }, - # A system-test service account for the admin user plus each of USERS. - *( - { - "client_id": f"system-test-blueapi-{fedid}", - "aud": "ixx-blueapi", - "account_type": "USER_SERVICE_ACCOUNT", - "fedid": fedid, - "secret": "secret", - "standardFlowEnabled": False, - "serviceAccountsEnabled": True, - "redirectUris": ["/*"], - } - for fedid in ("admin", *USERS) - ), -] - - -def create_clients() -> None: - for definition in CLIENT_DEFINITIONS: - create_client(**definition) - - -if __name__ == "__main__": - cleanup_components() - create_users() - create_clients() diff --git a/tests/system_tests/services/startup.py b/tests/system_tests/services/startup.py new file mode 100644 index 0000000000..fb19b54664 --- /dev/null +++ b/tests/system_tests/services/startup.py @@ -0,0 +1,262 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "mantelo==2.2.1", +# ] +# /// +"""Configure the local Keycloak instance used by the system tests.""" + +import os +from collections.abc import Callable +from functools import wraps +from typing import Any + +from mantelo import KeycloakAdmin + +SERVER = os.environ.get("KEYCLOAK_SERVER") +REALM = os.environ.get("KEYCLOAK_REALM") +ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME") +ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD") + +USERS = {"alice": "alice", "bob": "bob"} + +admin = KeycloakAdmin.from_username_password( + server_url=SERVER, + realm_name=REALM, + client_id="admin-cli", + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, +) + + +def protocol_mapper( + name: str, mapper_type: str, config: dict[str, str] +) -> dict[str, Any]: + return { + "name": name, + "protocol": "openid-connect", + "protocolMapper": mapper_type, + "consentRequired": False, + "config": config, + } + + +def audience_mapper( + audience: str, + name: str = "audience-mapper", + extra_config: dict[str, str] | None = None, +) -> dict[str, Any]: + return protocol_mapper( + name, + "oidc-audience-mapper", + { + "introspection.token.claim": "true", + "access.token.claim": "true", + "included.custom.audience": audience, + **(extra_config or {}), + }, + ) + + +def hardcoded_claim_mapper( + name: str, claim_value: str, extra_config: dict[str, str] | None = None +) -> dict[str, Any]: + return protocol_mapper( + name, + "oidc-hardcoded-claim-mapper", + { + "introspection.token.claim": "true", + "claim.value": claim_value, + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": name, + "jsonType.label": "String", + **(extra_config or {}), + }, + ) + + +def general_mappers(audience: str) -> dict[str, Any]: + return { + "protocolMappers": [ + protocol_mapper( + "username", + "oidc-usermodel-attribute-mapper", + { + "aggregate.attrs": "false", + "introspection.token.claim": "true", + "multivalued": "false", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "fedid", + "jsonType.label": "String", + }, + ), + audience_mapper(audience), + ] + } + + +def beamline_service_account_mappers() -> dict[str, Any]: + return { + "protocolMappers": [ + hardcoded_claim_mapper( + "beamline", + "adsim", + extra_config={ + "lightweight.claim": "false", + "access.tokenResponse.claim": "false", + }, + ), + audience_mapper( + "tiled-writer", + name="tiled", + extra_config={"id.token.claim": "false", "lightweight.claim": "false"}, + ), + ] + } + + +def user_service_account_mappers(audience: str, fedid: str) -> dict[str, Any]: + return { + "protocolMappers": [ + hardcoded_claim_mapper("fedid", fedid), + audience_mapper(audience), + ] + } + + +def cleanup_components() -> None: + for component_type in ("Allowed Protocol Mapper Types", "Allowed Client Scopes"): + for component in admin.components.get(name=component_type): + admin.components(component["id"]).delete() + + +def create_users() -> None: + for username, password in USERS.items(): + response, _ = admin.users.as_raw().post({"username": username, "enabled": True}) + user_id = response.headers["Location"].rsplit("/", 1)[-1] + admin.users(user_id).reset_password.put( + {"type": "password", "value": password, "temporary": False} + ) + print(f"User '{username}' created successfully.") + + +def create_client( + build_payload: Callable[..., dict[str, Any]], +) -> Callable[..., None]: + """Wrap a payload builder with the shared create-if-absent flow.""" + + @wraps(build_payload) + def wrapper(client_id: str, **kwargs: Any) -> None: + if admin.clients.get(clientId=client_id): + print(f">> Skipping {client_id} (exists)") + return + + print(f">> Creating {client_id}...") + payload = build_payload(**kwargs) + payload["clientId"] = client_id + admin.clients.post(payload) + + return wrapper + + +@create_client +def create_cli_client( + aud: str = "", + attributes: dict[str, str] = { # noqa: B006 + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "true", + "use.refresh.tokens": "true", + "backchannel.logout.session.required": "true", + }, +) -> dict[str, Any]: + payload = general_mappers(aud) + payload.update( + standardFlowEnabled=False, + publicClient=True, + redirectUris=["/*"], + attributes=attributes, + ) + return payload + + +@create_client +def create_web_client( + aud: str, + secret: str, + root_url: str, + attributes: dict[str, str] = { # noqa: B006 + "frontchannel.logout.session.required": "true", + "use.refresh.tokens": "true", + }, +) -> dict[str, Any]: + payload = general_mappers(aud) + payload.update( + standardFlowEnabled=True, + secret=secret, + rootUrl=root_url, + redirectUris=[f"{root_url}/*"], + **({"attributes": attributes} if attributes else {}), + ) + return payload + + +@create_client +def create_beamline_service_account_client(secret: str = "secret") -> dict[str, Any]: + payload = beamline_service_account_mappers() + payload.update( + secret=secret, + standardFlowEnabled=False, + serviceAccountsEnabled=True, + redirectUris=["/*"], + ) + return payload + + +@create_client +def create_user_service_account_client( + fedid: str, aud: str = "ixx-blueapi", secret: str = "secret" +) -> dict[str, Any]: + payload = user_service_account_mappers(aud, fedid) + payload.update( + secret=secret, + standardFlowEnabled=False, + serviceAccountsEnabled=True, + redirectUris=["/*"], + ) + return payload + + +def create_clients() -> None: + create_cli_client(client_id="ixx-cli-blueapi", aud="ixx-blueapi") + create_cli_client(client_id="tiled-cli", aud="tiled") + create_web_client( + client_id="ixx-blueapi", + aud="ixx-blueapi", + secret="blueapi-secret", + rootUrl="http://localhost:4180", + ) + create_web_client( + client_id="tiled", + aud="tiled", + secret="tiled-secret", + rootUrl="http://localhost:4181", + ) + create_beamline_service_account_client(client_id="tiled-writer") + + # A system-test service account for the admin user plus each of USERS. + for fedid in ("admin", *USERS): + create_user_service_account_client( + client_id=f"system-test-blueapi-{fedid}", fedid=fedid + ) + + +if __name__ == "__main__": + cleanup_components() + create_users() + create_clients() From fcada26975be3eeafde7337a590a9c672e7d367d Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:30:40 +0000 Subject: [PATCH 6/9] update file --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00dff822b3..81603f54f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,9 +101,7 @@ reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error -# keycloak_config is a standalone script run inside its own uv/mantelo container, not -# importable by the test suite's environment, so it is excluded from doctest collection. -addopts = '--tb=native -vv --doctest-modules --doctest-glob="*.rst" --ignore=tests/system_tests/services/keycloak_config' +addopts = '--tb=native -vv --doctest-modules --doctest-glob="*.rst" --ignore=tests/system_tests/services/*' # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = ["error", "ignore::DeprecationWarning"] # Doctest python code in docs, python code in src docstrings, test functions in tests From aa741ceefad2e69cca60c0ef27d8c2341a28e480 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:39:24 +0000 Subject: [PATCH 7/9] fix the test collection --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 81603f54f1..a1b0761ccc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,7 @@ reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = '--tb=native -vv --doctest-modules --doctest-glob="*.rst" --ignore=tests/system_tests/services/*' +addopts = '--tb=native -vv --doctest-modules --doctest-glob="*.rst" --ignore=tests/system_tests/services' # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = ["error", "ignore::DeprecationWarning"] # Doctest python code in docs, python code in src docstrings, test functions in tests From 5ea7a221b3bb89308d645dfbb6228a34f1fbb664 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:42:36 +0000 Subject: [PATCH 8/9] fix few issues --- tests/system_tests/services/startup.py | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/system_tests/services/startup.py b/tests/system_tests/services/startup.py index fb19b54664..d1072c0124 100644 --- a/tests/system_tests/services/startup.py +++ b/tests/system_tests/services/startup.py @@ -15,8 +15,8 @@ SERVER = os.environ.get("KEYCLOAK_SERVER") REALM = os.environ.get("KEYCLOAK_REALM") -ADMIN_USERNAME = os.environ.get("KEYCLOAK_ADMIN_USERNAME") -ADMIN_PASSWORD = os.environ.get("KEYCLOAK_ADMIN_PASSWORD") +ADMIN_USERNAME = os.environ.get("KC_BOOTSTRAP_ADMIN_USERNAME") +ADMIN_PASSWORD = os.environ.get("KC_BOOTSTRAP_ADMIN_PASSWORD") USERS = {"alice": "alice", "bob": "bob"} @@ -168,19 +168,19 @@ def wrapper(client_id: str, **kwargs: Any) -> None: @create_client def create_cli_client( aud: str = "", - attributes: dict[str, str] = { # noqa: B006 - "frontchannel.logout.session.required": "true", - "oauth2.device.authorization.grant.enabled": "true", - "use.refresh.tokens": "true", - "backchannel.logout.session.required": "true", - }, + attributes: dict[str, str] | None = None, ) -> dict[str, Any]: payload = general_mappers(aud) payload.update( standardFlowEnabled=False, publicClient=True, redirectUris=["/*"], - attributes=attributes, + attributes={ + "frontchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "true", + "use.refresh.tokens": "true", + "backchannel.logout.session.required": "true", + }, ) return payload @@ -190,10 +190,7 @@ def create_web_client( aud: str, secret: str, root_url: str, - attributes: dict[str, str] = { # noqa: B006 - "frontchannel.logout.session.required": "true", - "use.refresh.tokens": "true", - }, + attributes: dict[str, str] | None = None, ) -> dict[str, Any]: payload = general_mappers(aud) payload.update( @@ -239,13 +236,17 @@ def create_clients() -> None: client_id="ixx-blueapi", aud="ixx-blueapi", secret="blueapi-secret", - rootUrl="http://localhost:4180", + root_url="http://localhost:4180", + attributes={ + "frontchannel.logout.session.required": "true", + "use.refresh.tokens": "true", + }, ) create_web_client( client_id="tiled", aud="tiled", secret="tiled-secret", - rootUrl="http://localhost:4181", + root_url="http://localhost:4181", ) create_beamline_service_account_client(client_id="tiled-writer") From a6ccd50de06f66318f1886ba3f7866a6edc571dc Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:53:45 +0000 Subject: [PATCH 9/9] remove ci change --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd825bd493..d77f9c5d92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ concurrency: on: push: + branches: + - main + tags: + - "*" pull_request: merge_group: types: [checks_requested]