diff --git a/tests/system_tests/compose.yaml b/tests/system_tests/compose.yaml index 9c2efce8b5..f18d1e41c6 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 0000000000..5ef9b68e02 --- /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/config.yaml b/tests/system_tests/config.yaml index 11dcf5f059..a0a6ed9027 100644 --- a/tests/system_tests/config.yaml +++ b/tests/system_tests/config.yaml @@ -21,4 +21,4 @@ tiled: oidc: well_known_url: "http://localhost:8081/realms/master/.well-known/openid-configuration" client_id: "ixx-cli-blueapi" - client_audience: "blueapi" + client_audience: "ixx-blueapi" 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 0000000000..1502d004e3 --- /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 0000000000..809aba5354 --- /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 0000000000..6196af5f19 --- /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 aca5261cbc..1499f628b1 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=["/*"]' -s attributes='{"access.token.lifespan":"86400"}' + +# 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","access.token.lifespan":"86400"}' + +# 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","access.token.lifespan":"86400"}' + +# 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 0000000000..0d6b959d25 --- /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 0000000000..3bfc12e626 --- /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 e4ce504fab..3b39153493 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 ( @@ -78,58 +76,66 @@ # docker compose -f tests/system_tests/compose.yaml down # This client will give tokens for alice -CLIENT_ID = "system-test-blueapi" -CLIENT_SECRET = "secret" -KEYCLOAK_BASE_URL = "http://localhost:8081/" -OIDC_TOKEN_ENDPOINT = KEYCLOAK_BASE_URL + "realms/master/protocol/openid-connect/token" -@pytest.fixture -def client_without_auth() -> Generator[BlueapiClient]: - with patch( - "blueapi.service.authentication.SessionManager.from_cache", - return_value=None, - ): - yield BlueapiClient.from_config(config=ApplicationConfig()) +def load_config(path: Path) -> ApplicationConfig: + loader = ConfigLoader(ApplicationConfig) + loader.use_values_from_yaml(path) + return loader.load() def get_access_token() -> str: + token_url = "http://localhost:8081/realms/master/protocol/openid-connect/token" response = requests.post( - OIDC_TOKEN_ENDPOINT, + token_url, data={ - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, + "client_id": "system-test-blueapi", + "client_secret": "secret", "grant_type": "client_credentials", }, ) - response.raise_for_status() return response.json().get("access_token") +@pytest.fixture(scope="module") +def client_without_auth() -> Generator[BlueapiClient]: + with patch( + "blueapi.service.authentication.SessionManager.from_cache", + return_value=None, + ): + yield BlueapiClient.from_config(config=ApplicationConfig()) + + @pytest.fixture def client_with_stomp() -> Generator[BlueapiClient]: - mock_session_manager = MagicMock + mock_session_manager = MagicMock() mock_session_manager.get_valid_access_token = get_access_token with patch( "blueapi.service.authentication.SessionManager.from_cache", 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") ) +@pytest.fixture +def client() -> Generator[BlueapiClient]: + mock_session_manager = MagicMock() + mock_session_manager.get_valid_access_token = get_access_token + with patch( + "blueapi.service.authentication.SessionManager.from_cache", + return_value=mock_session_manager, + ): + yield BlueapiClient.from_config(config=ApplicationConfig()) + + @pytest.fixture(scope="module", autouse=True) -def wait_for_server(client: BlueapiClient): +def wait_for_server(client_without_auth: BlueapiClient): for _ in range(20): try: - client.get_environment() + client_without_auth.get_oidc_config() return except ConnectionError: ... @@ -137,17 +143,6 @@ def wait_for_server(client: BlueapiClient): raise TimeoutError("No connection to the blueapi server") -@pytest.fixture(scope="module") -def client() -> Generator[BlueapiClient]: - mock_session_manager = MagicMock - mock_session_manager.get_valid_access_token = get_access_token - with patch( - "blueapi.service.authentication.SessionManager.from_cache", - return_value=mock_session_manager, - ): - yield BlueapiClient.from_config(config=ApplicationConfig()) - - @pytest.fixture def expected_plans() -> PlanResponse: return TypeAdapter(PlanResponse).validate_json( @@ -185,15 +180,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): +def reset_numtracker(): + 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), @@ -225,9 +214,9 @@ def test_cannot_access_endpoints( def test_can_get_oidc_config_without_auth(client_without_auth: BlueapiClient): assert client_without_auth.get_oidc_config() == OIDCConfig( - well_known_url=KEYCLOAK_BASE_URL - + "realms/master/.well-known/openid-configuration", + well_known_url="http://localhost:8081/realms/master/.well-known/openid-configuration", client_id="ixx-cli-blueapi", + client_audience="ixx-blueapi", )