diff --git a/.github/workflows/run_tests_access_drogon_affiliate_login.yaml b/.github/workflows/run_tests_access_drogon_affiliate_login.yaml new file mode 100644 index 00000000..1b6dfc9f --- /dev/null +++ b/.github/workflows/run_tests_access_drogon_affiliate_login.yaml @@ -0,0 +1,51 @@ +name: Test access DROGON-AFFILIATE login + +on: + pull_request: + branches: [main] + schedule: + - cron: "59 4 * * *" + workflow_dispatch: + +jobs: + build_pywheels: + name: PY ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.11"] + os: [ubuntu-latest] + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Azure Login + uses: Azure/login@v2 + with: + client-id: a93bdf61-02ec-4e2d-8d8b-ca00673d11f3 + tenant-id: 3aa4a235-b6e2-48d5-9195-7fcf05b459b0 + allow-no-subscriptions: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install fmu-sumo + run: > + python -m pip install --upgrade pip && + python -m pip install .[test] + - name: Run tests + shell: bash + run: | + az --version + az account list + pip list | grep -i sumo + access_token=$(az account get-access-token --scope api://88d2b022-3539-4dda-9e66-853801334a86/.default --query accessToken --output tsv) + export ACCESS_TOKEN=$access_token + + pytest -s --timeout=300 tests/test_access/tst_access_drogon_affiliate_login.py + diff --git a/tests/test_access/README.md b/tests/test_access/README.md index 8df21229..4323c124 100644 --- a/tests/test_access/README.md +++ b/tests/test_access/README.md @@ -1,20 +1,24 @@ -# Testing access to SUMO: read, write, manage, no access +# Testing access to SUMO: read, write, manage, no access, affiliate Tests in this folder shall be run inside Github Actions as specific users with specific access. Each test file is tailored for a specific -user with either no-access, DROGON-READ, DROGON-WRITE or DROGON-MANAGE. +user with either no-access, DROGON-READ, DROGON-WRITE, DROGON-MANAGE +or DROGON-AFFILIATE. Since you as a developer have different accesses, many tests will fail if you run them as yourself. There are pytest skip decorators to avoid running these tests outside Github Actions. -In addition, the file names use the non-standard 'tst' over 'test' to avoid being picked -up by a call to pytest. +In addition, the file names use the non-standard 'tst' over 'test' to +avoid being picked up by a call to pytest. Print statements are used to ensure the Github Actions run prints information that can be used for debugging. -Using allow-no-subscriptions flag to avoid having to give the App Registrations access to some resource inside the subscription itself. Example: +Using allow-no-subscriptions flag to avoid having to give the +App Registrations access to some resource inside the subscription itself. +Example: + ``` - name: Azure Login uses: Azure/login@v2 @@ -24,28 +28,75 @@ Using allow-no-subscriptions flag to avoid having to give the App Registrations allow-no-subscriptions: true ``` -If you want to run the tests on your laptop, using bash: +## Run tests on your local laptop with your own identity + +If you want to run the tests on your laptop as yourself, using bash: + +``` export GITHUB_ACTIONS="true" +``` + +Note that since you have different access, most tests should fail + +## Run tests on your local laptop as one of the App Registrations + +To run these tests on your developer laptop _as the different +App Registrations_, using bash and az cli: + +* Create a secret for the relevant App Registration inside Azure portal, +copy the secret. +* Login as the App Registration: + +``` +az login --service-principal -t -u -p --allow-no-subscriptions +``` + +* Get a token and set it in the environment where sumo-wrapper-python will pick it up: -In theory you could run locally as the App Registration / Service Principal but I -do not think the sumo-wrapper-python makes it possible: ``` -az login --service-principal -u -p --tenant --allow-no-subscriptions +export ACCESS_TOKEN=$(az account get-access-token --scope api://88d2b022-3539-4dda-9e66-853801334a86/.default --query accessToken --output tsv) ``` +* Set the env-var to mimick Github Actions: +``` +export GITHUB_ACTIONS=true +``` + +* Run the tests; preferably start with running userpermissions or similar to verify that you have the +access you expect: +``` +pytest -s tests/test_access/tst_access_drogon_affiliate_login.py::test_get_userpermissions +``` + +It is good practice to delete the secret from the App Registration when you are finished. + +Note that the ACCESS_TOKEN can be used to login to the Swagger page (Bearer) too. + + Relevant App Registrations: * sumo-test-runner-no-access No access * sumo-test-runner-drogon-read DROGON-READ * sumo-test-runner-drogon-write DROGON-WRITE * sumo-test-runner-drogon-manage DROGON-MANAGE +* sumo-test-runner-drogon-affiliate DROGON-AFFILIATE + +(Note that the sumo-test-runner-drogon-affiliate app-reg is added as member +to Entra ID Group named 'Sumo admin' which have the DROGON-AFFILIATE role) -The Azure Entra ID 'App Registrations' blade named 'API permissions' is where the access is -given. +The Azure Entra ID 'App Registrations' blade named 'API permissions' is +where the access is given. Remember that the access must be granted/consented +for Equinor by a mail to AADAppConsent@equinor.com: +"Please grant admin consent for Azure Entra ID App Registration sumo-test-runner-drogon-affiliate +to the sumo-core-dev drogon-affiliate role" +as explained [here](https://docs.omnia.equinor.com/governance/iam/App-Admin-Consent/) ## Test access using shared-key -Shared key authentication is also tested. The shared keys are manually created with the /admin/make-shared-access-key, then manually put into Github Actions Secrets. Note that these secrets must be replaced when they expire after a year. +Shared key authentication is also tested. +The shared keys are manually created with the /admin/make-shared-access-key, +then manually put into Github Actions Secrets. +Note that these secrets must be replaced when they expire after a year. It is not possible to run a 'no-access' test with shared key. diff --git a/tests/test_access/tst_access_drogon_affiliate_login.py b/tests/test_access/tst_access_drogon_affiliate_login.py new file mode 100644 index 00000000..bee79613 --- /dev/null +++ b/tests/test_access/tst_access_drogon_affiliate_login.py @@ -0,0 +1,260 @@ +"""Test access to SUMO using a DROGON-AFFILIATE login. + Shall only run in Github Actions as a specific user with + specific access rights. Running this test with your personal login + will fail.""" +import os +import json +import inspect +import pytest +from context import ( + Explorer, +) + +if os.getenv("GITHUB_ACTIONS") == "true": + RUNNING_OUTSIDE_GITHUB_ACTIONS = "False" + print( + "Found the GITHUB_ACTIONS env var, so I know I am running on Github now. Will run these tests." + ) +else: + RUNNING_OUTSIDE_GITHUB_ACTIONS = "True" + msg = "Skipping these tests since they can only run on Github Actions as a specific user" + print("NOT running on Github now.", msg) + pytest.skip(msg, allow_module_level=True) + + +@pytest.fixture(name="explorer") +def fixture_explorer(token: str) -> Explorer: + """Returns explorer""" + return Explorer("dev", token=token) + + +def test_admin_access(explorer: Explorer): + """Test access to an admin endpoint""" + print("Running test:", inspect.currentframe().f_code.co_name) + with pytest.raises(Exception, match="403*"): + print("About to call an admin endpoint which should raise exception") + explorer._sumo.get( + f"/admin/make-shared-access-key?user=noreply%40equinor.com&roles=DROGON-READ&duration=111" + ) + print("Execution should never reach this line") + + +def test_get_userpermissions(explorer: Explorer): + """Test the userpermissions""" + print("Running test:", inspect.currentframe().f_code.co_name) + response = explorer._sumo.get(f"/userpermissions") + print("/Userpermissions response: ", response.text) + userperms = json.loads(response.text) + assert "Drogon" in userperms + assert "affiliate" in userperms.get("Drogon") + assert 1 == len(userperms.get("Drogon")) + assert 1 == len(userperms) + + +def test_get_cases(explorer: Explorer): + """Test the get_cases method""" + print("Running test:", inspect.currentframe().f_code.co_name) + cases = explorer.cases + print("Number of cases: ", len(cases)) + for case in cases: + assert case.field.lower() == "drogon" + # We have set up 1 case in KEEP in Drogon DEV with affiliate-access + assert len(cases) == 1 + # We have set up 2 children objects for this affiliate user to access + case = cases[0] + assert case.uuid == "2c2f47cf-c7ab-4112-87f9-b4797ec51cb6" + assert len(case.surfaces) == 1 + assert len(case.polygons) == 1 + assert len(case.tables) == 0 + assert len(case.cubes) == 0 + assert len(case.dictionaries) == 0 + assert case.polygons[0].uuid == "a5f38286-5cf6-d85c-9b3c-03c72b5947d5" + assert case.surfaces[0].uuid == "5f73b0c1-3bdc-2d0e-1a1d-271331615999" + + +def test_get_object(explorer: Explorer): + """Test the direct get on object by objectid method""" + print("Running test:", inspect.currentframe().f_code.co_name) + cases = explorer.cases + print("Number of cases: ", len(cases)) + + # We have set up a KEEP case in Drogon DEV with + # objects with affiliate-access + + # Read one child object + child_object_uuid = "a5f38286-5cf6-d85c-9b3c-03c72b5947d5" + response = explorer._sumo.get(f"/objects('{child_object_uuid}')") + print ("child retval:", response) + print("child retval.content:", response.content) + assert response.status_code == 200 + response_json = json.loads(response.text) + child_uuid = response_json.get("_id") + print("child_uuid returned:", child_uuid) + assert child_uuid == child_object_uuid + classification = response_json.get("_source").get("access").get("classification") + assert classification == "internal" + + # Read the other child object (which also have + # access.classification:restricted) + child_object_uuid = "5f73b0c1-3bdc-2d0e-1a1d-271331615999" + response = explorer._sumo.get(f"/objects('{child_object_uuid}')") + print ("child retval:", response) + print("child retval.content:", response.content) + assert response.status_code == 200 + response_json = json.loads(response.text) + child_uuid = response_json.get("_id") + print("child_uuid returned:", child_uuid) + assert child_uuid == child_object_uuid + classification = response_json.get("_source").get("access").get("classification") + assert classification == "restricted" + + # Read the case object + case_object_uuid = "2c2f47cf-c7ab-4112-87f9-b4797ec51cb6" + response = explorer._sumo.get(f"/objects('{case_object_uuid}')") + print ("case retval:", response) + print("case retval.content:", response.content) + assert response.status_code == 200 + response_json = json.loads(response.text) + case_uuid = response_json.get("_id") + print("case_uuid returned:", case_uuid) + assert case_uuid == case_object_uuid + + # Read an object which is NOT for affiliates + child_wo_access = "6d9d222f-25d3-5029-07b8-f450dbd2be54" + with pytest.raises(Exception, match="404*"): + print("About to call a endpoint which should raise exception") + response = explorer._sumo.get(f"/objects('{child_wo_access}')") + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text) + + assert len(cases) == 1 + + +def test_write(explorer: Explorer): + """Test a write method""" + print("Running test:", inspect.currentframe().f_code.co_name) + cases = explorer.cases + print("Number of cases: ", len(cases)) + + with open("./tests/data/test_case_080/case2.json") as json_file: + metadata = json.load(json_file) + with pytest.raises(Exception, match="403*"): + print( + "About to call a write endpoint which should raise exception" + ) + response = explorer._sumo.post("/objects", json=metadata) + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text) + + +def test_read_restricted_classification_data(explorer: Explorer): + """Test if can read restricted data aka 'access:classification: restricted'""" + print("Running test:", inspect.currentframe().f_code.co_name) + + # access.classification:restricted is available, + # EVEN for this DROGON-AFFILIATE user + # (This differs from DROGON-READ which cannot read restricted) + response = explorer._sumo.get( + "/search?%24query=access.classification%3Arestricted" + ) + assert response.status_code == 200 + response_json = json.loads(response.text) + hits = response_json.get("hits").get("total").get("value") + print("Hits on restricted:", hits) + assert hits == 1 + +def test_aggregate_bulk(explorer: Explorer): + """Test a bulk aggregation method""" + print("Running test:", inspect.currentframe().f_code.co_name) + # Fixed test case ("Drogon_AHM_2023-02-22") in Sumo/DEV + # This user does not have WRITE and hence this should fail + # (In addition this case is not set up with affiliate access) + TESTCASE_UUID = "10f41041-2c17-4374-a735-bb0de62e29dc" + print("About to trigger bulk aggregation on case", TESTCASE_UUID) + body = { + "operations": ["min"], + "case_uuid": TESTCASE_UUID, + "class": "surface", + "iteration_name": "iter-0", + } + with pytest.raises(Exception, match="40*"): + response = explorer._sumo.post(f"/aggregations", json=body) + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text) + + +def test_aggregations_fast(explorer: Explorer): + """Test a fast aggregation method""" + print("Running test:", inspect.currentframe().f_code.co_name) + # Fixed test case ("Drogon_AHM_2023-02-22") in Sumo/DEV + # This user has AFFILIATE and can READ, but this case + # is not set up with AFFILIATE access, so should fail + TESTCASE_UUID = "10f41041-2c17-4374-a735-bb0de62e29dc" + print("About to trigger fast-aggregation on case", TESTCASE_UUID) + SURFACE_UUID_1 = "ae6cf480-12ba-77ca-848e-92e707556b63" + SURFACE_UUID_2 = "7189835b-cc8a-2a8e-4a34-dde2ceb2a69c" + body = { + "operations": ["min"], + "object_ids": [SURFACE_UUID_1, SURFACE_UUID_2], + "class": "surface", + "iteration_name": "iter-0", + } + print("About to trigger fast-aggregation on hardcoded case", TESTCASE_UUID) + print("using body", body) + with pytest.raises(Exception, match="40*"): + response = explorer._sumo.post(f"/aggregations", json=body) + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text) + + +# TODO: TBC: Consider setting up a case with affiliate access on all +# surfaces, so we can test successful fast aggregation. Need first +# a clarification if affiliates are allowed fast aggregation or not + + +def test_get_access_log(explorer: Explorer): + """Test to get the access log method""" + print("Running test:", inspect.currentframe().f_code.co_name) + print("About to get access log") + response = explorer._sumo.get("/access-log") + print(response.status_code) + print(len(response.text)) + # Currently all authenticated users have access + assert response.status_code == 200 + + +def test_get_key(explorer: Explorer): + """Test to get key method""" + print("Running test:", inspect.currentframe().f_code.co_name) + print("About to get key, which should raise exception ") + with pytest.raises(Exception, match="403*"): + response = explorer._sumo.get("/key") + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text) + + +def test_get_purge(explorer: Explorer): + """Test to get purge method""" + print("Running test:", inspect.currentframe().f_code.co_name) + print("About to get purge, which should raise exception ") + with pytest.raises(Exception, match="403*"): + response = explorer._sumo.get("/purge") + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text) + + +def test_get_message_log_truncate(explorer: Explorer): + """Test to get msg log truncate method""" + print("Running test:", inspect.currentframe().f_code.co_name) + print("About to get msg log truncate, which should raise exception ") + with pytest.raises(Exception, match="403*"): + response = explorer._sumo.get("/message-log/truncate?cutoff=99") + print("Execution should never reach this line") + print("Unexpected status: ", response.status_code) + print("Unexpected response: ", response.text)