From cd8dc6f2b2bf9b9b99bde94ca9b1d812c1230c8e Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:52:27 +0000 Subject: [PATCH] feat: add oauth2-proxy for blueapi and tiled --- tests/system_tests/compose.yaml | 32 ++++- tests/system_tests/config-cli.yaml | 6 + .../blueapi-oauth2-proxy/oauth2-alpha.yaml | 27 ++++ .../blueapi-oauth2-proxy/oauth2-proxy.cfg | 20 +++ .../keycloak_config/mappers-template.json | 28 ++++ .../services/keycloak_config/startup.sh | 123 +++++++++--------- .../tiled-oauth2-proxy/oauth2-alpha.yaml | 27 ++++ .../tiled-oauth2-proxy/oauth2-proxy.cfg | 18 +++ tests/system_tests/test_blueapi_system.py | 23 ++-- 9 files changed, 223 insertions(+), 81 deletions(-) create mode 100644 tests/system_tests/config-cli.yaml create mode 100644 tests/system_tests/services/blueapi-oauth2-proxy/oauth2-alpha.yaml create mode 100644 tests/system_tests/services/blueapi-oauth2-proxy/oauth2-proxy.cfg create mode 100644 tests/system_tests/services/keycloak_config/mappers-template.json create mode 100644 tests/system_tests/services/tiled-oauth2-proxy/oauth2-alpha.yaml create mode 100644 tests/system_tests/services/tiled-oauth2-proxy/oauth2-proxy.cfg diff --git a/tests/system_tests/compose.yaml b/tests/system_tests/compose.yaml index 9c2efce8b..f18d1e41c 100644 --- a/tests/system_tests/compose.yaml +++ b/tests/system_tests/compose.yaml @@ -27,9 +27,9 @@ services: - KC_HOSTNAME=http://localhost:8081 command: ["start-dev"] volumes: - - ./services/keycloak_config/startup.sh:/tmp/startup.sh + - ./services/keycloak_config/:/tmp/config/ post_start: - - command: bash /tmp/startup.sh + - command: bash /tmp/config/startup.sh ports: - 8081:8080 healthcheck: @@ -59,3 +59,31 @@ services: environment: - ISSUER=http://localhost:8081/realms/master entrypoint: "sh /mnt/entrypoint.sh" + + blueapi-oauth2-proxy: + network_mode: host + image: "quay.io/oauth2-proxy/oauth2-proxy:v7.13.0" + volumes: + - ./services/blueapi-oauth2-proxy/:/opt/config + command: + [ + "--alpha-config=/opt/config/oauth2-alpha.yaml", + "--config=/opt/config/oauth2-proxy.cfg", + ] + depends_on: + keycloak: + condition: service_healthy + + tiled-oauth2-proxy: + network_mode: host + image: "quay.io/oauth2-proxy/oauth2-proxy:v7.13.0" + volumes: + - ./services/tiled-oauth2-proxy/:/opt/config + command: + [ + "--alpha-config=/opt/config/oauth2-alpha.yaml", + "--config=/opt/config/oauth2-proxy.cfg", + ] + depends_on: + keycloak: + condition: service_healthy diff --git a/tests/system_tests/config-cli.yaml b/tests/system_tests/config-cli.yaml new file mode 100644 index 000000000..5ef9b68e0 --- /dev/null +++ b/tests/system_tests/config-cli.yaml @@ -0,0 +1,6 @@ +stomp: + enabled: true + auth: + username: guest + password: guest + url: tcp://localhost:61613 diff --git a/tests/system_tests/services/blueapi-oauth2-proxy/oauth2-alpha.yaml b/tests/system_tests/services/blueapi-oauth2-proxy/oauth2-alpha.yaml new file mode 100644 index 000000000..1502d004e --- /dev/null +++ b/tests/system_tests/services/blueapi-oauth2-proxy/oauth2-alpha.yaml @@ -0,0 +1,27 @@ +injectRequestHeaders: +- name: Authorization + values: + - claim: access_token + prefix: "Bearer " +providers: +- provider: oidc + clientID: ixx-blueapi + clientSecret: blueapi-secret + id: authn + oidcConfig: + audienceClaims: ["aud"] + extraAudiences: ["account"] + emailClaim: sub + insecureAllowUnverifiedEmail: true + insecureSkipNonce: true + issuerURL: http://localhost:8081/realms/master +server: + BindAddress: localhost:4180 + SecureBindAddress: "" + TLS: null +upstreamConfig: + proxyRawPath: true + upstreams: + - id: ixx-blueapi + path: / + uri: http://localhost:8000 diff --git a/tests/system_tests/services/blueapi-oauth2-proxy/oauth2-proxy.cfg b/tests/system_tests/services/blueapi-oauth2-proxy/oauth2-proxy.cfg new file mode 100644 index 000000000..809aba535 --- /dev/null +++ b/tests/system_tests/services/blueapi-oauth2-proxy/oauth2-proxy.cfg @@ -0,0 +1,20 @@ +## OAuth2 Proxy Config File +## https://github.com/oauth2-proxy/oauth2-proxy + +email_domains = [ + "*" +] + +skip_auth_routes=[ + "GET=^/config/oidc", + "GET=^/healthz" +] + +skip_jwt_bearer_tokens = true +skip_provider_button = true + +cookie_secret = "Dhf3pGspLQ5DjtzIz_la8mq2MkCsXzeV" +cookie_expire="30m" +cookie_refresh="1m" +cookie_secure = false +whitelist_domains = "localhost:8081" diff --git a/tests/system_tests/services/keycloak_config/mappers-template.json b/tests/system_tests/services/keycloak_config/mappers-template.json new file mode 100644 index 000000000..6196af5f1 --- /dev/null +++ b/tests/system_tests/services/keycloak_config/mappers-template.json @@ -0,0 +1,28 @@ +{ + "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.sh b/tests/system_tests/services/keycloak_config/startup.sh index aca5261cb..174d8d5f7 100644 --- a/tests/system_tests/services/keycloak_config/startup.sh +++ b/tests/system_tests/services/keycloak_config/startup.sh @@ -1,74 +1,69 @@ #!/bin/bash export PATH=$PATH:/opt/keycloak/bin +# --- Config --- +export KC_CLI_PASSWORD="admin" +SERVER="http://localhost:8080" +TEMPLATE="/tmp/config/mappers-template.json" + +# Wait for Keycloak sleep 30 -while ! kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin; do - sleep 1 -done +until kcadm.sh config credentials --server $SERVER --realm master --user admin; do sleep 1; done -allowed_protocol_mappers=$(kcadm.sh get components -q name="Allowed Protocol Mapper Types" --fields id --format csv --noquotes) -allowed_client_scopes=$(kcadm.sh get components -q name="Allowed Client Scopes" --fields id --format csv --noquotes) -for i in $allowed_protocol_mappers $allowed_client_scopes;do - kcadm.sh delete components/$i +# 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 -kcreg.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin +kcreg.sh config credentials --server $SERVER --realm master --user admin -protocolMappers='{ - "protocolMappers": [ - { - "name": "fedid", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "config": { - "introspection.token.claim": "true", - "claim.value": "alice", - "userinfo.token.claim": "true", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "fedid", - "jsonType.label": "String" - } - }, - { - "name": "blueapi", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-mapper", - "config": { - "introspection.token.claim": "true", - "access.token.claim": "true", - "included.custom.audience": "blueapi" - } - } - ] -}' +# --- Client Creation Function --- +# Args: client_id, audience, extra_flags +create_client() { + local client_id=$1 + local aud=$2 + shift 2 # 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 -for client in "system-test-blueapi" "ixx-cli-blueapi"; do - if ! kcreg.sh get "$client" >/dev/null 2>&1; then - tmpfile=$(mktemp) - echo $protocolMappers > $tmpfile - case $client in - "system-test-blueapi") - kcreg.sh create -x \ - -s clientId=$client \ - -s secret="secret" \ - -s standardFlowEnabled=false \ - -s serviceAccountsEnabled=true \ - -s 'redirectUris=["/*"]' \ - -s attributes='{"access.token.lifespan":"86400"}' \ - -f $tmpfile - ;; - "ixx-cli-blueapi") - kcreg.sh create -x \ - -s clientId=$client \ - -s standardFlowEnabled=false \ - -s publicClient=true \ - -s 'redirectUris=["/*"]' \ - -s 'attributes={"access.token.lifespan":"86400","frontchannel.logout.session.required":"true","oauth2.device.authorization.grant.enabled":"true","use.refresh.tokens":"true","backchannel.logout.session.required":"true"}' \ - -f $tmpfile - ;; - esac - rm $tmpfile - fi -done + echo ">> Creating $client_id..." + local tmpfile=$(mktemp) + + # Use sed to replace placeholders in the JSON template + sed "s/__AUDIENCE__/$aud/g; s/__CLAIM_VALUE__/alice/g" "$TEMPLATE" > "$tmpfile" + + kcreg.sh create -x -s clientId="$client_id" -f "$tmpfile" "$@" + rm "$tmpfile" +} + +# --- Create Clients --- + +# System Test +create_client "system-test-blueapi" "ixx-blueapi" \ + -s secret="secret" -s standardFlowEnabled=false -s serviceAccountsEnabled=true -s 'redirectUris=["/*"]' + +# 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","standard.token.exchange.enabled": "true","standard.token.exchange.enableRefreshRequestedTokenType": "SAME_SESSION"}' + +# 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"}' diff --git a/tests/system_tests/services/tiled-oauth2-proxy/oauth2-alpha.yaml b/tests/system_tests/services/tiled-oauth2-proxy/oauth2-alpha.yaml new file mode 100644 index 000000000..0d6b959d2 --- /dev/null +++ b/tests/system_tests/services/tiled-oauth2-proxy/oauth2-alpha.yaml @@ -0,0 +1,27 @@ +injectRequestHeaders: +- name: Authorization + values: + - claim: access_token + prefix: "Bearer " +providers: +- provider: oidc + clientID: tiled + clientSecret: tiled-secret + id: authn + oidcConfig: + audienceClaims: ["aud"] + extraAudiences: ["account"] + emailClaim: sub + insecureAllowUnverifiedEmail: true + insecureSkipNonce: true + issuerURL: http://localhost:8081/realms/master +server: + BindAddress: localhost:4181 + SecureBindAddress: "" + TLS: null +upstreamConfig: + proxyRawPath: true + upstreams: + - id: tiled + path: / + uri: http://localhost:8407 diff --git a/tests/system_tests/services/tiled-oauth2-proxy/oauth2-proxy.cfg b/tests/system_tests/services/tiled-oauth2-proxy/oauth2-proxy.cfg new file mode 100644 index 000000000..3bfc12e62 --- /dev/null +++ b/tests/system_tests/services/tiled-oauth2-proxy/oauth2-proxy.cfg @@ -0,0 +1,18 @@ +## OAuth2 Proxy Config File +## https://github.com/oauth2-proxy/oauth2-proxy + +email_domains = [ + "*" +] + +skip_auth_routes=[ +"GET=^/api/v1", +] + +skip_jwt_bearer_tokens = true +skip_provider_button = true +cookie_secret = "isNsE3dWf1jum4BrqaU7PvXmwCjaqNQJ4sfAWTfuhLY=" +cookie_expire="30m" +cookie_refresh="1m" +cookie_secure = false +whitelist_domains = "localhost:8081" diff --git a/tests/system_tests/test_blueapi_system.py b/tests/system_tests/test_blueapi_system.py index e4ce504fa..961897346 100644 --- a/tests/system_tests/test_blueapi_system.py +++ b/tests/system_tests/test_blueapi_system.py @@ -7,7 +7,6 @@ import pytest import requests -from bluesky_stomp.models import BasicAuthentication from pydantic import TypeAdapter from requests.exceptions import ConnectionError @@ -21,7 +20,6 @@ ApplicationConfig, ConfigLoader, OIDCConfig, - StompConfig, ) from blueapi.core.bluesky_types import DataEvent from blueapi.service.model import ( @@ -84,6 +82,12 @@ OIDC_TOKEN_ENDPOINT = KEYCLOAK_BASE_URL + "realms/master/protocol/openid-connect/token" +def load_config(path: Path) -> ApplicationConfig: + loader = ConfigLoader(ApplicationConfig) + loader.use_values_from_yaml(path) + return loader.load() + + @pytest.fixture def client_without_auth() -> Generator[BlueapiClient]: with patch( @@ -116,12 +120,7 @@ def client_with_stomp() -> Generator[BlueapiClient]: return_value=mock_session_manager, ): yield BlueapiClient.from_config( - config=ApplicationConfig( - stomp=StompConfig( - enabled=True, - auth=BasicAuthentication(username="guest", password="guest"), # type: ignore - ) - ) + config=load_config(_DATA_PATH / "config-cli.yaml") ) @@ -185,15 +184,9 @@ def clean_existing_tasks(client: BlueapiClient): yield -@pytest.fixture(scope="module") -def server_config() -> ApplicationConfig: - loader = ConfigLoader(ApplicationConfig) - loader.use_values_from_yaml(Path("tests", "system_tests", "config.yaml")) - return loader.load() - - @pytest.fixture(autouse=True, scope="module") def reset_numtracker(server_config: ApplicationConfig): + server_config = load_config(Path(_DATA_PATH, "config.yaml")) nt_url = server_config.numtracker.url # type: ignore - if numtracker is None we should fail requests.post( str(nt_url),