diff --git a/.vscode/launch.json b/.vscode/launch.json index 2308cfec65..577a30c3f5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,6 +36,7 @@ "env": { "OTLP_EXPORT_ENABLED": "false" }, + "envFile": "${workspaceFolder}/example-services/.env" }, { "name": "Blueapi Controller", diff --git a/tests/system_tests/compose.yaml b/tests/system_tests/compose.yaml index 7a73ef5306..d7772edd63 100644 --- a/tests/system_tests/compose.yaml +++ b/tests/system_tests/compose.yaml @@ -64,6 +64,7 @@ services: network_mode: host # Port 8181 volumes: - "./services/opa_config:/mnt" + - "/scratch/ckk43848/authz/policy:/mnt/policy/" environment: - ISSUER=http://localhost:8081/realms/master entrypoint: "sh /mnt/entrypoint.sh" diff --git a/tests/system_tests/config.yaml b/tests/system_tests/config.yaml index 9a35e517cb..a55417d4cf 100644 --- a/tests/system_tests/config.yaml +++ b/tests/system_tests/config.yaml @@ -25,3 +25,8 @@ oidc: issuer: "http://localhost:8081/realms/master" client_id: "ixx-cli-blueapi" client_audience: "ixx-blueapi" +opa: + root: "http://localhost:8181/v1/data/diamond/policy/" + tiled_service_account_check: "blueapi/tiled_service_account_for_beamline" + submit_task_check: "blueapi/write_to_beamline_visit" + admin_check: "admin/admin" diff --git a/tests/system_tests/services/keycloak_config/mappers-template.json b/tests/system_tests/services/keycloak_config/mappers-template.json index 6196af5f19..27be3fb865 100644 --- a/tests/system_tests/services/keycloak_config/mappers-template.json +++ b/tests/system_tests/services/keycloak_config/mappers-template.json @@ -1,14 +1,18 @@ { "protocolMappers": [ { - "name": "fedid", + "name": "username", "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, "config": { + "aggregate.attrs": "false", "introspection.token.claim": "true", - "claim.value": "__CLAIM_VALUE__", + "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" diff --git a/tests/system_tests/services/keycloak_config/service-account.json b/tests/system_tests/services/keycloak_config/service-account-beamline.json similarity index 100% rename from tests/system_tests/services/keycloak_config/service-account.json rename to tests/system_tests/services/keycloak_config/service-account-beamline.json diff --git a/tests/system_tests/services/keycloak_config/service-account-user.json b/tests/system_tests/services/keycloak_config/service-account-user.json new file mode 100644 index 0000000000..6196af5f19 --- /dev/null +++ b/tests/system_tests/services/keycloak_config/service-account-user.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 6ca37c2953..32b22f3e4d 100644 --- a/tests/system_tests/services/keycloak_config/startup.sh +++ b/tests/system_tests/services/keycloak_config/startup.sh @@ -4,11 +4,14 @@ export PATH=$PATH:/opt/keycloak/bin # --- Config --- export KC_CLI_PASSWORD="admin" SERVER="http://localhost:8080" -TEMPLATE="/tmp/config/mappers-template.json" +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 30 -until kcadm.sh config credentials --server $SERVER --realm master --user admin; do sleep 1; done +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 @@ -17,14 +20,30 @@ for type in "Allowed Protocol Mapper Types" "Allowed Client Scopes"; do done done -kcreg.sh config credentials --server $SERVER --realm master --user admin + +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, extra_flags +# Args: client_id, audience, type, fedid, extra_flags create_client() { local client_id=$1 local aud=$2 - shift 2 # The rest are Keycloak attributes (-s key=value) + 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)" @@ -34,11 +53,13 @@ create_client() { echo ">> Creating $client_id..." local tmpfile=$(mktemp) - if [[ "$client_id" == "tiled-writer" ]]; then - cp /tmp/config/service-account.json "$tmpfile" + 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; s/__CLAIM_VALUE__/alice/g" "$TEMPLATE" > "$tmpfile" + sed "s/__AUDIENCE__/$aud/g;" "$GENERAL_TEMPLATE" > "$tmpfile" fi kcreg.sh create -x -s clientId="$client_id" -f "$tmpfile" "$@" @@ -47,31 +68,39 @@ create_client() { # --- 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" \ +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" \ +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" \ +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" \ +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" "" \ +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=["/*"]' diff --git a/tests/system_tests/services/opa_config/entrypoint.sh b/tests/system_tests/services/opa_config/entrypoint.sh index ff5d3b719d..73e271162b 100644 --- a/tests/system_tests/services/opa_config/entrypoint.sh +++ b/tests/system_tests/services/opa_config/entrypoint.sh @@ -1,3 +1,3 @@ #!/bin/sh /opa build -b /mnt/opa_data -o /tmp/bundle.tar.gz -/opa run --server --addr localhost:8181 -b /tmp/bundle.tar.gz --config-file /mnt/config.yaml +/opa run --server --addr localhost:8181 -b /tmp/bundle.tar.gz /mnt/policy/ diff --git a/tests/system_tests/services/opa_config/opa_data/data.json b/tests/system_tests/services/opa_config/opa_data/data.json index 165b8241d4..a337238561 100644 --- a/tests/system_tests/services/opa_config/opa_data/data.json +++ b/tests/system_tests/services/opa_config/opa_data/data.json @@ -4,14 +4,16 @@ "beamlines": { "adsim": { "sessions": [ - 1 + 1, + 2 ] } }, "proposals": { "12345": { "sessions": { - "1": 1 + "1": 1, + "2": 2 } } }, @@ -20,17 +22,34 @@ "beamline": "adsim", "proposal_number": 12345, "visit_number": 1 + }, + "2": { + "beamline": "adsim", + "proposal_number": 12345, + "visit_number": 2 } }, "subjects": { + "admin": { + "permissions": [ + "super_admin" + ], + "proposals": [], + "sessions": [] + }, "alice": { "permissions": [], - "proposals": [ - 12345 - ], + "proposals": [], "sessions": [ 1 ] + }, + "bob": { + "permissions": [], + "proposals": [], + "sessions": [ + 2 + ] } } } diff --git a/tests/system_tests/test_blueapi_system.py b/tests/system_tests/test_blueapi_system.py index c470f08448..a2031dde37 100644 --- a/tests/system_tests/test_blueapi_system.py +++ b/tests/system_tests/test_blueapi_system.py @@ -2,6 +2,8 @@ import time from asyncio import Queue from collections.abc import Generator +from contextlib import nullcontext +from enum import Enum from pathlib import Path from unittest.mock import MagicMock, patch @@ -34,7 +36,6 @@ WorkerTask, ) from blueapi.worker.event import ( - TaskError, TaskResult, TaskStatus, WorkerEvent, @@ -42,21 +43,20 @@ ) from blueapi.worker.task_worker import TrackableTask -AUTHORIZED_INSTRUMENT_SESSION = "cm12345-1" -UNAUTHORIZED_INSTRUMENT_SESSION = "cm54321-1" -FAKE_ACCESS_TAG = '{"proposal": 12345, "visit": 1, "beamline": "adsim"}' -CURRENT_NUMTRACKER_NUM = 43 -_SIMPLE_TASK = TaskRequest( - name="sleep", - params={"time": 0.0}, - instrument_session=AUTHORIZED_INSTRUMENT_SESSION, -) -_LONG_TASK = TaskRequest( - name="sleep", - params={"time": 1.0}, - instrument_session=AUTHORIZED_INSTRUMENT_SESSION, -) +class User(Enum): + alice = "alice" + bob = "bob" + admin = "admin" + + +# Below is the default authorized instrument session for Users +VALID_INSTRUMENT_SESSION: dict[User, str] = { + User.alice: "cm12345-1", + User.bob: "cm12345-2", +} + +CURRENT_NUMTRACKER_NUM = 43 _DATA_PATH = Path(__file__).parent @@ -94,12 +94,44 @@ def load_config(path: Path) -> ApplicationConfig: return loader.load() -def get_access_token() -> str: +@pytest.fixture +def user(request) -> User: + return getattr(request, "param", User.alice) + + +@pytest.fixture +def instrument_session(request) -> str: + return getattr(request, "param", VALID_INSTRUMENT_SESSION[User.alice]) + + +@pytest.fixture +def small_task(user: User, instrument_session: str) -> TaskRequest: + return TaskRequest( + name="sleep", + params={"time": 0.0}, + instrument_session=instrument_session + if instrument_session + else VALID_INSTRUMENT_SESSION[user], + ) + + +@pytest.fixture +def long_task(user: User, instrument_session: str | None = None) -> TaskRequest: + return TaskRequest( + name="sleep", + params={"time": 1.0}, + instrument_session=instrument_session + if instrument_session + else VALID_INSTRUMENT_SESSION[user], + ) + + +def get_access_token(user: User) -> str: token_url = "http://localhost:8081/realms/master/protocol/openid-connect/token" response = requests.post( token_url, data={ - "client_id": "system-test-blueapi", + "client_id": "system-test-blueapi-" + user.value, "client_secret": "secret", "grant_type": "client_credentials", }, @@ -117,27 +149,26 @@ def client_without_auth() -> Generator[BlueapiClient]: yield BlueapiClient.from_config(config=ApplicationConfig()) -@pytest.fixture -def client_with_stomp() -> Generator[BlueapiClient]: +def patch_session(user: User): mock_session_manager = MagicMock() - mock_session_manager.get_valid_access_token = get_access_token - with patch( + mock_session_manager.get_valid_access_token.return_value = get_access_token(user) + return patch( "blueapi.service.authentication.SessionManager.from_cache", return_value=mock_session_manager, - ): + ) + + +@pytest.fixture +def client_with_stomp(user: User) -> Generator[BlueapiClient]: + with patch_session(user): yield BlueapiClient.from_config( 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, - ): +def client(user: User) -> Generator[BlueapiClient]: + with patch_session(user): yield BlueapiClient.from_config(config=ApplicationConfig()) @@ -199,21 +230,21 @@ def reset_numtracker(): requests.post( str(nt_url), json={ - "query": """mutation { + "query": f"""mutation {{ configure(instrument: "adsim", - config: {directory: "/tmp/", - scan: "{instrument}-{scan_number}", - detector: "{instrument}-{scan_number}-{detector}", - scanNumber: 43}) { + config: {{directory: "/tmp/", + scan: "{{instrument}}-{{scan_number}}", + detector: "{{instrument}}-{{scan_number}}-{{detector}}", + scanNumber: {CURRENT_NUMTRACKER_NUM}}}) {{ scanTemplate - } - }""" + }} + }}""" }, ).raise_for_status() yield -def test_cannot_access_endpoints( +def test_cannot_access_endpoints_without_authentication( client_without_auth: BlueapiClient, blueapi_rest_client_get_methods: list[str] ): blueapi_rest_client_get_methods.remove( @@ -280,17 +311,21 @@ def test_client_non_existent_device(client: BlueapiClient): _ = client.devices.missing -def test_create_task_and_delete_task_by_id(rest_client: BlueapiRestClient): - create_task = rest_client.create_task(_SIMPLE_TASK) +def test_create_task_and_delete_task_by_id( + rest_client: BlueapiRestClient, small_task: TaskRequest +): + create_task = rest_client.create_task(small_task) rest_client.clear_task(create_task.task_id) -def test_instrument_session_propagated(rest_client: BlueapiRestClient): - response = rest_client.create_task(_SIMPLE_TASK) +def test_instrument_session_propagated( + rest_client: BlueapiRestClient, small_task: TaskRequest +): + response = rest_client.create_task(small_task) trackable_task = rest_client.get_task(response.task_id) assert trackable_task.task.metadata == { - "user": "alice", - "instrument_session": AUTHORIZED_INSTRUMENT_SESSION, + "user": User.alice.value, + "instrument_session": VALID_INSTRUMENT_SESSION[User.alice], "tiled_access_tags": [ '{"proposal": 12345, "visit": 1, "beamline": "adsim"}', ], @@ -308,9 +343,11 @@ def test_create_task_validation_error(rest_client: BlueapiRestClient): ) -def test_get_all_tasks(rest_client: BlueapiRestClient): +def test_get_all_tasks( + rest_client: BlueapiRestClient, small_task: TaskRequest, long_task: TaskRequest +): created_tasks: list[TaskResponse] = [] - for task in [_SIMPLE_TASK, _LONG_TASK]: + for task in [small_task, long_task]: created_task = rest_client.create_task(task) created_tasks.append(created_task) task_ids = [task.task_id for task in created_tasks] @@ -324,8 +361,8 @@ def test_get_all_tasks(rest_client: BlueapiRestClient): rest_client.clear_task(task_id) -def test_get_task_by_id(rest_client: BlueapiRestClient): - created_task = rest_client.create_task(_SIMPLE_TASK) +def test_get_task_by_id(rest_client: BlueapiRestClient, small_task: TaskRequest): + created_task = rest_client.create_task(small_task) get_task = rest_client.get_task(created_task.task_id) assert ( @@ -348,28 +385,30 @@ def test_delete_non_existent_task(rest_client: BlueapiRestClient): rest_client.clear_task("Not-exists") -def test_put_worker_task(rest_client: BlueapiRestClient): - created_task = rest_client.create_task(_SIMPLE_TASK) +def test_put_worker_task(rest_client: BlueapiRestClient, small_task: TaskRequest): + created_task = rest_client.create_task(small_task) rest_client.update_worker_task(WorkerTask(task_id=created_task.task_id)) active_task = rest_client.get_active_task() assert active_task.task_id == created_task.task_id rest_client.clear_task(created_task.task_id) -def test_put_worker_task_fails_if_not_idle(rest_client: BlueapiRestClient): - small_task = rest_client.create_task(_SIMPLE_TASK) - long_task = rest_client.create_task(_LONG_TASK) +def test_put_worker_task_fails_if_not_idle( + rest_client: BlueapiRestClient, small_task: TaskRequest, long_task: TaskRequest +): + _small_task = rest_client.create_task(small_task) + _long_task = rest_client.create_task(long_task) - rest_client.update_worker_task(WorkerTask(task_id=long_task.task_id)) + rest_client.update_worker_task(WorkerTask(task_id=_long_task.task_id)) active_task = rest_client.get_active_task() - assert active_task.task_id == long_task.task_id + assert active_task.task_id == _long_task.task_id with pytest.raises(BlueskyRemoteControlError) as exception: - rest_client.update_worker_task(WorkerTask(task_id=small_task.task_id)) + rest_client.update_worker_task(WorkerTask(task_id=_small_task.task_id)) assert exception.value.args[0] == 409 rest_client.cancel_current_task(WorkerState.ABORTING) - rest_client.clear_task(small_task.task_id) - rest_client.clear_task(long_task.task_id) + rest_client.clear_task(_small_task.task_id) + rest_client.clear_task(_long_task.task_id) def test_get_worker_state(client: BlueapiClient): @@ -385,9 +424,9 @@ def test_set_state_transition_error(client: BlueapiClient): assert "Cannot transition from IDLE to PAUSED" in exception.value.args[1] -def test_get_task_by_status(rest_client: BlueapiRestClient): - task_1 = rest_client.create_task(_SIMPLE_TASK) - task_2 = rest_client.create_task(_SIMPLE_TASK) +def test_get_task_by_status(rest_client: BlueapiRestClient, small_task: TaskRequest): + task_1 = rest_client.create_task(small_task) + task_2 = rest_client.create_task(small_task) task_by_pending = rest_client.get_all_tasks() # https://github.com/DiamondLightSource/blueapi/issues/680 # task_by_pending = client.get_tasks_by_status(TaskStatusEnum.PENDING) @@ -416,13 +455,13 @@ def test_get_task_by_status(rest_client: BlueapiRestClient): rest_client.clear_task(task_id=task_2.task_id) -def test_progress_with_stomp(client_with_stomp: BlueapiClient): +def test_progress_with_stomp(client_with_stomp: BlueapiClient, small_task: TaskRequest): all_events: list[AnyEvent] = [] def on_event(event: AnyEvent): all_events.append(event) - client_with_stomp.run_task(_SIMPLE_TASK, on_event=on_event) + client_with_stomp.run_task(small_task, on_event=on_event) assert isinstance(all_events[0], WorkerEvent) and all_events[0].task_status task_id = all_events[0].task_status.task_id assert all_events == [ @@ -470,7 +509,7 @@ def test_delete_current_environment(client: BlueapiClient): @pytest.mark.parametrize( - "task,scan_id", + "task,scan_id,user", [ ( TaskRequest( @@ -481,9 +520,10 @@ def test_delete_current_environment(client: BlueapiClient): ], "num": 5, }, - instrument_session=AUTHORIZED_INSTRUMENT_SESSION, + instrument_session=VALID_INSTRUMENT_SESSION[User.alice], ), CURRENT_NUMTRACKER_NUM + 1, + User.alice, ), ( TaskRequest( @@ -509,13 +549,16 @@ def test_delete_current_environment(client: BlueapiClient): "type": "Product", }, }, - instrument_session=AUTHORIZED_INSTRUMENT_SESSION, + instrument_session=VALID_INSTRUMENT_SESSION[User.bob], ), CURRENT_NUMTRACKER_NUM + 2, + User.bob, ), ], ) -def test_plan_runs(client_with_stomp: BlueapiClient, task: TaskRequest, scan_id: int): +def test_plan_runs( + client_with_stomp: BlueapiClient, task: TaskRequest, scan_id: int, user: User +): resource = Queue(maxsize=1) start = Queue(maxsize=1) @@ -534,7 +577,7 @@ def on_event(event: AnyEvent) -> None: start_doc = start.get_nowait() assert start_doc["scan_id"] == scan_id assert start_doc["instrument"] == "adsim" - assert start_doc["instrument_session"] == AUTHORIZED_INSTRUMENT_SESSION + assert start_doc["instrument_session"] == VALID_INSTRUMENT_SESSION[user] assert start_doc["data_session_directory"] == "/tmp" assert start_doc["scan_file"] == f"adsim-{scan_id}" @@ -544,7 +587,7 @@ def on_event(event: AnyEvent) -> None: tiled_url = f"http://localhost:8407/api/v1/metadata/{start_doc['uid']}" response = requests.get( - tiled_url, headers={"authorization": "Bearer " + get_access_token()} + tiled_url, headers={"authorization": "Bearer " + get_access_token(user)} ) assert response.status_code == 200 json = response.json() @@ -554,7 +597,7 @@ def on_event(event: AnyEvent) -> None: assert "start" in json["data"]["attributes"]["metadata"] start_metadata = response.json()["data"]["attributes"]["metadata"]["start"] assert "instrument_session" in start_metadata - assert start_metadata["instrument_session"] == AUTHORIZED_INSTRUMENT_SESSION + assert start_metadata["instrument_session"] == VALID_INSTRUMENT_SESSION[user] assert "scan_id" in start_metadata assert start_metadata["scan_id"] == scan_id assert "detectors" in start_metadata @@ -570,7 +613,7 @@ def on_event(event: AnyEvent) -> None: "movable": "stage.x", "value": "4.0", }, - instrument_session=AUTHORIZED_INSTRUMENT_SESSION, + instrument_session=VALID_INSTRUMENT_SESSION[User.alice], ), ], ) @@ -582,44 +625,70 @@ def test_stub_runs(client_with_stomp: BlueapiClient, task: TaskRequest): @pytest.mark.parametrize( - "task,scan_id", + "instrument_session,user,expectation", [ ( - TaskRequest( - name="count", - params={ - "detectors": [ - "det", - ], - "num": 5, - }, - instrument_session=UNAUTHORIZED_INSTRUMENT_SESSION, + # bob cannot submit a task that alice is on + VALID_INSTRUMENT_SESSION[User.alice], + User.bob, + pytest.raises( + UnauthorisedAccessError, match="Not authorized to submit task" + ), + ), + ( + # alice cannot submit a task that bob is on + VALID_INSTRUMENT_SESSION[User.bob], + User.alice, + pytest.raises( + UnauthorisedAccessError, match="Not authorized to submit task" + ), + ), + ( + # alice can submit a task that alice is on + VALID_INSTRUMENT_SESSION[User.alice], + User.alice, + nullcontext(), + ), + ( + # bob can submit a task that bob is on + VALID_INSTRUMENT_SESSION[User.bob], + User.bob, + nullcontext(), + ), + ( + # admin can submit a task that bob is on + VALID_INSTRUMENT_SESSION[User.bob], + User.admin, + nullcontext(), + ), + ( + # admin can submit a task that alice is on + VALID_INSTRUMENT_SESSION[User.alice], + User.admin, + nullcontext(), + ), + ( + # admin still needs to put a valid instrument_session + VALID_INSTRUMENT_SESSION[User.alice] + "3231232", + User.admin, + pytest.raises( + UnauthorisedAccessError, match="Not authorized to submit task" ), - CURRENT_NUMTRACKER_NUM + 1, ), ], ) -def test_unauthorized_plan_run( - client_with_stomp: BlueapiClient, task: TaskRequest, scan_id: int +def test_submit_plan_authorization( + client: BlueapiClient, + small_task: TaskRequest, + user: str, + instrument_session: str, + expectation, ): - resource = Queue(maxsize=1) - start = Queue(maxsize=1) + with expectation: + client.create_task(small_task) - def on_event(event: AnyEvent) -> None: - if isinstance(event, DataEvent): - if event.name == "start": - start.put_nowait(event.doc) - if event.name == "stream_resource": - resource.put_nowait(event.doc) - outcome = client_with_stomp.run_task(task, on_event) - assert outcome.task_failed - assert outcome.task_complete - assert isinstance(outcome.result, TaskError) - assert outcome.result.type == "ClientError" - assert outcome.result.message.startswith( - "403: Access policy rejects the provided access blob" - ) +def test_get_tasks_authorization(client: BlueapiClient): ... # Regression test for #1480 @@ -637,7 +706,7 @@ def test_task_submission_after_invalid_task(client_with_stomp: BlueapiClient): "det", ], }, - instrument_session=AUTHORIZED_INSTRUMENT_SESSION, + instrument_session=VALID_INSTRUMENT_SESSION[User.alice], ) ) assert isinstance(res.result, TaskResult)