From 19788b20cbe6efa14cdb0bef20a0ca6afccbfa1f Mon Sep 17 00:00:00 2001 From: Julien Perrochet Date: Fri, 8 Dec 2023 09:01:58 +0100 Subject: [PATCH] OPIN0035 access control for op intents --- monitoring/prober/infrastructure.py | 2 +- .../dev/f3548_self_contained.yaml | 26 ++ .../resources/astm/f3548/v21/dss.py | 28 ++ .../flight_intents_resource.py | 60 ++- monitoring/uss_qualifier/run_locally.sh | 2 + .../scenarios/astm/utm/__init__.py | 1 + .../astm/utm/off_nominal_planning/down_uss.py | 45 +- .../astm/utm/op_intent_access_control.md | 102 +++++ .../astm/utm/op_intent_access_control.py | 413 ++++++++++++++++++ .../suites/astm/utm/dss_probing.md | 15 +- .../suites/astm/utm/dss_probing.yaml | 10 + .../uss_qualifier/suites/astm/utm/f3548_21.md | 9 +- .../suites/astm/utm/f3548_21.yaml | 8 + .../suites/faa/uft/message_signing.md | 9 +- .../suites/uspace/flight_auth.md | 9 +- .../suites/uspace/required_services.md | 9 +- 16 files changed, 700 insertions(+), 48 deletions(-) create mode 100644 monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.md create mode 100644 monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.py diff --git a/monitoring/prober/infrastructure.py b/monitoring/prober/infrastructure.py index bca87ef15d..5ee5067d3d 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: 375 +# Next code: 377 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/configurations/dev/f3548_self_contained.yaml b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml index 46eef3d548..df3d99dc50 100644 --- a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml +++ b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml @@ -13,6 +13,7 @@ v1: # Mapping of to resources: + id_generator: id_generator flight_planners: flight_planners conflicting_flights: conflicting_flights invalid_flight_intents: invalid_flight_intents @@ -20,6 +21,7 @@ v1: dss: dss dss_instances: dss_instances mock_uss: mock_uss + second_utm_auth: second_utm_auth # This block defines all the resources available in the resource pool. # Presumably all resources defined below would be used either @@ -35,6 +37,30 @@ v1: # To avoid putting secrets in configuration files, the auth spec (including sensitive information) will be read from the AUTH_SPEC environment variable environment_variable_containing_auth_spec: AUTH_SPEC + # Means by which uss_qualifier can discover which subscription ('sub' claim of its tokes) it is described by + utm_client_identity: + resource_type: resources.communications.ClientIdentityResource + dependencies: + auth_adapter: utm_auth + specification: + # Audience and scope to be used to issue a dummy query, should it be required to discover the subscription + whoami_audience: localhost + whoami_scope: rid.display_provider + + # Means by which uss_qualifier generates identifiers + id_generator: + $content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json + resource_type: resources.interuss.IDGeneratorResource + dependencies: + client_identity: utm_client_identity + + # A second auth adapter, for checks that require a second set of credentials for accessing the ecosystem. + # Note that the 'sub' claim of the tokens obtained through this adepter MUST be different from the first auth adapter. + second_utm_auth: + resource_type: resources.communications.AuthAdapterResource + specification: + environment_variable_containing_auth_spec: AUTH_SPEC_2 + # Set of USSs capable of being tested as flight planners flight_planners: resource_type: resources.flight_planning.FlightPlannersResource diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py index 7c151a924d..c9a147e2a3 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py @@ -28,6 +28,7 @@ SetUssAvailabilityStatusParameters, UssAvailabilityState, UssAvailabilityStatusResponse, + GetOperationalIntentReferenceResponse, ) @@ -90,6 +91,30 @@ def find_op_intent( ).operational_intent_references return result, query + def get_op_intent( + self, + op_intent_id: str, + ) -> Tuple[OperationalIntentReference, fetch.Query]: + """ + Retrieve an OP Intent from the DSS, using only its ID + """ + url = f"/dss/v1/operational_intent_references/{op_intent_id}" + query = fetch.query_and_describe( + self.client, + "GET", + url, + QueryType.F3548v21DSSGetOperationalIntentReference, + self.participant_id, + scope=SCOPE_SC, # + ) + if query.status_code != 200: + result = None + else: + result = ImplicitDict.parse( + query.response.json, GetOperationalIntentReferenceResponse + ).operational_intent_reference + return result, query + def get_full_op_intent( self, op_intent_ref: OperationalIntentReference, @@ -128,6 +153,9 @@ def put_op_intent( if id is None: url = f"/dss/v1/operational_intent_references/{str(uuid.uuid4())}" query_type = QueryType.F3548v21DSSCreateOperationalIntentReference + elif ovn is None: + url = f"/dss/v1/operational_intent_references/{id}" + query_type = QueryType.F3548v21DSSCreateOperationalIntentReference else: url = f"/dss/v1/operational_intent_references/{id}/{ovn}" query_type = QueryType.F3548v21DSSUpdateOperationalIntentReference diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py index bffc95ffbc..1d6b554dcf 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py @@ -1,9 +1,13 @@ -from typing import Dict +from typing import Dict, List +import arrow from implicitdict import ImplicitDict +from uas_standards.astm.f3548.v21.api import OperationalIntentState + from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( FlightInfoTemplate, ) +from monitoring.monitorlib.geotemporal import Volume4DCollection from monitoring.uss_qualifier.resources.files import load_dict from monitoring.uss_qualifier.resources.resource import Resource @@ -11,6 +15,7 @@ FlightIntentCollection, FlightIntentsSpecification, FlightIntentID, + FlightIntent, ) @@ -39,3 +44,56 @@ def __init__(self, specification: FlightIntentsSpecification): def get_flight_intents(self) -> Dict[FlightIntentID, FlightInfoTemplate]: return self._intent_collection.resolve() + + +def unpack_flight_intents( + flight_intents: FlightIntentsResource, flight_identifiers: List[str] +): + """ + Wraps some validation logic for flight intents resources + + Args: + flight_intents: the flight intents resources as configured and passed to a scenario + flight_identifiers: flight identifiers expected to be found in the flight_intent resource, + and for which a few sanity checks will be done. + + Returns: A tuple of the intents' extent and a dict of the flight intents + + """ + flight_intents = { + k: FlightIntent.from_flight_info_template(v) + for k, v in flight_intents.get_flight_intents().items() + } + + extents = [] + for intent in flight_intents.values(): + extents.extend(intent.request.operational_intent.volumes) + extents.extend(intent.request.operational_intent.off_nominal_volumes) + + intents_extent = Volume4DCollection.from_interuss_scd_api( + extents + ).bounding_volume.to_f3548v21() + + # Check that we have a least one intent active now + # TODO possibly this check should be moved to the scenarios that actually require this + now = arrow.utcnow().datetime + for intent_name, intent in flight_intents.items(): + if intent.request.operational_intent.state == OperationalIntentState.Activated: + assert Volume4DCollection.from_interuss_scd_api( + intent.request.operational_intent.volumes + + intent.request.operational_intent.off_nominal_volumes + ).has_active_volume( + now + ), f"at least one volume of activated intent {intent_name} must be active now (now is {now})" + + # Check that the flights we are interested in are in an accepted state + planned_flights = {} + for fi in flight_identifiers: + planned_flight = flight_intents[fi] + assert ( + planned_flight.request.operational_intent.state + == OperationalIntentState.Accepted + ), f"{fi} must have state Accepted" + planned_flights[fi] = planned_flight + + return intents_extent, planned_flights diff --git a/monitoring/uss_qualifier/run_locally.sh b/monitoring/uss_qualifier/run_locally.sh index d6efcc3721..124260731e 100755 --- a/monitoring/uss_qualifier/run_locally.sh +++ b/monitoring/uss_qualifier/run_locally.sh @@ -43,6 +43,7 @@ echo "Running configuration(s): ${CONFIG_NAME}" CONFIG_FLAG="--config ${CONFIG_NAME}" AUTH_SPEC='DummyOAuth(http://oauth.authority.localutm:8085/token,uss_qualifier)' +AUTH_SPEC_2='DummyOAuth(http://oauth.authority.localutm:8085/token,uss_qualifier_2)' QUALIFIER_OPTIONS="$CONFIG_FLAG $OTHER_ARGS" @@ -66,6 +67,7 @@ docker run ${docker_args} --name uss_qualifier \ -u "$(id -u):$(id -g)" \ -e PYTHONBUFFERED=1 \ -e AUTH_SPEC=${AUTH_SPEC} \ + -e AUTH_SPEC_2=${AUTH_SPEC_2} \ -e MONITORING_GITHUB_ROOT=${MONITORING_GITHUB_ROOT:-} \ -v "$(pwd)/$OUTPUT_DIR:/app/$OUTPUT_DIR" \ -v "$(pwd)/$CACHE_DIR:/app/$CACHE_DIR" \ diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/__init__.py b/monitoring/uss_qualifier/scenarios/astm/utm/__init__.py index e87ac2555b..56f6fdb3b8 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/__init__.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/__init__.py @@ -9,3 +9,4 @@ from .aggregate_checks import AggregateChecks from .prep_planners import PrepareFlightPlanners from .off_nominal_planning.down_uss import DownUSS +from .op_intent_access_control import OpIntentAccessControl diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py index 969b192ddf..0a50e4398e 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py @@ -17,6 +17,9 @@ from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( FlightIntent, ) +from monitoring.uss_qualifier.resources.flight_planning.flight_intents_resource import ( + unpack_flight_intents, +) from monitoring.uss_qualifier.resources.flight_planning.flight_planner import ( FlightPlanner, ) @@ -57,46 +60,16 @@ def __init__( self.tested_uss = tested_uss.flight_planner self.dss = dss.dss - _flight_intents = { - k: FlightIntent.from_flight_info_template(v) - for k, v in flight_intents.get_flight_intents().items() - } - - extents = [] - for intent in _flight_intents.values(): - extents.extend(intent.request.operational_intent.volumes) - extents.extend(intent.request.operational_intent.off_nominal_volumes) - self._intents_extent = Volume4DCollection.from_interuss_scd_api( - extents - ).bounding_volume.to_f3548v21() - try: - (self.flight1_planned, self.flight2_planned,) = ( - _flight_intents["flight1_planned"], - _flight_intents["flight2_planned"], + + (intents_extent, planned_flights) = unpack_flight_intents( + flight_intents, ["flight1_planned", "flight2_planned"] ) - now = arrow.utcnow().datetime - for intent_name, intent in _flight_intents.items(): - if ( - intent.request.operational_intent.state - == OperationalIntentState.Activated - ): - assert Volume4DCollection.from_interuss_scd_api( - intent.request.operational_intent.volumes - + intent.request.operational_intent.off_nominal_volumes - ).has_active_volume( - now - ), f"at least one volume of activated intent {intent_name} must be active now (now is {now})" + self.flight1_planned = planned_flights["flight1_planned"] + self.flight2_planned = planned_flights["flight2_planned"] - assert ( - self.flight1_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight1_planned must have state Accepted" - assert ( - self.flight2_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight2_planned must have state Accepted" + self._intents_extent = intents_extent # TODO: check that flight data is the same across the different versions of the flight diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.md b/monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.md new file mode 100644 index 0000000000..72e75679d1 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.md @@ -0,0 +1,102 @@ +# ASTM F3548-21 UTM DSS Operational Intent Access Control test scenario + +## Overview + +This scenario ensures that a DSS will only let the owner of an operational intent modify it. + +## Resources + +### flight_intents + +A resources.flight_planning.FlightIntentsResource containing the flight intents to be used in this scenario: + +This scenario expects to find at least two non-conflicting flight intents in this resource. + +### dss + +A resources.astm.f3548.v21.DSSInstanceResource containing the DSS instance to test for this scenario. + +### second_utm_auth + +A resources.communications.AuthAdapterResource containing a second set of credentials for interacting with the DSS. + +Note that the 'sub' claim on the token that will be obtained for this resource MUST be different from the 'sub' claim on the token for the dss resource. + +### id_generator + +A resources.ineruss.IDGeneratorResource that will be used to generate the ID of the operational intent to be + +## Setup test case + +Makes sure that the DSS is in a clean and expected state before running the test, and that the passed resources work as required. + +The setup will create two separate operational intents: one for each set of the available credentials. + +### Ensure clean workspace test step + +#### Operational intents can be queried directly by their ID check + +If an existing operational intent cannot directly be queried by its ID, the DSS implementation is in violation of +**[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. + +#### Operational intents can be searched using valid credentials check + +A client with valid credentials should be allowed to search for operational intents in a given area. +Otherwise, the DSS is not in compliance with **[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. + +#### Operational intents can be deleted by their owner check + +If an existing operational intent cannot be deleted when providing the proper ID and OVN, the DSS implementation is in violation of +**[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. + +### Create operational intents with different credentials test step + +This test step ensures that an operation intent created with the main credentials is available for the main test case. + +To ensure that the second credentials are valid, it will also create an operational intent with those credentials. + +#### Can create an operational intent with valid credentials check + +If the DSS does not allow the creation of operation intents when the required parameters and credentials are provided, +it is in violation of **[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. + +#### Passed sets of credentials are different check + +This scenario requires two sets of credentials that have a diffrent 'sub' claim in order to validate that the +DSS properly controls access to operational intents. + +## Attempt unauthorized flight intent modification test case + +This test case ensures that the DSS does not allow a caller to modify or delete operational intent that they did not create. + +### Attempt unauthorized flight intent modification test step + +This test step will attempt to modify the operational intent that was created using the configured `dss` resource, +using the credentials provided in the `second_utm_auth` resource, and expect all such attempts to fail. + +#### Operational intents can be queried directly by their ID check + +If an existing operational intent cannot directly be queried by its ID, the DSS implementation is in violation of +**[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. + +#### Second credentials cannot modify operational intent created with main credentials check + +If an operational intent can be modified by a client which did not create it, the DSS implementation is +in violation of **[astm.f3548.v21.OPIN0035](../../../requirements/astm/f3548/v21.md)**. + +#### Second credentials cannot delete operational intent created with main credentials check + +If an operational intent can be deleted by a client which did not create it, the DSS implementation is +in violation of **[astm.f3548.v21.OPIN0035](../../../requirements/astm/f3548/v21.md)**. + +## Cleanup + +### Operational intents can be queried directly by their ID check + +If an existing operational intent cannot directly be queried by its ID, the DSS implementation is in violation of +**[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. + +### Operational intents can be deleted by their owner check + +If an existing operational intent cannot be deleted when providing the proper ID and OVN, the DSS implementation is in violation of +**[astm.f3548.v21.DSS0005](../../../requirements/astm/f3548/v21.md)**. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.py b/monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.py new file mode 100644 index 0000000000..a357a07ed0 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/op_intent_access_control.py @@ -0,0 +1,413 @@ +from typing import Optional, List + +import loguru +from uas_standards.astm.f3548.v21.api import OperationalIntentState + +from monitoring.monitorlib.geotemporal import Volume4DCollection +from uas_standards.astm.f3548.v21 import api as f3548v21 +from monitoring.prober.infrastructure import register_resource_type +from monitoring.uss_qualifier.common_data_definitions import Severity +from monitoring.uss_qualifier.resources.astm.f3548.v21 import DSSInstanceResource +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance +from monitoring.uss_qualifier.resources.communications import AuthAdapterResource +from monitoring.uss_qualifier.resources.flight_planning import FlightIntentsResource +from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( + FlightIntent, +) +from monitoring.uss_qualifier.resources.flight_planning.flight_intents_resource import ( + unpack_flight_intents, +) +from monitoring.uss_qualifier.resources.interuss import IDGeneratorResource +from monitoring.uss_qualifier.scenarios.scenario import TestScenario +from monitoring.uss_qualifier.suites.suite import ExecutionContext + + +class OpIntentAccessControl(TestScenario): + """ + Tests that the DSS only allows a client to edit their own flight intents, but not those of another USS. + """ + + OP_INTENT_1 = register_resource_type(375, "Operational Intent") + OP_INTENT_2 = register_resource_type(376, "Operational Intent") + + # The DSS under test + _dss: DSSInstance + _pid: List[str] + + # The same DSS, available via a separate auth adapter + _dss_separate_creds: DSSInstance + + _flight1_planned: FlightIntent + _flight2_planned: FlightIntent + + _volumes1: Volume4DCollection + _volumes2: Volume4DCollection + + _intents_extent: f3548v21.Volume4D + + _current_ref_1: f3548v21.OperationalIntentReference + _current_ref_2: f3548v21.OperationalIntentReference + + def __init__( + self, + # TODO check if it's ok to request such a resource, or if we could just have a 4D volume configured? + # requiring the FlightIntentsResource means not needing to configure another resource. On the other hand we don't need most of what it contains. + # Also, this resources still requires from the implementation to know under which key the flight intents are stored. + flight_intents: FlightIntentsResource, + dss: DSSInstanceResource, + second_utm_auth: AuthAdapterResource, + id_generator: IDGeneratorResource, + ): + super().__init__() + self._dss = dss.dss + self._pid = [dss.dss.participant_id] + + self._oid_1 = id_generator.id_factory.make_id(self.OP_INTENT_1) + self._oid_2 = id_generator.id_factory.make_id(self.OP_INTENT_2) + + if second_utm_auth is not None: + # Build a second DSSWrapper identical to the first but with the other auth adapter + self._dss_separate_creds = DSSInstance( + participant_id=dss.dss.participant_id, + base_url=dss.dss.base_url, + has_private_address=dss.dss.has_private_address, + auth_adapter=second_utm_auth.adapter, + ) + + try: + (self._intents_extent, planned_flights) = unpack_flight_intents( + flight_intents, ["flight_1", "flight_2"] + ) + self._flight1_planned = planned_flights["flight_1"] + self._flight2_planned = planned_flights["flight_2"] + + self._volumes1 = Volume4DCollection.from_interuss_scd_api( + self._flight1_planned.request.operational_intent.volumes + ) + + self._volumes2 = Volume4DCollection.from_interuss_scd_api( + self._flight2_planned.request.operational_intent.volumes + ) + + except KeyError as e: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: missing flight intent {e}" + ) + except AssertionError as e: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" + ) + + def run(self, context: ExecutionContext): + self.begin_test_scenario(context) + self.begin_test_case("Setup") + + self.begin_test_step("Ensure clean workspace") + self._ensure_clean_workspace() + self.end_test_step() + + self.begin_test_step("Create operational intents with different credentials") + self._create_op_intents() + self._ensure_credentials_are_different() + self.end_test_step() + + self.end_test_case() + + self.begin_test_case("Attempt unauthorized flight intent modification") + self.begin_test_step("Attempt unauthorized flight intent modification") + + self._check_mutation_on_non_owned_intent_fails() + + self.end_test_step() + self.end_test_case() + + self.end_test_scenario() + + def _clean_known_op_intents_ids(self): + (oi_ref, q) = self._dss.get_op_intent(self._oid_1) + self.record_query(q) + with self.check( + "Operational intents can be queried directly by their ID", self._pid + ) as check: + # If the Op Intent does not exist, it's fine to run into a 404. + if q.response.status_code not in [200, 404]: + check.record_failed( + f"Could not access operational intent using main credentials", + Severity.High, + f"DSS responded with {q.response.status_code} to attempt to access OI {self._oid_1}", + query_timestamps=[q.request.timestamp], + ) + if q.response.status_code != 404: + # TODO handle notifications + (_, notifs, dq) = self._dss.delete_op_intent(self._oid_1, oi_ref.ovn) + self.record_query(dq) + if dq.response.status_code != 200: + with self.check( + "Operational intents can be deleted by their owner", self._pid + ) as check: + check.record_failed( + f"Could not delete operational intent using main credentials", + Severity.High, + f"DSS responded with {dq.response.status_code} to attempt to delete OI {self._oid_1}", + query_timestamps=[dq.request.timestamp], + ) + + (oi_ref, q) = self._dss_separate_creds.get_op_intent(self._oid_2) + self.record_query(q) + with self.check( + "Operational intents can be queried directly by their ID", self._pid + ) as check: + if q.response.status_code not in [200, 404]: + check.record_failed( + f"Could not access operational intent using second credentials", + Severity.High, + f"DSS responded with {q.response.status_code} to attempt to access OI {self._oid_2}", + query_timestamps=[q.request.timestamp], + ) + if q.response.status_code != 404: + # TODO handle notifications + (_, notifs, dq) = self._dss_separate_creds.delete_op_intent( + self._oid_2, oi_ref.ovn + ) + self.record_query(dq) + if dq.response.status_code != 200: + with self.check( + "Operational intents can be deleted by their owner", self._pid + ) as check: + check.record_failed( + f"Could not delete operational intent using second credentials", + Severity.High, + f"DSS responded with {dq.response.status_code} to attempt to delete OI {self._oid_2}", + query_timestamps=[dq.request.timestamp], + ) + + def _ensure_clean_workspace(self): + self._clean_known_op_intents_ids() + + # Also check for any potential other op_intents and delete them + (op_intents_1, q) = self._dss.find_op_intent(self._intents_extent) + self.record_query(q) + loguru.logger.info(f"Search query: {q.response}") + with self.check( + "Operational intents can be searched using valid credentials", self._pid + ) as check: + if q.response.status_code != 200: + check.record_failed( + f"Could not search operational intents using main credentials", + Severity.High, + f"DSS responded with {q.response.status_code} to attempt to search OIs", + query_timestamps=[q.request.timestamp], + ) + + for op_intent in op_intents_1: + # We look for an op_intent where the uss_qualifier is the manager; + if op_intent.manager == self._dss.client.auth_adapter.get_sub(): + # TODO handle notifications + (_, _, dq) = self._dss.delete_op_intent(op_intent.id, op_intent.ovn) + self.record_query(dq) + with self.check( + "Operational intents can be deleted by their owner", self._pid + ) as check: + if dq.response.status_code != 200: + check.record_failed( + f"Could not delete operational intent using main credentials", + Severity.High, + f"DSS responded with {dq.response.status_code} to attempt to delete OI {op_intent.id}", + query_timestamps=[dq.request.timestamp], + ) + + (op_intents_2, q) = self._dss_separate_creds.find_op_intent( + self._intents_extent + ) + self.record_query(q) + with self.check( + "Operational intents can be searched using valid credentials", self._pid + ) as check: + if q.response.status_code != 200: + check.record_failed( + f"Could not search operational intents using second credentials", + Severity.High, + f"DSS responded with {q.response.status_code} to attempt to search OIs", + query_timestamps=[q.request.timestamp], + ) + + for op_intent in op_intents_2: + # We look for an op_intent where the uss_qualifier is the manager; + if ( + op_intent.manager + == self._dss_separate_creds.client.auth_adapter.get_sub() + ): + # TODO handle notifications + (_, _, dq) = self._dss_separate_creds.delete_op_intent( + op_intent.id, op_intent.ovn + ) + self.record_query(dq) + with self.check( + "Operational intents can be deleted by their owner", self._pid + ) as check: + if dq.response.status_code != 200: + check.record_failed( + f"Could not delete operational intent using second credentials", + Severity.High, + f"DSS responded with {dq.response.status_code} to attempt to delete OI {op_intent.id}", + query_timestamps=[dq.request.timestamp], + ) + + def _create_op_intents(self): + (self._current_ref_1, subscribers1, q1) = self._dss.put_op_intent( + id=self._oid_1, + extents=self._volumes1.to_f3548v21(), + key=[], # we assume there is no other operational intent in that area + state=OperationalIntentState.Accepted, + base_url="https://fake.uss/down", # TODO Should we be publishing the URL of our mock USS here? + ) + self.record_query(q1) + + with self.check( + "Can create an operational intent with the main credentials", self._pid + ) as check: + if q1.response.status_code != 201: + check.record_failed( + f"Could not create operational intent using main credentials", + Severity.High, + f"DSS responded with {q1.response.status_code} to attempt to create OI {self._oid_1}", + query_timestamps=[q1.request.timestamp], + ) + + ( + self._current_ref_2, + subscribers2, + q2, + ) = self._dss_separate_creds.put_op_intent( + id=self._oid_2, + extents=self._volumes2.to_f3548v21(), + key=[ + self._current_ref_1.ovn + ], # we assume there is no other operational intent in that area + state=OperationalIntentState.Accepted, + base_url="https://fake.uss/down", # TODO Should we be publishing the URL of our mock USS here? + ) + self.record_query(q2) + with self.check( + "Can create an operational intent with the second credentials", self._pid + ) as check: + if q2.response.status_code != 201: + check.record_failed( + f"Could not create operational intent using second credentials", + Severity.High, + f"DSS responded with {q2.response.status_code} to attempt to create OI {self._oid_2}", + query_timestamps=[q2.request.timestamp], + ) + + def _ensure_credentials_are_different(self): + """ + Checks the auth adapters for the subscription they used and raises an exception if they are the same. + Note that both adapters need to have been used at least once before this check can be performed, + otherwise they have no token available. + """ + if ( + self._dss_separate_creds.client.auth_adapter.get_sub() + == self._dss.client.auth_adapter.get_sub() + ): + raise Exception( + f"The same credentials were provided for both utm_auth instances ({self._dss.client.auth_adapter.get_sub()})," + f" cannot verify access controls, skipping test." + ) + + def _check_mutation_on_non_owned_intent_fails(self): + # Attempt to update the state of the intent created with the main credentials using the second credentials + (ref, notif, q) = self._dss_separate_creds.put_op_intent( + id=self._oid_1, + extents=self._volumes1.to_f3548v21(), + key=[self._current_ref_2.ovn], + state=OperationalIntentState.Accepted, + base_url=self._current_ref_1.uss_base_url, + ovn=self._current_ref_1.ovn, + ) + self.record_query(q) + with self.check( + "Second credentials cannot modify operational intent created with main credentials", + self._pid, + ) as check: + if q.response.status_code != 403: + check.record_failed( + f"Could update operational intent using second credentials", + Severity.High, + f"DSS responded with {q.response.status_code} to attempt to update OI {self._oid_1}", + query_timestamps=[q.request.timestamp], + ) + # Attempt to update the base_url of the intent created with the main credentials using the second credentials + (ref, notif, q) = self._dss_separate_creds.put_op_intent( + id=self._oid_1, + extents=self._volumes1.to_f3548v21(), + key=[self._current_ref_2.ovn], + state=self._current_ref_1.state, + base_url="https://another-url.uss/down", + ovn=self._current_ref_1.ovn, + ) + self.record_query(q) + with self.check( + "Second credentials cannot modify operational intent created with main credentials", + self._pid, + ) as check: + if q.response.status_code != 403: + check.record_failed( + f"Could update operational intent using second credentials", + Severity.High, + f"DSS responded with {q.response.status_code} to attempt to update OI {self._oid_1}", + query_timestamps=[q.request.timestamp], + ) + + # Try to delete + # TODO handle notifications if deletion is successful + (_, _, dq) = self._dss_separate_creds.delete_op_intent( + self._oid_1, self._current_ref_1.ovn + ) + self.record_query(dq) + with self.check( + "Second credentials cannot delete operational intent created with main credentials", + self._pid, + ) as check: + if dq.response.status_code != 403: + check.record_failed( + f"Could delete operational intent using second credentials", + Severity.High, + f"DSS responded with {dq.response.status_code} to attempt to delete OI {self._oid_1}", + query_timestamps=[dq.request.timestamp], + ) + + # Query again to confirm that the op intent has not been modified in any way: + (op_1_current, qcheck) = self._dss.get_op_intent(self._oid_1) + self.record_query(qcheck) + + with self.check( + "Operational intents can be queried directly by their ID", self._pid + ) as check: + if qcheck.response.status_code != 200: + check.record_failed( + f"Could not access operational intent using main credentials", + Severity.High, + f"DSS responded with {qcheck.response.status_code} to attempt to access OI {self._oid_1} " + f"while this OI should have been available.", + query_timestamps=[qcheck.request.timestamp], + ) + + with self.check( + "Second credentials cannot modify operational intent created with main credentials", + self._pid, + ) as check: + if op_1_current != self._current_ref_1: + check.record_failed( + f"Could update operational intent using second credentials", + Severity.High, + f"Operational intent {self._oid_1} was modified by second credentials", + query_timestamps=[q.request.timestamp, qcheck.request.timestamp], + ) + + def cleanup(self): + self.begin_cleanup() + + # We remove the op intents that were created for this scenario + self._clean_known_op_intents_ids() + + self.end_cleanup() diff --git a/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md b/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md index 2cc19f3443..44fe733e30 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md +++ b/monitoring/uss_qualifier/suites/astm/utm/dss_probing.md @@ -4,7 +4,8 @@ ## [Actions](../../README.md#actions) -1. Scenario: [ASTM F3548-21 UTM DSS interoperability](../../../scenarios/astm/utm/dss_interoperability.md) ([`scenarios.astm.utm.DSSInteroperability`](../../../scenarios/astm/utm/dss_interoperability.py)) +1. Scenario: [ASTM F3548-21 UTM DSS Operational Intent Access Control](../../../scenarios/astm/utm/op_intent_access_control.md) ([`scenarios.astm.utm.OpIntentAccessControl`](../../../scenarios/astm/utm/op_intent_access_control.py)) +2. Scenario: [ASTM F3548-21 UTM DSS interoperability](../../../scenarios/astm/utm/dss_interoperability.md) ([`scenarios.astm.utm.DSSInteroperability`](../../../scenarios/astm/utm/dss_interoperability.py)) ## [Checked requirements](../../README.md#checked-requirements) @@ -16,9 +17,19 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
+ DSS0005 + Implemented + ASTM F3548-21 UTM DSS Operational Intent Access Control + + DSS0300 Implemented ASTM F3548-21 UTM DSS interoperability + + OPIN0035 + Implemented + ASTM F3548-21 UTM DSS Operational Intent Access Control + diff --git a/monitoring/uss_qualifier/suites/astm/utm/dss_probing.yaml b/monitoring/uss_qualifier/suites/astm/utm/dss_probing.yaml index a6a4b2d792..eb6ee931e2 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/dss_probing.yaml +++ b/monitoring/uss_qualifier/suites/astm/utm/dss_probing.yaml @@ -1,8 +1,18 @@ name: DSS testing for ASTM NetRID F3548-21 resources: dss: resources.astm.f3548.v21.DSSInstanceResource + second_utm_auth: resources.communications.AuthAdapterResource? all_dss_instances: resources.astm.f3548.v21.DSSInstancesResource? + flight_intents: resources.flight_planning.FlightIntentsResource + id_generator: resources.interuss.IDGeneratorResource actions: + - test_scenario: + scenario_type: scenarios.astm.utm.OpIntentAccessControl + resources: + dss: dss + second_utm_auth: second_utm_auth + flight_intents: flight_intents + id_generator: id_generator - test_scenario: scenario_type: scenarios.astm.utm.DSSInteroperability resources: diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md index 4c49a431c3..913c206465 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md @@ -29,10 +29,10 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented - ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + ASTM F3548 flight planners preparation
ASTM F3548-21 UTM DSS Operational Intent Access Control
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents DSS0100 @@ -74,6 +74,11 @@ Implemented Validation of operational intents + + OPIN0035 + Implemented + ASTM F3548-21 UTM DSS Operational Intent Access Control + OPIN0040 Implemented diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml index 408b3565e1..2082272c3f 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml @@ -9,19 +9,27 @@ resources: non_conflicting_flights: resources.flight_planning.FlightIntentsResource nominal_planning_selector: resources.flight_planning.FlightPlannerCombinationSelectorResource? priority_planning_selector: resources.flight_planning.FlightPlannerCombinationSelectorResource? + second_utm_auth: resources.communications.AuthAdapterResource? mock_uss: resources.interuss.mock_uss.client.MockUSSResource + id_generator: resources.interuss.IDGeneratorResource actions: - action_generator: generator_type: action_generators.astm.f3548.ForEachDSS resources: dss_instances: dss_instances + second_utm_auth: second_utm_auth + flight_intents: non_conflicting_flights + id_generator: id_generator specification: action_to_repeat: test_suite: suite_type: suites.astm.utm.dss_probing resources: dss: dss + second_utm_auth: second_utm_auth all_dss_instances: dss_instances + flight_intents: flight_intents + id_generator: id_generator on_failure: Continue dss_instances_source: dss_instances dss_instance_id: dss diff --git a/monitoring/uss_qualifier/suites/faa/uft/message_signing.md b/monitoring/uss_qualifier/suites/faa/uft/message_signing.md index 475cae5788..199405ed76 100644 --- a/monitoring/uss_qualifier/suites/faa/uft/message_signing.md +++ b/monitoring/uss_qualifier/suites/faa/uft/message_signing.md @@ -18,10 +18,10 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented - ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + ASTM F3548 flight planners preparation
ASTM F3548-21 UTM DSS Operational Intent Access Control
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents DSS0100 @@ -63,6 +63,11 @@ Implemented Validation of operational intents + + OPIN0035 + Implemented + ASTM F3548-21 UTM DSS Operational Intent Access Control + OPIN0040 Implemented diff --git a/monitoring/uss_qualifier/suites/uspace/flight_auth.md b/monitoring/uss_qualifier/suites/uspace/flight_auth.md index 1a677d1cd7..c7c46dbdbb 100644 --- a/monitoring/uss_qualifier/suites/uspace/flight_auth.md +++ b/monitoring/uss_qualifier/suites/uspace/flight_auth.md @@ -19,10 +19,10 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented - ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + ASTM F3548 flight planners preparation
ASTM F3548-21 UTM DSS Operational Intent Access Control
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents DSS0100 @@ -64,6 +64,11 @@ Implemented Validation of operational intents + + OPIN0035 + Implemented + ASTM F3548-21 UTM DSS Operational Intent Access Control + OPIN0040 Implemented diff --git a/monitoring/uss_qualifier/suites/uspace/required_services.md b/monitoring/uss_qualifier/suites/uspace/required_services.md index 666589a1ca..4ff38e4bb1 100644 --- a/monitoring/uss_qualifier/suites/uspace/required_services.md +++ b/monitoring/uss_qualifier/suites/uspace/required_services.md @@ -449,10 +449,10 @@ ASTM NetRID DSS: Concurrent Requests
ASTM NetRID DSS: ISA Expiry
ASTM NetRID DSS: ISA Subscription Interactions
ASTM NetRID DSS: Simple ISA
ASTM NetRID DSS: Submitted ISA Validations
ASTM NetRID DSS: Subscription Simple
ASTM NetRID DSS: Subscription Validation
ASTM NetRID DSS: Token Validation - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented - ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + ASTM F3548 flight planners preparation
ASTM F3548-21 UTM DSS Operational Intent Access Control
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents DSS0100 @@ -494,6 +494,11 @@ Implemented Validation of operational intents + + OPIN0035 + Implemented + ASTM F3548-21 UTM DSS Operational Intent Access Control + OPIN0040 Implemented