From fc6b021b145d87682c8ab27538e7f547e0e9a612 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 --- monitoring/prober/infrastructure.py | 2 +- .../authentication_validation.md | 132 +++++ .../authentication_validation.py | 37 +- .../dss/authentication/oir_api_validator.py | 518 ++++++++++++++++++ .../astm/utm/dss/test_step_fragments.py | 4 +- .../uss_qualifier/scenarios/scenario.py | 5 +- 6 files changed, 692 insertions(+), 6 deletions(-) create mode 100644 monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/oir_api_validator.py diff --git a/monitoring/prober/infrastructure.py b/monitoring/prober/infrastructure.py index 95e3aac299..1aadafa3b6 100644 --- a/monitoring/prober/infrastructure.py +++ b/monitoring/prober/infrastructure.py @@ -100,7 +100,7 @@ def wrapper_default_scope(*args, **kwargs): resource_type_code_descriptions: Dict[ResourceType, str] = {} -# Next code: 381 +# Next code: 382 def register_resource_type(code: int, description: str) -> ResourceType: """Register that the specified code refers to the described resource. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.md index 387760901b..1662ae934b 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.md @@ -173,6 +173,138 @@ it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../require If the DSS does not allow searching for subscriptions when valid credentials are presented, it is in violation of **[astm.f3548.v21.DSS0005,5](../../../../../requirements/astm/f3548/v21.md)**. +### Operational intents endpoints authentication test step + +#### 🛑 Unauthorized requests return the proper error message body check + +If the DSS under test does not return a proper error message body when an unauthorized request is received, +it fails to properly implement the OpenAPI specification that is part of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Create operational intent reference with missing credentials check + +If the DSS under test allows the creation of an operational intent without any credentials being presented, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Create operational intent reference with invalid credentials check + +If the DSS under test allows the creation of an operational intent with credentials that are well-formed but invalid, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Create operational intent reference with missing scope check + +If the DSS under test allows the creation of an operational intent with valid credentials but a missing scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Create operational intent reference with incorrect scope check + +If the DSS under test allows the creation of an operational intent with valid credentials but an incorrect scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Create operational intent reference with valid credentials check + +If the DSS does not allow the creation of an operational intent when valid credentials are presented, +it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Get operational intent reference with missing credentials check + +If the DSS under test allows the fetching of an operational intent without any credentials being presented, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Get operational intent reference with invalid credentials check + +If the DSS under test allows the fetching of an operational intent with credentials that are well-formed but invalid, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Get operational intent reference with missing scope check + +If the DSS under test allows the fetching of an operational intent with valid credentials but a missing scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Get operational intent reference with incorrect scope check + +If the DSS under test allows the fetching of an operational intent with valid credentials but an incorrect scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Get operational intent reference with valid credentials check + +If the DSS does not allow fetching an operational intent when valid credentials are presented, +it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Mutate operational intent reference with missing credentials check + +If the DSS under test allows the mutation of an operational intent without any credentials being presented, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Mutate operational intent reference with invalid credentials check + +If the DSS under test allows the mutation of an operational intent with credentials that are well-formed but invalid, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Mutate operational intent reference with missing scope check + +If the DSS under test allows the mutation of an operational intent with valid credentials but a missing scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Mutate operational intent reference with incorrect scope check + +If the DSS under test allows the mutation of an operational intent with valid credentials but an incorrect scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Mutate operational intent reference with valid credentials check + +If the DSS does not allow the mutation of an operational intent when valid credentials are presented, +it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Delete operational intent reference with missing credentials check + +If the DSS under test allows the deletion of an operational intent without any credentials being presented, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Delete operational intent reference with invalid credentials check + +If the DSS under test allows the deletion of an operational intent with credentials that are well-formed but invalid, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Delete operational intent reference with missing scope check + +If the DSS under test allows the deletion of an operational intent with valid credentials but a missing scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Delete operational intent reference with incorrect scope check + +If the DSS under test allows the deletion of an operational intent with valid credentials but an incorrect scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Delete operational intent reference with valid credentials check + +If the DSS does not allow the deletion of an operational intent when valid credentials are presented, +it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Search operational intent references with missing credentials check + +If the DSS under test allows searching for operational intents without any credentials being presented, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Search operational intent references with invalid credentials check + +If the DSS under test allows searching for operational intents with credentials that are well-formed but invalid, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Search operational intent references with missing scope check + +If the DSS under test allows searching for operational intents with valid credentials but a missing scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Search operational intent references with incorrect scope check + +If the DSS under test allows searching for operational intents with valid credentials but an incorrect scope, +it is in violation of **[astm.f3548.v21.DSS0210,A2-7-2,7](../../../../../requirements/astm/f3548/v21.md)**. + +#### 🛑 Search operational intent references with valid credentials check + +If the DSS does not allow searching for operational intents when valid credentials are presented, +it is in violation of **[astm.f3548.v21.DSS0005,1](../../../../../requirements/astm/f3548/v21.md)**. + ## [Cleanup](../clean_workspace.md) The cleanup phase of this test scenario removes the subscription with the known test ID if it has not been removed before. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.py index 6891720f67..878d4ee16c 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/authentication_validation.py @@ -6,6 +6,7 @@ ) from monitoring.monitorlib.auth import InvalidTokenSignatureAuth +from monitoring.monitorlib.fetch import QueryError from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import UTMClientSession from monitoring.prober.infrastructure import register_resource_type @@ -18,6 +19,8 @@ from monitoring.uss_qualifier.scenarios.astm.utm.dss.authentication.generic import ( GenericAuthValidator, ) +from monitoring.uss_qualifier.scenarios.astm.utm.dss.authentication.oir_api_validator import \ + OperationalIntentRefAuthValidator from monitoring.uss_qualifier.scenarios.astm.utm.dss.authentication.sub_api_validator import ( SubscriptionAuthValidator, ) @@ -37,7 +40,7 @@ class AuthenticationValidation(TestScenario): """ SUB_TYPE = register_resource_type( - 380, "Subscription, Operational Entity Id, Constraint" + 381, "Subscription, Operational Entity Id, Constraint" ) # Reuse the same ID for every type of entity. @@ -126,6 +129,17 @@ def __init__( test_missing_scope=self._test_missing_scope, ) + self._oir_validator = OperationalIntentRefAuthValidator( + scenario=self, + generic_validator=generic_validator, + dss=self._dss, + test_id=self._test_id, + planning_area=self._planning_area, + planning_area_volume4d=self._planning_area_volume4d, + no_auth_session=self._no_auth_session, + invalid_token_session=self._invalid_token_session, + ) + def run(self, context: ExecutionContext): self.begin_test_scenario(context) self._setup_case() @@ -146,8 +160,15 @@ def run(self, context: ExecutionContext): self.begin_test_step("Subscription endpoints authentication") self._sub_validator.verify_sub_endpoints_authentication() + self.end_test_step() + self.begin_test_step("Operational intents endpoints authentication") + self._oir_validator.verify_oir_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() @@ -167,6 +188,20 @@ def _ensure_clean_workspace_step(self): self.end_test_step() def _ensure_test_entities_dont_exist(self): + + # Drop OIR's first: subscriptions may be tied to them and can't be deleted + # as long as they exist + # TODO migrate the below to use existing test fragments or add it to them + try: + oir, q = self._dss.get_op_intent_reference(self._test_id) + self.record_query(q) + self._dss.delete_op_intent(oir.id, oir.ovn) + except QueryError as qe: + if qe.queries[0].response.status_code == 404: + pass # All is good + else: + raise qe + # TODO confirm this is the way we want to be handling expected 4XX errors test_step_fragments.cleanup_sub(self, self._dss, self._test_id) def _ensure_no_active_subs_exist(self): diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/oir_api_validator.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/oir_api_validator.py new file mode 100644 index 0000000000..cff1bc084e --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/authentication/oir_api_validator.py @@ -0,0 +1,518 @@ +from datetime import datetime, timedelta +from typing import Optional + +from implicitdict import ImplicitDict, StringBasedDateTime +from uas_standards.astm.f3548.v21.api import ( + OPERATIONS, + OperationID, + OperationalIntentState, + ChangeOperationalIntentReferenceResponse, + PutOperationalIntentReferenceParameters, + Time, + QueryOperationalIntentReferenceParameters, +) + +from monitoring.monitorlib import fetch +from monitoring.monitorlib.fetch import QueryType, QueryError +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.infrastructure import UTMClientSession +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 OperationalIntentRefAuthValidator: + 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, + test_wrong_scope: Optional[str] = None, + test_missing_scope: bool = False, + ): + 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 + + time_start = datetime.now().astimezone() - timedelta(seconds=10) + time_end = time_start + timedelta(minutes=20) + + self._oir_params = planning_area.get_new_operational_intent_ref_params( + key=[], # we expect the area to have been emptied + state=OperationalIntentState.Accepted, + uss_base_url=planning_area.base_url, + time_start=time_start, + time_end=time_end, + subscription_id=None, # We provide no subscription for this intent + implicit_sub_base_url=None, # we don't need an implicit subscription + ) + self._planning_area_volume4d = planning_area_volume4d + self._no_auth_session = no_auth_session + self._invalid_token_session = invalid_token_session + + self._test_wrong_scope = test_wrong_scope + self._test_missing_scope = test_missing_scope + + def verify_oir_endpoints_authentication(self): + self._verify_oir_creation() + self._verify_oir_get() + self._verify_oir_mutation() + self._verify_oir_deletion() + self._verify_oir_search() + + def _verify_oir_creation(self): + op = OPERATIONS[OperationID.CreateOperationalIntentReference] + query_kwargs = dict( + verb=op.verb, + url=op.path.format(entityid=self._test_id), + json=self._oir_params, + query_type=QueryType.F3548v21DSSCreateOperationalIntentReference, + participant_id=self._dss.participant_id, + ) + + # No auth + no_auth_q = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Create operational intent reference with missing credentials", self._pid + ) 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_oir_not_created(check, no_auth_q) + + self._gen_val.verify_4xx_response(no_auth_q) + + # Invalid token + invalid_token_q = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Create operational intent reference with invalid credentials", self._pid + ) 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_oir_not_created(check, invalid_token_q) + + self._gen_val.verify_4xx_response(invalid_token_q) + + # Valid credentials but missing scope: + if self._test_missing_scope: + no_scope_q = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check( + "Create operational intent reference with missing scope", self._pid + ) as check: + if no_scope_q.status_code != 401: + check.record_failed( + summary=f"Expected 403, got {no_scope_q.status_code}", + query_timestamps=[no_scope_q.request.timestamp], + ) + self._sanity_check_oir_not_created(check, no_scope_q) + + self._gen_val.verify_4xx_response(no_scope_q) + + # Valid credentials but wrong scope: + if self._test_wrong_scope: + wrong_scope_q = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check( + "Create operational intent reference with incorrect scope", self._pid + ) as check: + if wrong_scope_q.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {wrong_scope_q.status_code}", + query_timestamps=[wrong_scope_q.request.timestamp], + ) + self._sanity_check_oir_not_created(check, wrong_scope_q) + + self._gen_val.verify_4xx_response(wrong_scope_q) + + # Valid credentials + valid_q = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Create operational intent reference with valid credentials", self._pid + ) as check: + if valid_q.status_code != 201: # As specified in OpenAPI spec + check.record_failed( + summary=f"Expected 200, got {valid_q.status_code}", + query_timestamps=[valid_q.request.timestamp], + ) + + oir_resp = ImplicitDict.parse( + valid_q.response.json, ChangeOperationalIntentReferenceResponse + ) + + # Save the current OIR + self._current_oir = oir_resp.operational_intent_reference + + def _verify_oir_get(self): + op = OPERATIONS[OperationID.GetOperationalIntentReference] + query_kwargs = dict( + verb=op.verb, + url=op.path.format(entityid=self._test_id), + query_type=QueryType.F3548v21DSSGetOperationalIntentReference, + participant_id=self._dss.participant_id, + ) + + # No Auth + query_no_auth = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Get operational intent reference with missing credentials", self._pid + ) 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) + + # Invalid token + query_invalid_token = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Get operational intent reference with invalid credentials", self._pid + ) 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) + + # Valid credentials but missing scope + if self._test_missing_scope: + query_missing_scope = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check( + "Get operational intent reference with missing scope", self._pid + ) as check: + if query_missing_scope.status_code != 401: + check.record_failed( + summary=f"Expected 403, got {query_missing_scope.status_code}", + query_timestamps=[query_missing_scope.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_missing_scope) + + # Valid credentials but wrong scope + if self._test_wrong_scope: + query_wrong_scope = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check( + "Get operational intent reference with incorrect scope", self._pid + ) as check: + if query_wrong_scope.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {query_wrong_scope.status_code}", + query_timestamps=[query_wrong_scope.request.timestamp], + ) + + self._gen_val.verify_4xx_response(query_wrong_scope) + + # Valid credentials + query_valid_auth = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Get operational intent reference with valid credentials", self._pid + ) as check: + if query_valid_auth.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {query_valid_auth.status_code}", + query_timestamps=[query_valid_auth.request.timestamp], + ) + + def _verify_oir_mutation(self): + op = OPERATIONS[OperationID.UpdateOperationalIntentReference] + new_params = PutOperationalIntentReferenceParameters(**self._oir_params) + updated_volume = new_params.extents[0] + new_end = updated_volume.time_end.value.datetime - timedelta(seconds=10) + updated_volume.time_end = Time(value=StringBasedDateTime(new_end)) + new_params.extents = [updated_volume] + # TODO This feels weird, but is required by the current version of the DSS -> confirm this is as expected + # (the spec does not mention that the entity being mutated may be excluded from the keys argument) + new_params.key = [self._current_oir.ovn] + query_kwargs = dict( + verb=op.verb, + url=op.path.format(entityid=self._test_id, ovn=self._current_oir.ovn), + json=new_params, + query_type=QueryType.F3548v21DSSUpdateOperationalIntentReference, + participant_id=self._dss.participant_id, + ) + + no_auth_q = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Mutate operational intent reference with missing credentials", self._pid + ) 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_oir_not_created(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 operational intent reference with invalid credentials", self._pid + ) 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_oir_not_updated(check, invalid_token_q) + + self._gen_val.verify_4xx_response(invalid_token_q) + + if self._test_missing_scope: + no_scope_q = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check( + "Mutate operational intent reference with missing scope", self._pid + ) as check: + if no_scope_q.status_code != 401: + check.record_failed( + summary=f"Expected 403, got {no_scope_q.status_code}", + query_timestamps=[no_scope_q.request.timestamp], + ) + self._sanity_check_oir_not_updated(check, no_scope_q) + + self._gen_val.verify_4xx_response(no_scope_q) + + if self._test_wrong_scope: + wrong_scope_q = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check( + "Mutate operational intent reference with incorrect scope", self._pid + ) as check: + if wrong_scope_q.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {wrong_scope_q.status_code}", + query_timestamps=[wrong_scope_q.request.timestamp], + ) + self._sanity_check_oir_not_updated(check, wrong_scope_q) + + self._gen_val.verify_4xx_response(wrong_scope_q) + + valid_q = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Mutate operational intent reference with valid credentials", self._pid + ) as check: + if valid_q.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {valid_q.status_code}", + details=f"Mutation is expected to have succeeded, but got status {valid_q.status_code} with body {valid_q.response.json} instead", + query_timestamps=[valid_q.request.timestamp], + ) + + parsed_oir = ImplicitDict.parse( + valid_q.response.json, ChangeOperationalIntentReferenceResponse + ) + self._current_oir = parsed_oir.operational_intent_reference + + def _verify_oir_deletion(self): + op = OPERATIONS[OperationID.DeleteOperationalIntentReference] + query_kwargs = dict( + verb=op.verb, + url=op.path.format(entityid=self._test_id, ovn=self._current_oir.ovn), + query_type=QueryType.F3548v21DSSDeleteOperationalIntentReference, + participant_id=self._dss.participant_id, + ) + + no_auth_q = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Delete operational intent reference with missing credentials", self._pid + ) 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._gen_val.verify_4xx_response(no_auth_q) + + invalid_token_q = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Delete operational intent reference with invalid credentials", self._pid + ) 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._gen_val.verify_4xx_response(invalid_token_q) + + if self._test_missing_scope: + no_scope_q = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check( + "Delete operational intent reference with missing scope", self._pid + ) as check: + if no_scope_q.status_code != 401: + check.record_failed( + summary=f"Expected 403, got {no_scope_q.status_code}", + query_timestamps=[no_scope_q.request.timestamp], + ) + self._gen_val.verify_4xx_response(no_scope_q) + + if self._test_wrong_scope: + wrong_scope_q = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check( + "Delete operational intent reference with incorrect scope", self._pid + ) as check: + if wrong_scope_q.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {wrong_scope_q.status_code}", + query_timestamps=[wrong_scope_q.request.timestamp], + ) + self._gen_val.verify_4xx_response(wrong_scope_q) + + valid_q = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Delete operational intent reference with valid credentials", self._pid + ) as check: + if valid_q.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {valid_q.status_code}", + query_timestamps=[valid_q.request.timestamp], + ) + + self._current_oir = None + + def _verify_oir_search(self): + op = OPERATIONS[OperationID.QueryOperationalIntentReferences] + query_kwargs = dict( + verb=op.verb, + url=op.path, + query_type=QueryType.F3548v21DSSQueryOperationalIntentReferences, + json=QueryOperationalIntentReferenceParameters( + area_of_interest=self._planning_area_volume4d.to_f3548v21() + ), + participant_id=self._dss.participant_id, + ) + + no_auth_q = self._gen_val.query_no_auth(**query_kwargs) + with self._scenario.check( + "Search operational intent references with missing credentials", + self._pid, + ) 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._gen_val.verify_4xx_response(no_auth_q) + + invalid_token_q = self._gen_val.query_invalid_token(**query_kwargs) + with self._scenario.check( + "Search operational intent references with invalid credentials", + self._pid, + ) 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._gen_val.verify_4xx_response(invalid_token_q) + + if self._test_missing_scope: + no_scope_q = self._gen_val.query_missing_scope(**query_kwargs) + with self._scenario.check( + "Search operational intent references with missing scope", self._pid + ) as check: + if no_scope_q.status_code != 401: + check.record_failed( + summary=f"Expected 403, got {no_scope_q.status_code}", + query_timestamps=[no_scope_q.request.timestamp], + ) + + self._gen_val.verify_4xx_response(no_scope_q) + + if self._test_wrong_scope: + wrong_scope_q = self._gen_val.query_wrong_scope(**query_kwargs) + with self._scenario.check( + "Search operational intent references with incorrect scope", self._pid + ) as check: + if wrong_scope_q.status_code != 403: + check.record_failed( + summary=f"Expected 403, got {wrong_scope_q.status_code}", + query_timestamps=[wrong_scope_q.request.timestamp], + ) + + self._gen_val.verify_4xx_response(wrong_scope_q) + + valid_q = self._gen_val.query_valid_auth(**query_kwargs) + with self._scenario.check( + "Search operational intent references with valid credentials", self._pid + ) as check: + if valid_q.status_code != 200: + check.record_failed( + summary=f"Expected 200, got {valid_q.status_code}", + query_timestamps=[valid_q.request.timestamp], + ) + + def _sanity_check_oir_not_created( + self, check: PendingCheck, creation_q: fetch.Query + ): + try: + _, sanity_check = self._dss.get_op_intent_reference(self._test_id) + self._scenario.record_query(sanity_check) + except QueryError as qe: + for q in qe.queries: + self._scenario.record_query(q) + if qe.queries[0].response.status_code == 404: + pass # All is good + else: + check.record_failed( + summary="OIR was created by an unauthorized request.", + details="The Operational Intent Reference should not have been created, as the creation attempt was not authenticated.", + query_timestamps=[ + creation_q.request.timestamp, + qe.queries[0].request.timestamp, + ], + ) + self._gen_val.verify_4xx_response(qe.queries[0]) + + def _sanity_check_oir_not_updated( + self, check: PendingCheck, creation_q: fetch.Query + ): + try: + oir, sanity_check = self._dss.get_op_intent_reference(self._test_id) + self._scenario.record_query(sanity_check) + if ( + abs( + oir.time_end.value.datetime + - self._current_oir.time_end.value.datetime + ).total_seconds() + > TIME_TOLERANCE_SEC + ): + check.record_failed( + summary="OIR was updated by an unauthorized request.", + details="The Operational Intent Reference should not have been updated, as the update attempt was not authenticated.", + query_timestamps=[ + creation_q.request.timestamp, + sanity_check.request.timestamp, + ], + ) + except QueryError as qe: + for q in qe.queries: + self._scenario.record_query(q) + check.record_failed( + summary="Could not fetch OIR to confirm it has not been mutated", + query_timestamps=[ + creation_q.request.timestamp, + qe.queries[0].request.timestamp, + ], + ) 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 = [