From 5f417e3e39d5a3026de07de7ad15a00f6218b4ad Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Fri, 16 May 2025 15:03:10 -0500 Subject: [PATCH 1/9] Added extra output to dryrun functionality --- ferry_cli/helpers/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ferry_cli/helpers/api.py b/ferry_cli/helpers/api.py index 33b0bc3..34e1885 100755 --- a/ferry_cli/helpers/api.py +++ b/ferry_cli/helpers/api.py @@ -46,7 +46,9 @@ def call_endpoint( ) -> Any: # Create a session object to persist certain parameters across requests if self.dryrun: - print(f"\nWould call endpoint: {self.base_url}{endpoint}") + print( + f"\nWould call endpoint: {self.base_url}{endpoint} with params\n{params}" + ) return None debug = self.debug_level == DebugLevel.DEBUG From e2af10b648e921af5762883a0bdbdff4c5ca55b4 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Fri, 16 May 2025 15:06:54 -0500 Subject: [PATCH 2/9] If we have a failure here, don't exit; rather raise a RuntimeError --- ferry_cli/helpers/workflows.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ferry_cli/helpers/workflows.py b/ferry_cli/helpers/workflows.py index 9023508..64bdd06 100755 --- a/ferry_cli/helpers/workflows.py +++ b/ferry_cli/helpers/workflows.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -import sys from typing import Any, Dict, List try: @@ -41,9 +40,14 @@ def verify_output(self: "Workflow", api: "FerryAPI", response: Any) -> Any: if api.dryrun: return {} if not response: - print("Failed'") - sys.exit(1) - if "ferry_status" in response and response.get("ferry_status", "") != "success": + print("Failed to verify output") + raise RuntimeError("Empty response from FERRY") + if response.get("ferry_status", "") != "success": + print("Failed to verify output") print(f"{response}") - sys.exit(1) + if "ferry_error" in response: + raise RuntimeError( + "FERRY returned error(s): " + ", ".join(response["ferry_error"]) + ) + raise RuntimeError("FERRY did not return a successful response") return response["ferry_output"] From c71d2f99faa851d8516a6271771864d740c949bb Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Fri, 16 May 2025 16:02:48 -0500 Subject: [PATCH 3/9] Disable unused-ignore errors from mypy --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b78cc84..3ee2324 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: mypy name: mypy (no tests) exclude: ^(tests/) - args: [--strict, '--ignore-missing-imports'] + args: [--strict, '--ignore-missing-imports', '--disable-error-code=unused-ignore'] additional_dependencies: ['types-requests', 'types-toml'] - repo: https://github.com/psf/black rev: 22.8.0 From 1aec97161a3f1dda9d35ed578e83d102ea657dc5 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Fri, 16 May 2025 16:16:40 -0500 Subject: [PATCH 4/9] Added NewCapabilitySet workflow --- .pre-commit-config.yaml | 2 +- .../supported_workflows/NewCapabilitySet.py | 305 ++++++++++++++++++ .../helpers/supported_workflows/__init__.py | 3 + tests/test_NewCapabilitySet.py | 113 +++++++ 4 files changed, 422 insertions(+), 1 deletion(-) create mode 100755 ferry_cli/helpers/supported_workflows/NewCapabilitySet.py create mode 100644 tests/test_NewCapabilitySet.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3ee2324..b78cc84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: mypy name: mypy (no tests) exclude: ^(tests/) - args: [--strict, '--ignore-missing-imports', '--disable-error-code=unused-ignore'] + args: [--strict, '--ignore-missing-imports'] additional_dependencies: ['types-requests', 'types-toml'] - repo: https://github.com/psf/black rev: 22.8.0 diff --git a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py new file mode 100755 index 0000000..a2e2b6c --- /dev/null +++ b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py @@ -0,0 +1,305 @@ +# pylint: disable=invalid-name,arguments-differ,unused-import +import sys +from typing import Any, List + +try: + from ferry_cli.helpers.api import FerryAPI + from ferry_cli.helpers.auth import DebugLevel + from ferry_cli.helpers.workflows import Workflow +except ImportError: + from helpers.api import FerryAPI # type: ignore + from helpers.auth import DebugLevel # type: ignore + from helpers.workflows import Workflow # type: ignore + + +class NewCapabilitySet(Workflow): + def __init__(self: "NewCapabilitySet") -> None: + self.name = "newCapabilitySet" + self.method = "PUT" + self.description = ( + "Creates a new capability set based on a given role and unix group" + ) + self.params = [ + { + "name": "groupname", + "description": "UNIX group the new capability set will be associated with", + "type": "string", + "required": True, + }, + { + "name": "gid", + "description": "GID of the UNIX group groupname", + "type": int, + "required": True, + }, + { + "name": "unitname", + "description": "Affiliation Unit the new capability set will be associated with", + "type": "string", + "required": True, + }, + { + "name": "fqan", + "description": "FQAN associated with the new capability set", + "type": "string", + "required": True, + }, + { + "name": "setname", + "description": "Name of the new capability set", + "type": "string", + "required": True, + }, + { + "name": "scopes_pattern", + "description": "Scopes of the new capability set", + "type": "string", + "required": True, + }, + ] + super().__init__() + + def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: ignore # pylint: disable=arguments-differ,too-many-branches,too-many-statements + """Run the workflow to add a new capability set to FERRY""" + if api.dryrun: + print( + "WARNING: This workflow is being run with the --dryrun flag. The exact steps shown here may differ since " + "some of the workflow steps depend on the output of API calls." + ) + + # Note - we don't have explicit dryrun checks here because the FerryAPI class handles that for us + # 1. Create new group in FERRY + try: + self.verify_output( + api, + api.call_endpoint( + "createGroup", + method="PUT", + params={ + "groupname": args["groupname"], + "gid": args["gid"], + "grouptype": "UnixGroup", + }, + ), + ) + except Exception as e: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to create group") + if "groupname already exists" in str(e): + print( + f"Group {args['groupname']} already exists in FERRY. Continuing with the workflow." + ) + else: + raise + + # Check + if not api.dryrun: + try: + response = self.verify_output( + api, + api.call_endpoint( + "getGroupName", + params={"gid": args["gid"]}, + ), + ) + if response["groupname"] != args["groupname"]: + print( + f"Group name {response['groupname']} does not match expected group name {args['groupname']}" + ) + raise ValueError("Group name mismatch") + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to verify group creation") + raise + + # 2. Add group to unit + try: + self.verify_output( + api, + api.call_endpoint( + "addGroupToUnit", + method="PUT", + params={ + "groupname": args["groupname"], + "unitname": args["unitname"], + "grouptype": "UnixGroup", + }, + ), + ) + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to add group to unit") + raise + + # Check + if not api.dryrun: + try: + response = self.verify_output( + api, + api.call_endpoint( + "getGroupUnits", + params={"groupname": args["groupname"]}, + ), + ) + units = (entry["unitname"] for entry in response) + for unit in units: + if unit == args["unitname"]: + break + else: + print( + f"Group {args['groupname']} does not belong to unit {args['unitname']}" + ) + raise ValueError("Group does not belong to unit") + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to verify group-unit association") + raise + + # 3. Create new FQAN + try: + self.verify_output( + api, + api.call_endpoint( + "createFQAN", + method="PUT", + params={ + "fqan": args["fqan"], + "unitname": args["unitname"], + "groupname": args["groupname"], + }, + ), + ) + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to create FQAN") + raise + + # No Check available for FQAN creation at this time + + # 4. Create capability set + try: + self.verify_output( + api, + api.call_endpoint( + "createCapabilitySet", + method="PUT", + params={ + "setname": args["setname"], + "pattern": args["scopes_pattern"], + }, + ), + ) + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to create capability set") + raise + # Check will be after next step + + # 5. Associate capability set with FQAN + role = self._calculate_role(args["fqan"]) + if not role: + print(f"Failed to calculate role from FQAN {args['fqan']}") + raise ValueError("Role calculation failed") + try: + self.verify_output( + api, + api.call_endpoint( + "addCapabilitySetToFQAN", + method="PUT", + params={ + "setname": args["setname"], + "unitname": args["unitname"], + "role": role, + }, + ), + ) + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to associate capability set with FQAN") + raise + + # Check all capability set settings + # TODO something is still wrong here # pylint: disable=fixme + + # Testing this, we get the error: + # Group jobsubadmin already exists in FERRY. Continuing with the workflow. + # Failed to verify capability set creation + # An error occurred while using the FERRY CLI: list indices must be integers or slices, not str + + # This indicates that there's some kind of indexing issue in the verification code here + # Test this in dev. + + if not api.dryrun: + try: + response = self.verify_output( + api, + api.call_endpoint( + "getCapabilitySet", + params={"setname": args["setname"]}, + ), + ) + # Verify that the capability set name matches the expected name + try: + assert response["setname"] == args["setname"] + except AssertionError: + raise ValueError( + f"Capability set name {response['setname']} does not match expected name {args['setname']}" + ) + + # Verify that the capability set pattern matches the expected pattern + try: + assert self._check_lists_for_same_elts( + response["patterns"], + self.scopes_string_to_list(args["scopes_pattern"]), + ) + except AssertionError: + raise ValueError( + f"Capability set pattern {response['patterns']} does not match expected pattern {args['scopes_pattern']}" + ) + + # Verify that the capability set FQAN and role matches the expected FQAN and role + role_entries = (entry for entry in response["roles"]) + for entry in role_entries: + if entry["role"] == role: + try: + assert entry["fqan"] == args["fqan"] + except AssertionError: + raise ValueError( + f"Capability set role {entry['role']} does not match expected role {role}" + ) + break # Good case - role and fqan match + else: + raise ValueError( + f"Capability set role does not match expected role {role} or FQAN {args['fqan']} is not found in proper role entry" + ) + except Exception: # pylint: disable=broad-except + if api.debug_level != DebugLevel.QUIET: + print("Failed to verify capability set creation") + raise + + @staticmethod + def scopes_string_to_list( + scopes_string: str, out_delimiter: str = "," + ) -> List[str]: + """Convert a scopes list to a string of scopes delimited by out_delimiter + e.g. "scope1,scope2" -> ["scope1", "scope2"] + """ + if not scopes_string: + return [] + return scopes_string.split(out_delimiter) + + @staticmethod + def _check_lists_for_same_elts(list1: List[str], list2: List[str]) -> bool: + """Compare two lists for the same elements, regardless of order""" + return sorted(list1) == sorted(list2) + + @staticmethod + def _calculate_role(fqan: str) -> str: + """Calculate the role from the FQAN + Something like /fermilab/Role=Rolename/Capability=NULL -> Rolename + """ + parts = fqan.split("/") + for part in parts: + if part.startswith("Role="): + return part.split("=")[1] + return "" diff --git a/ferry_cli/helpers/supported_workflows/__init__.py b/ferry_cli/helpers/supported_workflows/__init__.py index c573050..0009ff1 100755 --- a/ferry_cli/helpers/supported_workflows/__init__.py +++ b/ferry_cli/helpers/supported_workflows/__init__.py @@ -7,13 +7,16 @@ from ferry_cli.helpers.supported_workflows.GetFilteredGroupInfo import ( GetFilteredGroupInfo, ) + from ferry_cli.helpers.supported_workflows.NewCapabilitySet import NewCapabilitySet except ImportError: from helpers.workflows import Workflow # type: ignore from helpers.supported_workflows.CloneResource import CloneResource # type: ignore from helpers.supported_workflows.GetFilteredGroupInfo import GetFilteredGroupInfo # type: ignore + from helpers.supported_workflows.NewCapabilitySet import NewCapabilitySet # type: ignore SUPPORTED_WORKFLOWS: Mapping[str, Type[Workflow]] = { "cloneResource": CloneResource, "getFilteredGroupInfo": GetFilteredGroupInfo, + "newCapabilitySet": NewCapabilitySet, } diff --git a/tests/test_NewCapabilitySet.py b/tests/test_NewCapabilitySet.py new file mode 100644 index 0000000..bbe64cb --- /dev/null +++ b/tests/test_NewCapabilitySet.py @@ -0,0 +1,113 @@ +import pytest + +from ferry_cli.helpers.api import FerryAPI +from ferry_cli.helpers.auth import Auth +from ferry_cli.helpers.supported_workflows.NewCapabilitySet import NewCapabilitySet + + +@pytest.mark.unit +def test_NewCapabiilitySet_run(capsys): + endpoint = "https://example.com" + args = { + "groupname": "testgroup", + "gid": 1234, + "unitname": "testunit", + "fqan": "/org/Role=myrole/Capability=NULL", + "setname": "testcapabilityset", + "scopes_pattern": "scope1,scope2", + } + role = "myrole" + + expected_output = [ + ( + f"Would call endpoint: {endpoint}/createGroup with params\n" + + f"{{'groupname': '{args['groupname']}', 'gid': {args['gid']}, 'grouptype': 'UnixGroup'}}" + ), + ( + f"Would call endpoint: {endpoint}/addGroupToUnit with params\n" + + f"{{'groupname': '{args['groupname']}', 'unitname': '{args['unitname']}', 'grouptype': 'UnixGroup'}}" + ), + ( + f"Would call endpoint: {endpoint}/createFQAN with params\n" + + f"{{'fqan': '{args['fqan']}', 'unitname': '{args['unitname']}', 'groupname': '{args['groupname']}'}}" + ), + ( + f"Would call endpoint: {endpoint}/createCapabilitySet with params\n" + + f"{{'setname': '{args['setname']}', 'pattern': '{args['scopes_pattern']}'}}" + ), + ( + f"Would call endpoint: {endpoint}/addCapabilitySetToFQAN with params\n" + + f"{{'setname': '{args['setname']}', 'unitname': '{args['unitname']}', 'role': '{role}'}}" + ), + ] + + api = FerryAPI( + base_url="https://example.com/", + authorizer=Auth(), + dryrun=True, + ) + + NewCapabilitySet().run( + api=api, + args=args, + ) + + captured = capsys.readouterr() + for elt in expected_output: + assert elt in captured.out + + +@pytest.mark.parametrize( + "scopes_string, out_delimiter, expected", + [ + ("scope1,scope2", ",", ["scope1", "scope2"]), + ("scope1 scope2", " ", ["scope1", "scope2"]), + ("scope1, scope2", ", ", ["scope1", "scope2"]), + ( + "scope,1,scope2", + ",", + ["scope", "1", "scope2"], + ), # This is a malformed scope, but we want to test that it is handled correctly + ], +) +@pytest.mark.unit +def test_scopes_string_to_list(scopes_string, out_delimiter, expected): + assert ( + NewCapabilitySet.scopes_string_to_list(scopes_string, out_delimiter) == expected + ) + + +@pytest.mark.parametrize( + "list1, list2, expected", + [ + (["a", "b", "c"], ["c", "b", "a"], True), + (["a", "b", "c"], ["a", "b"], False), + (["a", "b", "c"], ["d", "e", "f"], False), + ([], [], True), + (["a"], ["a"], True), + (["a"], [], False), + ], +) +@pytest.mark.unit +def test_check_lists_for_same_elts(list1, list2, expected) -> bool: + assert NewCapabilitySet._check_lists_for_same_elts(list1, list2) == expected + + +@pytest.mark.parametrize( + "fqan, expected", + [ + ("/org/exp1/Role=myrole/Capability=NULL", "myrole"), + ("/org/Role=myrole/Capability=NULL", "myrole"), + ("/org/role=myrole/Capability=NULL", ""), + ("/org/myrole/Capability=NULL", ""), + ( + "/org/Role=my/role/Capability=NULL", + "my", + ), # This is a malformed FQAN, but we want to test that it is handled correctly + ("randomstring", ""), + ("", ""), + ], +) +@pytest.mark.unit +def test_calculate_role(fqan, expected) -> str: + assert NewCapabilitySet._calculate_role(fqan) == expected From 42b8bd4f4432f6fe9a134da81526c645241d3f43 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Fri, 16 May 2025 22:18:41 -0500 Subject: [PATCH 5/9] Added case where we want to map FQAN/Capability set to user --- .../supported_workflows/NewCapabilitySet.py | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py index a2e2b6c..da11412 100755 --- a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py +++ b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py @@ -56,6 +56,12 @@ def __init__(self: "NewCapabilitySet") -> None: "type": "string", "required": True, }, + { + "name": "mapped_user", + "description": "If the capability set needs to be mapped to a specific user, this is that mapped username", + "type": "string", + "required": False, + }, ] super().__init__() @@ -146,27 +152,73 @@ def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: i if unit == args["unitname"]: break else: - print( + raise ValueError( f"Group {args['groupname']} does not belong to unit {args['unitname']}" ) - raise ValueError("Group does not belong to unit") except Exception: # pylint: disable=broad-except if api.debug_level != DebugLevel.QUIET: print("Failed to verify group-unit association") raise + # TODO Test this case # pylint: disable=fixme + # 2a. Optional - add mapped user to group + if args.get("mapped_user", ""): + try: + self.verify_output( + api, + api.call_endpoint( + "addUserToGroup", + method="PUT", + params={ + "groupname": args["groupname"], + "username": args["mapped_user"], + "grouptype": "UnixGroup", + }, + ), + ) + except Exception: + if api.debug_level != DebugLevel.QUIET: + print("Failed to add mapped user to group") + raise + # Check + if not api.dryrun: + try: + response = self.verify_output( + api, + api.call_endpoint( + "getGroupMembers", + params={"groupname": args["groupname"]}, + ), + ) + users = (entry["username"] for entry in response) + for user in users: + if user == args["mapped_user"]: + break + else: + raise ValueError( + f"Mapped user {args['mapped_user']} does not belong to group {args['groupname']}" + ) + except Exception: + if api.debug_level != DebugLevel.QUIET: + print("Failed to verify mapped user-group association") + raise + # 3. Create new FQAN try: + params = { + "fqan": args["fqan"], + "unitname": args["unitname"], + "groupname": args["groupname"], + } + if args.get("mapped_user"): + params["username"] = args["mapped_user"] + self.verify_output( api, api.call_endpoint( "createFQAN", method="PUT", - params={ - "fqan": args["fqan"], - "unitname": args["unitname"], - "groupname": args["groupname"], - }, + params=params, ), ) except Exception: # pylint: disable=broad-except From 3932877d2ae029fd4184bc0c1454d50ba00ed676 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Mon, 19 May 2025 12:03:50 -0500 Subject: [PATCH 6/9] Fixed issue where we weren't extracting response data correctly for verification --- ferry_cli/__main__.py | 2 +- .../supported_workflows/NewCapabilitySet.py | 24 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/ferry_cli/__main__.py b/ferry_cli/__main__.py index cbb3a93..62e2ca3 100755 --- a/ferry_cli/__main__.py +++ b/ferry_cli/__main__.py @@ -368,7 +368,7 @@ def run( workflow.init_parser() workflow_params, _ = workflow.parser.parse_known_args(endpoint_args) json_result = workflow.run(self.ferry_api, vars(workflow_params)) # type: ignore - if not dryrun: + if (not dryrun) and json_result: self.handle_output( json.dumps(json_result, indent=4), args.output, debug_level ) diff --git a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py index da11412..bfcb987 100755 --- a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py +++ b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py @@ -271,16 +271,6 @@ def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: i raise # Check all capability set settings - # TODO something is still wrong here # pylint: disable=fixme - - # Testing this, we get the error: - # Group jobsubadmin already exists in FERRY. Continuing with the workflow. - # Failed to verify capability set creation - # An error occurred while using the FERRY CLI: list indices must be integers or slices, not str - - # This indicates that there's some kind of indexing issue in the verification code here - # Test this in dev. - if not api.dryrun: try: response = self.verify_output( @@ -290,27 +280,30 @@ def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: i params={"setname": args["setname"]}, ), ) + # For some reason, the getCapabilitySet API returns a list, so we need to extract the first element + set_info = response[0] + # Verify that the capability set name matches the expected name try: - assert response["setname"] == args["setname"] + assert set_info["setname"] == args["setname"] except AssertionError: raise ValueError( - f"Capability set name {response['setname']} does not match expected name {args['setname']}" + f"Capability set name {set_info['setname']} does not match expected name {args['setname']}" ) # Verify that the capability set pattern matches the expected pattern try: assert self._check_lists_for_same_elts( - response["patterns"], + set_info["patterns"], self.scopes_string_to_list(args["scopes_pattern"]), ) except AssertionError: raise ValueError( - f"Capability set pattern {response['patterns']} does not match expected pattern {args['scopes_pattern']}" + f"Capability set pattern {set_info['patterns']} does not match expected pattern {args['scopes_pattern']}" ) # Verify that the capability set FQAN and role matches the expected FQAN and role - role_entries = (entry for entry in response["roles"]) + role_entries = (entry for entry in set_info["roles"]) for entry in role_entries: if entry["role"] == role: try: @@ -328,6 +321,7 @@ def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: i if api.debug_level != DebugLevel.QUIET: print("Failed to verify capability set creation") raise + print(f"Successfully created capability set {args['setname']}.") @staticmethod def scopes_string_to_list( From 25efb613a014862f9b4ef34091fddba99f2d6694 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Mon, 19 May 2025 12:04:35 -0500 Subject: [PATCH 7/9] Added test case for mapped user in capability set creation --- tests/test_NewCapabilitySet.py | 106 +++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 33 deletions(-) diff --git a/tests/test_NewCapabilitySet.py b/tests/test_NewCapabilitySet.py index bbe64cb..9308abd 100644 --- a/tests/test_NewCapabilitySet.py +++ b/tests/test_NewCapabilitySet.py @@ -5,42 +5,82 @@ from ferry_cli.helpers.supported_workflows.NewCapabilitySet import NewCapabilitySet -@pytest.mark.unit -def test_NewCapabiilitySet_run(capsys): - endpoint = "https://example.com" - args = { - "groupname": "testgroup", - "gid": 1234, - "unitname": "testunit", - "fqan": "/org/Role=myrole/Capability=NULL", - "setname": "testcapabilityset", - "scopes_pattern": "scope1,scope2", - } - role = "myrole" - - expected_output = [ - ( - f"Would call endpoint: {endpoint}/createGroup with params\n" - + f"{{'groupname': '{args['groupname']}', 'gid': {args['gid']}, 'grouptype': 'UnixGroup'}}" - ), - ( - f"Would call endpoint: {endpoint}/addGroupToUnit with params\n" - + f"{{'groupname': '{args['groupname']}', 'unitname': '{args['unitname']}', 'grouptype': 'UnixGroup'}}" - ), - ( - f"Would call endpoint: {endpoint}/createFQAN with params\n" - + f"{{'fqan': '{args['fqan']}', 'unitname': '{args['unitname']}', 'groupname': '{args['groupname']}'}}" - ), +@pytest.mark.parametrize( + "args, expected", + [ ( - f"Would call endpoint: {endpoint}/createCapabilitySet with params\n" - + f"{{'setname': '{args['setname']}', 'pattern': '{args['scopes_pattern']}'}}" + { + "groupname": "testgroup", + "gid": 1234, + "unitname": "testunit", + "fqan": "/org/Role=myrole/Capability=NULL", + "setname": "testcapabilityset", + "scopes_pattern": "scope1,scope2", + }, + [ + ( + "Would call endpoint: https://example.com/createGroup with params\n" + + "{'groupname': 'testgroup', 'gid': 1234, 'grouptype': 'UnixGroup'}" + ), + ( + "Would call endpoint: https://example.com/addGroupToUnit with params\n" + + "{'groupname': 'testgroup', 'unitname': 'testunit', 'grouptype': 'UnixGroup'}" + ), + ( + f"Would call endpoint: https://example.com/createFQAN with params\n" + + "{'fqan': '/org/Role=myrole/Capability=NULL', 'unitname': 'testunit', 'groupname': 'testgroup'}" + ), + ( + "Would call endpoint: https://example.com/createCapabilitySet with params\n" + + "{'setname': 'testcapabilityset', 'pattern': 'scope1,scope2'}" + ), + ( + "Would call endpoint: https://example.com/addCapabilitySetToFQAN with params\n" + + "{'setname': 'testcapabilityset', 'unitname': 'testunit', 'role': 'myrole'}" + ), + ], ), ( - f"Would call endpoint: {endpoint}/addCapabilitySetToFQAN with params\n" - + f"{{'setname': '{args['setname']}', 'unitname': '{args['unitname']}', 'role': '{role}'}}" + { + "groupname": "testgroup", + "mapped_user": "testuser", + "gid": 1234, + "unitname": "testunit", + "fqan": "/org/Role=myrole/Capability=NULL", + "setname": "testcapabilityset", + "scopes_pattern": "scope1,scope2", + }, + [ + ( + "Would call endpoint: https://example.com/createGroup with params\n" + + "{'groupname': 'testgroup', 'gid': 1234, 'grouptype': 'UnixGroup'}" + ), + ( + "Would call endpoint: https://example.com/addGroupToUnit with params\n" + + "{'groupname': 'testgroup', 'unitname': 'testunit', 'grouptype': 'UnixGroup'}" + ), + ( + "Would call endpoint: https://example.com/addUserToGroup with params\n" + + "{'groupname': 'testgroup', 'username': 'testuser', 'grouptype': 'UnixGroup'}" + ), + ( + f"Would call endpoint: https://example.com/createFQAN with params\n" + + "{'fqan': '/org/Role=myrole/Capability=NULL', 'unitname': 'testunit', 'groupname': 'testgroup', 'username': 'testuser'}" + ), + ( + "Would call endpoint: https://example.com/createCapabilitySet with params\n" + + "{'setname': 'testcapabilityset', 'pattern': 'scope1,scope2'}" + ), + ( + "Would call endpoint: https://example.com/addCapabilitySetToFQAN with params\n" + + "{'setname': 'testcapabilityset', 'unitname': 'testunit', 'role': 'myrole'}" + ), + ], ), - ] - + ], +) +@pytest.mark.unit +def test_NewCapabiilitySet_run(args, expected, capsys): api = FerryAPI( base_url="https://example.com/", authorizer=Auth(), @@ -53,7 +93,7 @@ def test_NewCapabiilitySet_run(capsys): ) captured = capsys.readouterr() - for elt in expected_output: + for elt in expected: assert elt in captured.out From 2106143f48de94e2e6816c81ed04153feaa73827 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Mon, 19 May 2025 12:13:38 -0500 Subject: [PATCH 8/9] Thank you copilot for catching this - fixed docstring --- ferry_cli/helpers/supported_workflows/NewCapabilitySet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py index bfcb987..6901553 100755 --- a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py +++ b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py @@ -327,7 +327,7 @@ def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: i def scopes_string_to_list( scopes_string: str, out_delimiter: str = "," ) -> List[str]: - """Convert a scopes list to a string of scopes delimited by out_delimiter + """Convert a scopes string to a list of scopes delimited by out_delimiter e.g. "scope1,scope2" -> ["scope1", "scope2"] """ if not scopes_string: From 039be984d620c496b243a0845bd6dd52b38f5528 Mon Sep 17 00:00:00 2001 From: Shreyas Bhat Date: Wed, 28 May 2025 15:50:16 -0500 Subject: [PATCH 9/9] Added token_subject parameter to newCapabilitySet workflow to allow token subject to be FERRY UID --- .../supported_workflows/NewCapabilitySet.py | 21 +++++++++--- tests/test_NewCapabilitySet.py | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py index 6901553..c3d9916 100755 --- a/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py +++ b/ferry_cli/helpers/supported_workflows/NewCapabilitySet.py @@ -62,6 +62,15 @@ def __init__(self: "NewCapabilitySet") -> None: "type": "string", "required": False, }, + { + "name": "token_subject", + "description": ( + "The default will just be setname@fnal.gov, but if the resultant token should have the user UID from FERRY as the subject " + + 'then set this to the string "none"' + ), + "type": "string", + "required": False, + }, ] super().__init__() @@ -230,15 +239,19 @@ def run(self: "NewCapabilitySet", api: "FerryAPI", args: Any) -> Any: # type: i # 4. Create capability set try: + new_cap_set_params = { + "setname": args["setname"], + "pattern": args["scopes_pattern"], + } + if args.get("token_subject", None) is not None: + new_cap_set_params["token_subject"] = args["token_subject"] + self.verify_output( api, api.call_endpoint( "createCapabilitySet", method="PUT", - params={ - "setname": args["setname"], - "pattern": args["scopes_pattern"], - }, + params=new_cap_set_params, ), ) except Exception: # pylint: disable=broad-except diff --git a/tests/test_NewCapabilitySet.py b/tests/test_NewCapabilitySet.py index 9308abd..9b60210 100644 --- a/tests/test_NewCapabilitySet.py +++ b/tests/test_NewCapabilitySet.py @@ -40,6 +40,39 @@ ), ], ), + ( + { + "groupname": "testgroup", + "gid": 1234, + "unitname": "testunit", + "fqan": "/org/Role=myrole/Capability=NULL", + "setname": "testcapabilityset", + "scopes_pattern": "scope1,scope2", + "token_subject": "none", + }, + [ + ( + "Would call endpoint: https://example.com/createGroup with params\n" + + "{'groupname': 'testgroup', 'gid': 1234, 'grouptype': 'UnixGroup'}" + ), + ( + "Would call endpoint: https://example.com/addGroupToUnit with params\n" + + "{'groupname': 'testgroup', 'unitname': 'testunit', 'grouptype': 'UnixGroup'}" + ), + ( + f"Would call endpoint: https://example.com/createFQAN with params\n" + + "{'fqan': '/org/Role=myrole/Capability=NULL', 'unitname': 'testunit', 'groupname': 'testgroup'}" + ), + ( + "Would call endpoint: https://example.com/createCapabilitySet with params\n" + + "{'setname': 'testcapabilityset', 'pattern': 'scope1,scope2', 'token_subject': 'none'}" + ), + ( + "Would call endpoint: https://example.com/addCapabilitySetToFQAN with params\n" + + "{'setname': 'testcapabilityset', 'unitname': 'testunit', 'role': 'myrole'}" + ), + ], + ), ( { "groupname": "testgroup",