From 54fd720c22f6d14b60131c75a4b25a249bb2816c Mon Sep 17 00:00:00 2001 From: Julien Perrochet Date: Wed, 21 Feb 2024 17:28:58 +0100 Subject: [PATCH] DSS0210,A2-7-2,7 OIR --- .../resources/astm/f3548/v21/planning_area.py | 57 +- .../astm/utm/dss/authentication/generic.py | 99 +++ .../dss/authentication/sub_api_validator.py | 515 +++++++++++++++ .../astm/utm/dss/authentication_validation.py | 601 ++---------------- .../astm/utm/dss/test_step_fragments.py | 4 +- .../uss_qualifier/scenarios/scenario.py | 5 +- 6 files changed, 708 insertions(+), 573 deletions(-) create mode 100644 monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/generic.py create mode 100644 monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/sub_api_validator.py diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py index 73bf102364..eade2fae25 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/planning_area.py @@ -1,10 +1,19 @@ import datetime -from typing import List, Dict, Any, Optional, Self +from typing import List, Optional -from implicitdict import ImplicitDict, StringBasedDateTime -from uas_standards.astm.f3548.v21.api import Volume4D +from implicitdict import ImplicitDict +from uas_standards.astm.f3548.v21.api import ( + EntityOVN, + OperationalIntentState, + UssBaseURL, + EntityID, + PutOperationalIntentReferenceParameters, + ImplicitSubscriptionParameters, +) -from monitoring.monitorlib.geo import LatLngPoint, make_latlng_rect, Volume3D, Polygon +from monitoring.monitorlib.geo import make_latlng_rect, Volume3D +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.temporal import Time from monitoring.uss_qualifier.resources.astm.f3548.v21.subscription_params import ( SubscriptionParams, ) @@ -52,6 +61,46 @@ def get_new_subscription_params( notify_for_constraints=notify_for_constraints, ) + def get_new_operational_intent_ref_params( + self, + key: List[EntityOVN], + state: OperationalIntentState, + uss_base_url: UssBaseURL, + time_start: datetime.datetime, + time_end: datetime.datetime, + subscription_id: Optional[EntityID], + implicit_sub_base_url: Optional[UssBaseURL] = None, + implicit_sub_for_constraints: Optional[bool] = None, + ) -> PutOperationalIntentReferenceParameters: + """ + Build a PutOperationalIntentReferenceParameters object that can be used against the DSS OIR API. + + The extents contained in these parameters contain a single 4DVolume, which may not be entirely realistic, + but is sufficient in situations where the content of the OIR is irrelevant as long as it is valid, such + as for testing authentication or parameter validation. + + Note that this method allows building inconsistent parameters: + """ + return PutOperationalIntentReferenceParameters( + extents=[ + Volume4D( + volume=self.volume, + time_start=Time(time_start), + time_end=Time(time_end), + ).to_f3548v21() + ], + key=key, + state=state, + uss_base_url=uss_base_url, + subscription_id=subscription_id, + new_subscription=ImplicitSubscriptionParameters( + uss_base_url=implicit_sub_base_url, + notify_for_constraints=implicit_sub_for_constraints, + ) + if implicit_sub_base_url + else None, + ) + class PlanningAreaResource(Resource[PlanningAreaSpecification]): specification: PlanningAreaSpecification diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/generic.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/generic.py new file mode 100644 index 0000000000..b687eb65d5 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/generic.py @@ -0,0 +1,99 @@ +from uas_standards.astm.f3548.v21.constants import Scope + +from monitoring.monitorlib import fetch +from monitoring.monitorlib.auth import InvalidTokenSignatureAuth +from monitoring.monitorlib.infrastructure import UTMClientSession +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance +from monitoring.uss_qualifier.scenarios.scenario import TestScenario + + +class GenericAuthValidator: + """ + Utility class for common DSS authentication validation requirements. + """ + + def __init__( + self, + scenario: TestScenario, + dss: DSSInstance, + ): + self._scenario = scenario + self._authenticated_session = dss.client + self._invalid_token_session = UTMClientSession( + dss.base_url, auth_adapter=InvalidTokenSignatureAuth() + ) + self._no_auth_session = UTMClientSession(dss.base_url, auth_adapter=None) + + def query_no_auth(self, **query_kwargs) -> fetch.Query: + """Issue a query to the DSS without any credentials being passed""" + q = fetch.query_and_describe(client=self._no_auth_session, **query_kwargs) + self._scenario.record_query(q) + return q + + def query_invalid_token(self, **query_kwargs) -> fetch.Query: + """ + Issue a query to the DSS with an invalid token signature, but a valid token. + An appropriate scope is provided. + """ + q = fetch.query_and_describe( + client=self._invalid_token_session, + scope=Scope.StrategicCoordination, + **query_kwargs, + ) + self._scenario.record_query(q) + return q + + def query_missing_scope(self, **query_kwargs) -> fetch.Query: + """ + Issue a query to the DSS with a valid token, but omits specifying a scope. + """ + q = fetch.query_and_describe(client=self._authenticated_session, **query_kwargs) + self._scenario.record_query(q) + return q + + def query_wrong_scope(self, **query_kwargs) -> fetch.Query: + """ + Issue a query to the DSS with a valid token, but with a scope that is not allowed + to perform the operation. + """ + q = fetch.query_and_describe( + client=self._authenticated_session, + scope=Scope.AvailabilityArbitration, # TODO make these configurable via the constructor? + **query_kwargs, + ) + self._scenario.record_query(q) + return q + + def query_valid_auth(self, **query_kwargs) -> fetch.Query: + """ + Issue a query to the DSS with valid credentials. + """ + q = fetch.query_and_describe( + client=self._authenticated_session, + scope=Scope.StrategicCoordination, + # TODO make these configurable via the constructor? 'valid' may be different for certain endpoints + **query_kwargs, + ) + self._scenario.record_query(q) + return q + + def verify_4xx_response(self, q: fetch.Query): + """Verifies that the passed query response's body is a valid ErrorResponse: + it is either empty or contains a single 'message' field, as per the OpenAPI spec. + + Note that 409 responses to Operational Intent Reference mutations will contain more fields, + these are not handled here. + """ + with self._scenario.check( + "Unauthorized requests return the proper error message body" + ) as check: + if len(q.response.json) == 0: + return + elif len(q.response.json) == 1 and "message" in q.response.json: + return + else: + check.record_failed( + summary="Unexpected error response body", + details=f"Response body for {q.request.method} query to {q.request.url} should be empty or contain a single 'message' field. Was: {q.response.json}", + query_timestamps=[q.request.timestamp], + ) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/sub_api_validator.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/sub_api_validator.py new file mode 100644 index 0000000000..7712f991fc --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/sub_api_validator.py @@ -0,0 +1,515 @@ +from datetime import datetime, timedelta + +from uas_standards.astm.f3548.v21.api import ( + OPERATIONS, + OperationID, + QuerySubscriptionParameters, +) + +from monitoring.monitorlib import fetch +from monitoring.monitorlib.fetch import QueryType +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.infrastructure import UTMClientSession +from monitoring.monitorlib.mutate import scd as mutate +from monitoring.monitorlib.mutate.scd import MutatedSubscription +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance +from monitoring.uss_qualifier.resources.astm.f3548.v21.planning_area import ( + PlanningAreaSpecification, +) +from monitoring.uss_qualifier.scenarios.astm.utm.dss.authentication.generic import ( + GenericAuthValidator, +) +from monitoring.uss_qualifier.scenarios.scenario import TestScenario, PendingCheck + +TIME_TOLERANCE_SEC = 1 + + +class SubscriptionAuthValidator: + def __init__( + self, + scenario: TestScenario, + generic_validator: GenericAuthValidator, + dss: DSSInstance, + test_id: str, + planning_area: PlanningAreaSpecification, + planning_area_volume4d: Volume4D, + no_auth_session: UTMClientSession, + invalid_token_session: UTMClientSession, + ): + self._scenario = scenario + self._gen_val = generic_validator + self._dss = dss + self._pid = dss.participant_id + self._test_id = test_id + self._planning_area = planning_area + self._planning_area_volume4d = planning_area_volume4d + + self._sub_params = self._planning_area.get_new_subscription_params( + subscription_id=self._test_id, + # Set this slightly in the past: we will update the subscriptions + # to a later value that still needs to be roughly 'now' without getting into the future + start_time=datetime.now().astimezone() - timedelta(seconds=10), + duration=timedelta(minutes=20), + # This is a planning area without constraint processing + notify_for_op_intents=True, + notify_for_constraints=False, + ) + + self._no_auth_session = no_auth_session + self._invalid_token_session = invalid_token_session + + def verify_sub_endpoints_authentication(self): + self._verify_sub_creation() + self._verify_sub_get() + self._verify_sub_mutation() + self._verify_sub_deletion() + self._verify_sub_search() + + def _verify_sub_creation(self): + # Subscription creation request: + sub_params = dict(**self._sub_params) + op = OPERATIONS[OperationID.CreateSubscription] + del sub_params["sub_id"] + query_kwargs = dict( + verb=op.verb, + url=op.path.format(subscriptionid=self._sub_params.sub_id), + json=mutate.build_upsert_subscription_params(**sub_params), + query_type=QueryType.F3548v21DSSCreateSubscription, + participant_id=self._dss.participant_id, + ) + + # No auth: + no_auth_q = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Create subscription with missing credentials" + ) as check: + if no_auth_q.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {no_auth_q.status_code}", + query_timestamps=[no_auth_q.request.timestamp], + ) + + self._sanity_check_sub_not_created(check, no_auth_q) + + self._gen_val.verify_4xx_response(no_auth_q) + + # Bad token signature: + invalid_token_q = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Create subscription with invalid credentials" + ) as check: + if invalid_token_q.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {invalid_token_q.status_code}", + query_timestamps=[invalid_token_q.request.timestamp], + ) + + self._sanity_check_sub_not_created(check, invalid_token_q) + + self._gen_val.verify_4xx_response(invalid_token_q) + + # Valid credentials but missing scope: + no_scope_q = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check("Create subscription with missing scope") as check: + if no_scope_q.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {no_scope_q.status_code}", + query_timestamps=[no_scope_q.request.timestamp], + ) + + self._sanity_check_sub_not_created(check, no_scope_q) + + # Valid credentials but wrong scope: + wrong_scope_q = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check("Create subscription with incorrect scope") as check: + if wrong_scope_q.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {wrong_scope_q.status_code}", + details="With an incorrect scope, the DSS should return a 403 without any data.", + query_timestamps=[wrong_scope_q.request.timestamp], + ) + + self._sanity_check_sub_not_created(check, wrong_scope_q) + + # Correct token: + # - confirms that the request would otherwise work + # - makes a subscription available for read/mutation tests + create_ok_q = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Create subscription with valid credentials" + ) as check: + if create_ok_q.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {create_ok_q.status_code}", + query_timestamps=[create_ok_q.request.timestamp], + ) + + # Store the subscription + self._current_sub = MutatedSubscription(create_ok_q).subscription + + def _verify_sub_get(self): + op = OPERATIONS[OperationID.GetSubscription] + + query_kwargs = dict( + verb=op.verb, + url=op.path.format(subscriptionid=self._sub_params.sub_id), + query_type=QueryType.F3548v21DSSGetSubscription, + participant_id=self._dss.participant_id, + ) + + query_no_auth = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check("Get subscription with missing credentials") as check: + if query_no_auth.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_no_auth.status_code}", + query_timestamps=[query_no_auth.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_no_auth) + + query_invalid_token = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check("Get subscription with invalid credentials") as check: + if query_invalid_token.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_invalid_token.status_code}", + query_timestamps=[query_invalid_token.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_invalid_token) + + query_missing_scope = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check("Get subscription with missing scope") as check: + if query_missing_scope.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_missing_scope.status_code}", + details="Without the proper scope, the DSS should return a 401 without any data.", + query_timestamps=[query_missing_scope.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_missing_scope) + + query_wrong_scope = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check("Get subscription with incorrect scope") as check: + if query_wrong_scope.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {query_wrong_scope.status_code}", + details="With an incorrect scope, the DSS should return a 403 without any data.", + query_timestamps=[query_wrong_scope.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_wrong_scope) + + query_ok = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check("Get subscription with valid credentials") as check: + if query_ok.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {query_ok.status_code}", + query_timestamps=[query_ok.request.timestamp], + ) + + def _verify_sub_mutation(self): + # Subscription creation request: + new_params = self._sub_params.copy() + new_params.end_time = new_params.end_time - timedelta(seconds=10) + pld_params = dict(**new_params) + op = OPERATIONS[OperationID.UpdateSubscription] + del pld_params["sub_id"] + + query_kwargs = dict( + verb=op.verb, + url=op.path.format( + subscriptionid=self._sub_params.sub_id, + version=self._current_sub.version, + ), + json=mutate.build_upsert_subscription_params(**pld_params), + query_type=QueryType.F3548v21DSSCreateSubscription, + participant_id=self._dss.participant_id, + ) + + no_auth_q = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Mutate subscription with missing credentials" + ) as check: + if no_auth_q.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {no_auth_q.status_code}", + query_timestamps=[no_auth_q.request.timestamp], + ) + # Sanity check: confirm the subscription was not mutated by the faulty call: + self._sanity_check_sub_not_mutated(check, no_auth_q) + + self._gen_val.verify_4xx_response(no_auth_q) + + invalid_token_q = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Mutate subscription with invalid credentials" + ) as check: + if invalid_token_q.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {invalid_token_q.status_code}", + query_timestamps=[invalid_token_q.request.timestamp], + ) + # Sanity check: confirm the subscription was not mutated by the faulty call: + self._sanity_check_sub_not_mutated(check, invalid_token_q) + + self._gen_val.verify_4xx_response(invalid_token_q) + + query_missing_scope = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check("Mutate subscription with missing scope") as check: + if query_missing_scope.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_missing_scope.status_code}", + details="Without the proper scope, the DSS should return a 401 without any data.", + query_timestamps=[query_missing_scope.request.timestamp], + ) + # Sanity check: confirm the subscription was not mutated by the faulty call: + self._sanity_check_sub_not_mutated(check, query_missing_scope) + + self._gen_val.verify_4xx_response(query_missing_scope) + + query_wrong_scope = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check("Mutate subscription with incorrect scope") as check: + if query_wrong_scope.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {query_wrong_scope.status_code}", + details="With an incorrect scope, the DSS should return a 403 without any data.", + query_timestamps=[query_wrong_scope.request.timestamp], + ) + # Sanity check: confirm the subscription was not mutated by the faulty call: + self._sanity_check_sub_not_mutated(check, query_wrong_scope) + + mutate_ok_q = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Mutate subscription with valid credentials" + ) as check: + if mutate_ok_q.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {mutate_ok_q.status_code}", + query_timestamps=[mutate_ok_q.request.timestamp], + ) + + self._current_sub = MutatedSubscription(mutate_ok_q).subscription + + def _verify_sub_deletion(self): + op = OPERATIONS[OperationID.DeleteSubscription] + + query_kwargs = dict( + verb=op.verb, + url=op.path.format( + subscriptionid=self._sub_params.sub_id, + version=self._current_sub.version, + ), + query_type=QueryType.F3548v21DSSDeleteSubscription, + participant_id=self._dss.participant_id, + ) + + query_no_auth = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Delete subscription with missing credentials" + ) as check: + if query_no_auth.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_no_auth.status_code}", + query_timestamps=[query_no_auth.request.timestamp], + ) + # Sanity check + self._sanity_check_sub_not_deleted(check, query_no_auth) + + self._gen_val.verify_4xx_response(query_no_auth) + + query_invalid_token = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Delete subscription with invalid credentials" + ) as check: + if query_invalid_token.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_invalid_token.status_code}", + query_timestamps=[query_invalid_token.request.timestamp], + ) + # Sanity check + self._sanity_check_sub_not_deleted(check, query_invalid_token) + + self._gen_val.verify_4xx_response(query_invalid_token) + + query_missing_scope = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check("Delete subscription with missing scope") as check: + if query_missing_scope.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_missing_scope.status_code}", + details="Without the proper scope, the DSS should return a 401 without any data.", + query_timestamps=[query_missing_scope.request.timestamp], + ) + # Sanity check + self._sanity_check_sub_not_deleted(check, query_missing_scope) + + self._gen_val.verify_4xx_response(query_missing_scope) + + query_wrong_scope = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check("Delete subscription with incorrect scope") as check: + if query_wrong_scope.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {query_wrong_scope.status_code}", + details="With an incorrect scope, the DSS should return a 403 without any data.", + query_timestamps=[query_wrong_scope.request.timestamp], + ) + # Sanity check + self._sanity_check_sub_not_deleted(check, query_wrong_scope) + + self._gen_val.verify_4xx_response(query_wrong_scope) + + query_ok = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Delete subscription with valid credentials" + ) as check: + if query_ok.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {query_ok.status_code}", + query_timestamps=[query_ok.request.timestamp], + ) + + # Confirm the subscription was deleted + not_found = self._dss.get_subscription(self._sub_params.sub_id) + with self._scenario.check( + "Delete subscription with valid credentials" + ) as check: + if not_found.status_code != 404: + check.record_failed( + summary=f"Expected 404, got {not_found.status_code}", + details="The subscription should have been deleted, as the deletion attempt was appropriately authenticated.", + query_timestamps=[ + query_ok.request.timestamp, + not_found.request.timestamp, + ], + ) + + def _verify_sub_search(self): + op = OPERATIONS[OperationID.QuerySubscriptions] + + query_kwargs = dict( + verb=op.verb, + url=op.path, + query_type=QueryType.F3548v21DSSQuerySubscriptions, + json=QuerySubscriptionParameters( + area_of_interest=self._planning_area_volume4d + ), + participant_id=self._dss.participant_id, + ) + + query_no_auth = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Search subscriptions with missing credentials" + ) as check: + if query_no_auth.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_no_auth.status_code}", + details="Without valid credentials, the DSS should return a 401 without any data.", + query_timestamps=[query_no_auth.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_no_auth) + + query_invalid_token = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Search subscriptions with invalid credentials" + ) as check: + if query_invalid_token.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_invalid_token.status_code}", + details="Without valid credentials, the DSS should return a 401 without any data.", + query_timestamps=[query_invalid_token.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_invalid_token) + + query_missing_scope = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check("Search subscriptions with missing scope") as check: + if query_missing_scope.status_code != 401: + check.record_failed( + summary=f"Expected 401, got {query_missing_scope.status_code}", + details="Without the proper scope, the DSS should return a 401 without any data.", + query_timestamps=[query_missing_scope.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_missing_scope) + + query_wrong_scope = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check("Search subscriptions with incorrect scope") as check: + if query_wrong_scope.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {query_wrong_scope.status_code}", + details="With an incorrect scope, the DSS should return a 403 without any data.", + query_timestamps=[query_wrong_scope.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_wrong_scope) + + query_ok = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Search subscriptions with valid credentials" + ) as check: + if query_ok.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {query_ok.status_code}", + query_timestamps=[query_ok.request.timestamp], + ) + + def _sanity_check_sub_not_created( + self, check: PendingCheck, creation_q: fetch.Query + ): + sanity_check = self._dss.get_subscription(self._sub_params.sub_id) + self._scenario.record_query(sanity_check) + if sanity_check.status_code != 404: + check.record_failed( + summary=f"Subscription was created by an unauthorized request.", + details="The subscription should not have been created, as the creation attempt was not authenticated.", + query_timestamps=[ + creation_q.request.timestamp, + sanity_check.request.timestamp, + ], + ) + self._gen_val.verify_4xx_response(sanity_check) + + def _sanity_check_sub_not_mutated( + self, check: PendingCheck, mutation_q: fetch.Query + ): + sanity_check = self._dss.get_subscription(self._sub_params.sub_id) + self._scenario.record_query(sanity_check) + if ( + abs( + sanity_check.subscription.time_end.value.datetime + - self._current_sub.time_end.value.datetime + ).total_seconds() + > TIME_TOLERANCE_SEC + ): + check.record_failed( + summary=f"Subscription was mutated by an unauthorized query.", + details="The subscription should not have been mutated, as the mutation attempt was not appropriately authenticated.", + query_timestamps=[ + mutation_q.request.timestamp, + sanity_check.request.timestamp, + ], + ) + + def _sanity_check_sub_not_deleted( + self, check: PendingCheck, deletion_q: fetch.Query + ): + sanity_check = self._dss.get_subscription(self._sub_params.sub_id) + self._scenario.record_query(sanity_check) + if sanity_check.status_code == 404: + check.record_failed( + summary=f"Unauthorized request could delete the subscription.", + details="The subscription should not have been deleted, as the deletion attempt was not authenticated.", + query_timestamps=[ + deletion_q.request.timestamp, + sanity_check.request.timestamp, + ], + ) + elif sanity_check.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {sanity_check.status_code}", + details="The subscription should not have been deleted, as the deletion attempt was not appropriately authenticated.", + query_timestamps=[ + deletion_q.request.timestamp, + sanity_check.request.timestamp, + ], + ) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication_validation.py index 140d511a20..c6affd219c 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication_validation.py @@ -1,21 +1,14 @@ from datetime import datetime, timedelta -from uas_standards.astm.f3548.v21.api import ( - OPERATIONS, - OperationID, - QuerySubscriptionParameters, -) from uas_standards.astm.f3548.v21.constants import ( Scope, ) from monitoring.monitorlib import fetch from monitoring.monitorlib.auth import InvalidTokenSignatureAuth -from monitoring.monitorlib.fetch import QueryType +from monitoring.monitorlib.fetch import QueryError from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import UTMClientSession -from monitoring.monitorlib.mutate import scd as mutate -from monitoring.monitorlib.mutate.scd import MutatedSubscription from monitoring.prober.infrastructure import register_resource_type from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstanceResource from monitoring.uss_qualifier.resources.astm.f3548.v21.planning_area import ( @@ -27,14 +20,17 @@ ) from monitoring.uss_qualifier.resources.interuss.id_generator import IDGeneratorResource from monitoring.uss_qualifier.scenarios.astm.utm.dss import test_step_fragments +from monitoring.uss_qualifier.scenarios.astm.utm.dss.authentication.generic import ( + GenericAuthValidator, +) +from monitoring.uss_qualifier.scenarios.astm.utm.dss.authentication.sub_api_validator import ( + SubscriptionAuthValidator, +) from monitoring.uss_qualifier.scenarios.scenario import ( TestScenario, - PendingCheck, ) from monitoring.uss_qualifier.suites.suite import ExecutionContext -TIME_TOLERANCE_SEC = 1 - class AuthenticationValidation(TestScenario): """ @@ -44,16 +40,14 @@ class AuthenticationValidation(TestScenario): is intended to cover DSS0210,A2-7-2,7 """ - SUB_TYPE = register_resource_type(380, "Subscription") + SUB_TYPE = register_resource_type( + 380, "Subscription, Operational Entity Id, Constraint" + ) - _sub_id: str - """Base identifier for the subscriptions that will be created""" - - _planning_area: PlanningAreaSpecification - - _planning_area_volume4d: Volume4D - - _sub_params: SubscriptionParams + # Reuse the same ID for every type of entity. + # As we are testing serially and cleaning up after each test, this should be fine. + _test_id: str + """Base identifier for the entities that will be created""" def __init__( self, @@ -73,7 +67,7 @@ def __init__( # This is an UTMClientSession self._dss = dss.get_instance(scopes) self._pid = [self._dss.participant_id] - self._sub_id = id_generator.id_factory.make_id(self.SUB_TYPE) + self._test_id = id_generator.id_factory.make_id(self.SUB_TYPE) self._planning_area = planning_area.specification # Build a ready-to-use 4D volume with no specified time for searching @@ -83,7 +77,7 @@ def __init__( ) self._sub_params = self._planning_area.get_new_subscription_params( - subscription_id=self._sub_id, # subscription ID will need to be overwritten + subscription_id=self._test_id, # Set this slightly in the past: we will update the subscriptions # to a later value that still needs to be roughly 'now' without getting into the future start_time=datetime.now().astimezone() - timedelta(seconds=10), @@ -101,17 +95,28 @@ def __init__( self._dss.base_url, auth_adapter=InvalidTokenSignatureAuth() ) + generic_validator = GenericAuthValidator(self, self._dss) + + self._sub_validator = SubscriptionAuthValidator( + self, + generic_validator, + self._dss, + self._test_id, + self._planning_area, + self._planning_area_volume4d, + self._no_auth_session, + self._invalid_token_session, + ) + def run(self, context: ExecutionContext): self.begin_test_scenario(context) self._setup_case() self.begin_test_case("Endpoint authorization") self.begin_test_step("Subscription endpoints authentication") - self._verify_subscription_endpoints_authentication() + self._sub_validator.verify_sub_endpoints_authentication() self.end_test_step() - # TODO consider adding test cases for: - # - valid credentials without the required scopes self.end_test_case() self.end_test_scenario() @@ -124,14 +129,14 @@ def _setup_case(self): def _ensure_clean_workspace_step(self): self.begin_test_step("Ensure clean workspace") - # Start by dropping any active sub - self._ensure_no_active_subs_exist() # Check for subscriptions that will collide with our IDs and drop them - self._ensure_test_sub_ids_do_not_exist() + self._ensure_test_entities_dont_exist() + # Drop any active remaining sub + self._ensure_no_active_subs_exist() self.end_test_step() - def _ensure_test_sub_ids_do_not_exist(self): - test_step_fragments.cleanup_sub(self, self._dss, self._sub_id) + def _ensure_test_entities_dont_exist(self): + test_step_fragments.cleanup_sub(self, self._dss, self._test_id) def _ensure_no_active_subs_exist(self): test_step_fragments.cleanup_active_subs( @@ -140,541 +145,7 @@ def _ensure_no_active_subs_exist(self): self._planning_area_volume4d, ) - def _verify_error_response(self, q: fetch.Query): - """Verifies that the passed query response's body is a valid ErrorResponse: - it is either empty or contains a single 'message' field, as per the OpenAPI spec. - """ - with self.check( - "Unauthorized requests return the proper error message body" - ) as check: - if len(q.response.json) == 0: - return - elif len(q.response.json) == 1 and "message" in q.response.json: - return - else: - check.record_failed( - summary="Unexpected error response body", - details=f"Response body for {q.request.method} query to {q.request.url} should be empty or contain a single 'message' field. Was: {q.response.json}", - query_timestamps=[q.request.timestamp], - ) - - def _verify_subscription_endpoints_authentication(self): - self._verify_sub_creation() - self._verify_sub_get() - self._verify_sub_mutation() - self._verify_sub_deletion() - self._verify_search() - - def _verify_sub_creation(self): - # Subscription creation request: - sub_params = dict(**self._sub_params) - op = OPERATIONS[OperationID.CreateSubscription] - del sub_params["sub_id"] - query_kwargs = dict( - verb=op.verb, - url=op.path.format(subscriptionid=self._sub_params.sub_id), - json=mutate.build_upsert_subscription_params(**sub_params), - query_type=QueryType.F3548v21DSSCreateSubscription, - participant_id=self._dss.participant_id, - ) - - # No auth: - no_auth_q = fetch.query_and_describe( - client=self._no_auth_session, **query_kwargs - ) - self.record_query(no_auth_q) - - with self.check("Create subscription with missing credentials") as check: - if no_auth_q.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {no_auth_q.status_code}", - query_timestamps=[no_auth_q.request.timestamp], - ) - - self._sanity_check_not_created(check, no_auth_q) - - self._verify_error_response(no_auth_q) - - # Bad token signature: - invalid_token_q = fetch.query_and_describe( - client=self._invalid_token_session, - scope=Scope.StrategicCoordination, - **query_kwargs, - ) - self.record_query(invalid_token_q) - - with self.check("Create subscription with invalid credentials") as check: - if invalid_token_q.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {invalid_token_q.status_code}", - query_timestamps=[invalid_token_q.request.timestamp], - ) - - self._sanity_check_not_created(check, invalid_token_q) - - self._verify_error_response(invalid_token_q) - - # Valid credentials but missing scope: - no_scope_q = fetch.query_and_describe( - client=self._dss.client, - **query_kwargs, - ) - self.record_query(no_scope_q) - - with self.check("Create subscription with missing scope") as check: - if no_scope_q.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {no_scope_q.status_code}", - query_timestamps=[no_scope_q.request.timestamp], - ) - - self._sanity_check_not_created(check, no_scope_q) - - # Valid credentials but wrong scope: - wrong_scope_q = fetch.query_and_describe( - client=self._dss.client, - scope=Scope.AvailabilityArbitration, # This scope should not be allowed to create subscriptions - **query_kwargs, - ) - self.record_query(wrong_scope_q) - - with self.check("Create subscription with incorrect scope") as check: - if wrong_scope_q.status_code != 403: - check.record_failed( - summary=f"Expected 403, got {wrong_scope_q.status_code}", - details="With an incorrect scope, the DSS should return a 403 without any data.", - query_timestamps=[wrong_scope_q.request.timestamp], - ) - - self._sanity_check_not_created(check, wrong_scope_q) - - # Correct token: - # - confirms that the request would otherwise work - # - makes a subscription available for read/mutation tests - create_ok_q = fetch.query_and_describe( - client=self._dss.client, scope=Scope.StrategicCoordination, **query_kwargs - ) - self.record_query(create_ok_q) - - with self.check("Create subscription with valid credentials") as check: - if create_ok_q.status_code != 200: - check.record_failed( - summary=f"Expected 200, got {create_ok_q.status_code}", - query_timestamps=[create_ok_q.request.timestamp], - ) - - # Store the subscription - self._current_sub = MutatedSubscription(create_ok_q).subscription - - def _verify_sub_get(self): - op = OPERATIONS[OperationID.GetSubscription] - - query_kwargs = dict( - verb=op.verb, - url=op.path.format(subscriptionid=self._sub_params.sub_id), - query_type=QueryType.F3548v21DSSGetSubscription, - participant_id=self._dss.participant_id, - ) - - query_no_auth = fetch.query_and_describe( - client=self._no_auth_session, **query_kwargs - ) - self.record_query(query_no_auth) - - with self.check("Get subscription with missing credentials") as check: - if query_no_auth.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_no_auth.status_code}", - query_timestamps=[query_no_auth.request.timestamp], - ) - - self._verify_error_response(query_no_auth) - - query_invalid_token = fetch.query_and_describe( - client=self._invalid_token_session, - scope=Scope.StrategicCoordination, - **query_kwargs, - ) - self.record_query(query_invalid_token) - - with self.check("Get subscription with invalid credentials") as check: - if query_invalid_token.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_invalid_token.status_code}", - query_timestamps=[query_invalid_token.request.timestamp], - ) - - self._verify_error_response(query_invalid_token) - - query_ok = fetch.query_and_describe( - client=self._dss.client, scope=Scope.StrategicCoordination, **query_kwargs - ) - self.record_query(query_ok) - - with self.check("Get subscription with valid credentials") as check: - if query_ok.status_code != 200: - check.record_failed( - summary=f"Expected 200, got {query_ok.status_code}", - query_timestamps=[query_ok.request.timestamp], - ) - - def _verify_sub_mutation(self): - # Subscription creation request: - new_params = self._sub_params.copy() - new_params.end_time = new_params.end_time - timedelta(seconds=10) - pld_params = dict(**new_params) - op = OPERATIONS[OperationID.UpdateSubscription] - del pld_params["sub_id"] - - query_kwargs = dict( - verb=op.verb, - url=op.path.format( - subscriptionid=self._sub_params.sub_id, - version=self._current_sub.version, - ), - json=mutate.build_upsert_subscription_params(**pld_params), - query_type=QueryType.F3548v21DSSCreateSubscription, - participant_id=self._dss.participant_id, - ) - - no_auth_q = fetch.query_and_describe( - client=self._no_auth_session, **query_kwargs - ) - self.record_query(no_auth_q) - - with self.check("Mutate subscription with missing credentials") as check: - if no_auth_q.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {no_auth_q.status_code}", - query_timestamps=[no_auth_q.request.timestamp], - ) - # Sanity check: confirm the subscription was not mutated by the faulty call: - self._sanity_check_not_mutated(check, no_auth_q) - - self._verify_error_response(no_auth_q) - - invalid_token_q = fetch.query_and_describe( - client=self._invalid_token_session, - scope=Scope.StrategicCoordination, - **query_kwargs, - ) - self.record_query(invalid_token_q) - - with self.check("Mutate subscription with invalid credentials") as check: - if invalid_token_q.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {invalid_token_q.status_code}", - query_timestamps=[invalid_token_q.request.timestamp], - ) - # Sanity check: confirm the subscription was not mutated by the faulty call: - self._sanity_check_not_mutated(check, invalid_token_q) - - self._verify_error_response(invalid_token_q) - - query_missing_scope = fetch.query_and_describe( - client=self._dss.client, - **query_kwargs, - ) - self.record_query(query_missing_scope) - - with self.check("Mutate subscription with missing scope") as check: - if query_missing_scope.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_missing_scope.status_code}", - details="Without the proper scope, the DSS should return a 401 without any data.", - query_timestamps=[query_missing_scope.request.timestamp], - ) - # Sanity check: confirm the subscription was not mutated by the faulty call: - self._sanity_check_not_mutated(check, query_missing_scope) - - self._verify_error_response(query_missing_scope) - - query_wrong_scope = fetch.query_and_describe( - client=self._dss.client, - scope=Scope.AvailabilityArbitration, - **query_kwargs, - ) - self.record_query(query_wrong_scope) - - with self.check("Mutate subscription with incorrect scope") as check: - if query_wrong_scope.status_code != 403: - check.record_failed( - summary=f"Expected 403, got {query_wrong_scope.status_code}", - details="With an incorrect scope, the DSS should return a 403 without any data.", - query_timestamps=[query_wrong_scope.request.timestamp], - ) - # Sanity check: confirm the subscription was not mutated by the faulty call: - self._sanity_check_not_mutated(check, query_wrong_scope) - - mutate_ok_q = fetch.query_and_describe( - client=self._dss.client, scope=Scope.StrategicCoordination, **query_kwargs - ) - self.record_query(mutate_ok_q) - - with self.check("Mutate subscription with valid credentials") as check: - if mutate_ok_q.status_code != 200: - check.record_failed( - summary=f"Expected 200, got {mutate_ok_q.status_code}", - query_timestamps=[mutate_ok_q.request.timestamp], - ) - - self._current_sub = MutatedSubscription(mutate_ok_q).subscription - - def _verify_sub_deletion(self): - op = OPERATIONS[OperationID.DeleteSubscription] - - query_kwargs = dict( - verb=op.verb, - url=op.path.format( - subscriptionid=self._sub_params.sub_id, - version=self._current_sub.version, - ), - query_type=QueryType.F3548v21DSSDeleteSubscription, - participant_id=self._dss.participant_id, - ) - - query_no_auth = fetch.query_and_describe( - client=self._no_auth_session, **query_kwargs - ) - self.record_query(query_no_auth) - - with self.check("Delete subscription with missing credentials") as check: - if query_no_auth.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_no_auth.status_code}", - query_timestamps=[query_no_auth.request.timestamp], - ) - # Sanity check - self._sanity_check_not_deleted(check, query_no_auth) - - self._verify_error_response(query_no_auth) - - query_invalid_token = fetch.query_and_describe( - client=self._invalid_token_session, - scope=Scope.StrategicCoordination, - **query_kwargs, - ) - self.record_query(query_invalid_token) - - with self.check("Delete subscription with invalid credentials") as check: - if query_invalid_token.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_invalid_token.status_code}", - query_timestamps=[query_invalid_token.request.timestamp], - ) - # Sanity check - self._sanity_check_not_deleted(check, query_invalid_token) - - self._verify_error_response(query_invalid_token) - - query_missing_scope = fetch.query_and_describe( - client=self._dss.client, - **query_kwargs, - ) - self.record_query(query_missing_scope) - - with self.check("Delete subscription with missing scope") as check: - if query_missing_scope.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_missing_scope.status_code}", - details="Without the proper scope, the DSS should return a 401 without any data.", - query_timestamps=[query_missing_scope.request.timestamp], - ) - # Sanity check - self._sanity_check_not_deleted(check, query_missing_scope) - - self._verify_error_response(query_missing_scope) - - query_wrong_scope = fetch.query_and_describe( - client=self._dss.client, - scope=Scope.AvailabilityArbitration, - **query_kwargs, - ) - self.record_query(query_wrong_scope) - - with self.check("Delete subscription with incorrect scope") as check: - if query_wrong_scope.status_code != 403: - check.record_failed( - summary=f"Expected 403, got {query_wrong_scope.status_code}", - details="With an incorrect scope, the DSS should return a 403 without any data.", - query_timestamps=[query_wrong_scope.request.timestamp], - ) - # Sanity check - self._sanity_check_not_deleted(check, query_wrong_scope) - - self._verify_error_response(query_wrong_scope) - - query_ok = fetch.query_and_describe( - client=self._dss.client, scope=Scope.StrategicCoordination, **query_kwargs - ) - self.record_query(query_ok) - - with self.check("Delete subscription with valid credentials") as check: - if query_ok.status_code != 200: - check.record_failed( - summary=f"Expected 200, got {query_ok.status_code}", - query_timestamps=[query_ok.request.timestamp], - ) - - # Confirm the subscription was deleted - not_found = self._dss.get_subscription(self._sub_params.sub_id) - with self.check("Delete subscription with valid credentials") as check: - if not_found.status_code != 404: - check.record_failed( - summary=f"Expected 404, got {not_found.status_code}", - details="The subscription should have been deleted, as the deletion attempt was appropriately authenticated.", - query_timestamps=[ - query_ok.request.timestamp, - not_found.request.timestamp, - ], - ) - - def _verify_search(self): - op = OPERATIONS[OperationID.QuerySubscriptions] - - query_kwargs = dict( - verb=op.verb, - url=op.path, - query_type=QueryType.F3548v21DSSQuerySubscriptions, - json=QuerySubscriptionParameters( - area_of_interest=self._planning_area_volume4d - ), - participant_id=self._dss.participant_id, - ) - - query_no_auth = fetch.query_and_describe( - client=self._no_auth_session, **query_kwargs - ) - self.record_query(query_no_auth) - - with self.check("Search subscriptions with missing credentials") as check: - if query_no_auth.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_no_auth.status_code}", - details="Without valid credentials, the DSS should return a 401 without any data.", - query_timestamps=[query_no_auth.request.timestamp], - ) - - self._verify_error_response(query_no_auth) - - query_invalid_token = fetch.query_and_describe( - client=self._invalid_token_session, - scope=Scope.StrategicCoordination, - **query_kwargs, - ) - self.record_query(query_invalid_token) - - with self.check("Search subscriptions with invalid credentials") as check: - if query_invalid_token.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_invalid_token.status_code}", - details="Without valid credentials, the DSS should return a 401 without any data.", - query_timestamps=[query_invalid_token.request.timestamp], - ) - - self._verify_error_response(query_invalid_token) - - query_missing_scope = fetch.query_and_describe( - client=self._dss.client, - **query_kwargs, - ) - self.record_query(query_missing_scope) - - with self.check("Search subscriptions with missing scope") as check: - if query_missing_scope.status_code != 401: - check.record_failed( - summary=f"Expected 401, got {query_missing_scope.status_code}", - details="Without the proper scope, the DSS should return a 401 without any data.", - query_timestamps=[query_missing_scope.request.timestamp], - ) - - self._verify_error_response(query_missing_scope) - - query_wrong_scope = fetch.query_and_describe( - client=self._dss.client, - scope=Scope.AvailabilityArbitration, - **query_kwargs, - ) - self.record_query(query_wrong_scope) - - with self.check("Search subscriptions with incorrect scope") as check: - if query_wrong_scope.status_code != 403: - check.record_failed( - summary=f"Expected 403, got {query_wrong_scope.status_code}", - details="With an incorrect scope, the DSS should return a 403 without any data.", - query_timestamps=[query_wrong_scope.request.timestamp], - ) - - self._verify_error_response(query_wrong_scope) - - query_ok = fetch.query_and_describe( - client=self._dss.client, scope=Scope.StrategicCoordination, **query_kwargs - ) - self.record_query(query_ok) - - with self.check("Search subscriptions with valid credentials") as check: - if query_ok.status_code != 200: - check.record_failed( - summary=f"Expected 200, got {query_ok.status_code}", - query_timestamps=[query_ok.request.timestamp], - ) - - def _sanity_check_not_created(self, check: PendingCheck, creation_q: fetch.Query): - sanity_check = self._dss.get_subscription(self._sub_params.sub_id) - self.record_query(sanity_check) - if sanity_check.status_code != 404: - check.record_failed( - summary=f"Subscription was created by an unauthorized request.", - details="The subscription should not have been created, as the creation attempt was not authenticated.", - query_timestamps=[ - creation_q.request.timestamp, - sanity_check.request.timestamp, - ], - ) - self._verify_error_response(sanity_check) - - def _sanity_check_not_mutated(self, check: PendingCheck, mutation_q: fetch.Query): - sanity_check = self._dss.get_subscription(self._sub_params.sub_id) - self.record_query(sanity_check) - if ( - abs( - sanity_check.subscription.time_end.value.datetime - - self._current_sub.time_end.value.datetime - ).total_seconds() - > TIME_TOLERANCE_SEC - ): - check.record_failed( - summary=f"Subscription was mutated by an unauthorized query.", - details="The subscription should not have been mutated, as the mutation attempt was not appropriately authenticated.", - query_timestamps=[ - mutation_q.request.timestamp, - sanity_check.request.timestamp, - ], - ) - - def _sanity_check_not_deleted(self, check: PendingCheck, deletion_q: fetch.Query): - sanity_check = self._dss.get_subscription(self._sub_params.sub_id) - self.record_query(sanity_check) - if sanity_check.status_code == 404: - check.record_failed( - summary=f"Unauthorized request could delete the subscription.", - details="The subscription should not have been deleted, as the deletion attempt was not authenticated.", - query_timestamps=[ - deletion_q.request.timestamp, - sanity_check.request.timestamp, - ], - ) - elif sanity_check.status_code != 200: - check.record_failed( - summary=f"Expected 200, got {sanity_check.status_code}", - details="The subscription should not have been deleted, as the deletion attempt was not appropriately authenticated.", - query_timestamps=[ - deletion_q.request.timestamp, - sanity_check.request.timestamp, - ], - ) - def cleanup(self): self.begin_cleanup() - self._ensure_test_sub_ids_do_not_exist() + self._ensure_test_entities_dont_exist() self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py index 1cf30767cf..c471774ba2 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/test_step_fragments.py @@ -51,7 +51,7 @@ def cleanup_sub( if existing_sub.status_code not in [200, 404]: check.record_failed( summary=f"Could not query subscription {sub_id}", - details=f"When attempting to query subscription {sub_id} from the DSS, received {existing_sub.status_code}", + details=f"When attempting to query subscription {sub_id} from the DSS, received {existing_sub.status_code} with body {existing_sub.response.json}", query_timestamps=[existing_sub.request.timestamp], ) @@ -64,7 +64,7 @@ def cleanup_sub( if deleted_sub.status_code != 200: check.record_failed( summary=f"Could not delete subscription {sub_id}", - details=f"When attempting to delete subscription {sub_id} from the DSS, received {deleted_sub.status_code}", + details=f"When attempting to delete subscription {sub_id} from the DSS, received {deleted_sub.status_code} with body {deleted_sub.response.json}", query_timestamps=[deleted_sub.request.timestamp], ) diff --git a/monitoring/uss_qualifier/scenarios/scenario.py b/monitoring/uss_qualifier/scenarios/scenario.py index feabe79001..d91f3cf80c 100644 --- a/monitoring/uss_qualifier/scenarios/scenario.py +++ b/monitoring/uss_qualifier/scenarios/scenario.py @@ -1,9 +1,10 @@ +import inspect import traceback from abc import ABC, abstractmethod from datetime import datetime from enum import Enum -import inspect from typing import Callable, Dict, List, Optional, TypeVar, Union, Set, Type + import arrow from implicitdict import StringBasedDateTime from loguru import logger @@ -25,6 +26,7 @@ ParticipantID, PassedCheck, ) +from monitoring.uss_qualifier.resources.definitions import ResourceTypeName, ResourceID from monitoring.uss_qualifier.resources.resource import MissingResourceError from monitoring.uss_qualifier.scenarios.definitions import ( TestScenarioDeclaration, @@ -36,7 +38,6 @@ TestStepDocumentation, TestCheckDocumentation, ) -from monitoring.uss_qualifier.resources.definitions import ResourceTypeName, ResourceID from monitoring.uss_qualifier.scenarios.documentation.parsing import get_documentation SQUELCH_WARN_ON_QUERY_TYPE = [