From c9176f6a99b14fd931750fdc0ca5fbcd721169f3 Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Tue, 16 May 2023 13:14:20 -0500 Subject: [PATCH 01/12] Export more objects --- grouper_python/__init__.py | 2 +- grouper_python/objects/__init__.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/grouper_python/__init__.py b/grouper_python/__init__.py index 12b2e23..da3ae0e 100644 --- a/grouper_python/__init__.py +++ b/grouper_python/__init__.py @@ -4,5 +4,5 @@ Client = GrouperClient -__version__ = "0.1.3" +__version__ = "0.1.4" __all__ = ["GrouperClient"] diff --git a/grouper_python/objects/__init__.py b/grouper_python/objects/__init__.py index 3c93669..f3de13d 100644 --- a/grouper_python/objects/__init__.py +++ b/grouper_python/objects/__init__.py @@ -1,9 +1,21 @@ """grouper_python.objects, Classes for the grouper_python package.""" -from .group import Group +from .group import Group, CreateGroup from .person import Person -from .stem import Stem +from .stem import Stem, CreateStem from .subject import Subject from .privilege import Privilege +from .membership import Membership, MemberType, MembershipType -__all__ = ["Group", "Person", "Stem", "Subject", "Privilege"] +__all__ = [ + "Group", + "Person", + "Stem", + "Subject", + "Privilege", + "CreateGroup", + "CreateStem", + "Membership", + "MemberType", + "MembershipType", +] From 6f7ee60f032ce833e4bf949ca3e41a07f7d0517e Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Thu, 1 Jun 2023 10:16:03 -0500 Subject: [PATCH 02/12] handle empty privilege results --- grouper_python/privilege.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/grouper_python/privilege.py b/grouper_python/privilege.py index d888988..6cbe32c 100644 --- a/grouper_python/privilege.py +++ b/grouper_python/privilege.py @@ -166,10 +166,13 @@ def get_privileges( act_as_subject=act_as_subject, ) result = r["WsGetGrouperPrivilegesLiteResult"] - return [ - Privilege(client, priv, result["subjectAttributeNames"]) - for priv in result["privilegeResults"] - ] + if "privilegeResults" in result: + return [ + Privilege(client, priv, result["subjectAttributeNames"]) + for priv in result["privilegeResults"] + ] + else: + return [] except GrouperSuccessException as err: r = err.grouper_result r_code = r["WsGetGrouperPrivilegesLiteResult"]["resultMetadata"]["resultCode"] From 9beaf6c1fd38e4fd7d6b75ffa6e47e0c04efeb90 Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Thu, 1 Jun 2023 10:16:25 -0500 Subject: [PATCH 03/12] Add rudimentary attribute functions --- grouper_python/attribute.py | 156 ++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 grouper_python/attribute.py diff --git a/grouper_python/attribute.py b/grouper_python/attribute.py new file mode 100644 index 0000000..d1df3e5 --- /dev/null +++ b/grouper_python/attribute.py @@ -0,0 +1,156 @@ +"""grouper-python.attribute - functions to interact with grouper attributess. + +These are "helper" functions that normally will not be called directly. +However these are still in development, so do not return Python types, +and are not usable from any objects currently. +They can be used by calling directly. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: # pragma: no cover + # from .objects.group import CreateGroup, Group + from .objects.client import GrouperClient + from .objects.subject import Subject +# from .objects.exceptions import ( +# GrouperGroupNotFoundException, +# GrouperSuccessException, +# GrouperStemNotFoundException, +# GrouperPermissionDenied, +# ) + + +def assign_attribute( + attribute_assign_type: str, + assign_operation: str, + client: GrouperClient, + # owner_id: str | None = None, + attribute_assign_id: str | None = None, + owner_name: str | None = None, + attribute_def_name_name: str | None = None, + value: None | str | int | float = None, + assign_value_operation: str | None = None, + act_as_subject: Subject | None = None, +) -> dict[str, Any]: + """Assign an attribute. + + :param attribute_assign_type: _description_ + :type attribute_assign_type: str + :param assign_operation: _description_ + :type assign_operation: str + :param client: _description_ + :type client: GrouperClient + :param attribute_assign_id: _description_, defaults to None + :type attribute_assign_id: str | None, optional + :param owner_name: _description_, defaults to None + :type owner_name: str | None, optional + :param attribute_def_name_name: _description_, defaults to None + :type attribute_def_name_name: str | None, optional + :param value: _description_, defaults to None + :type value: None | str | int | float, optional + :param assign_value_operation: _description_, defaults to None + :type assign_value_operation: str | None, optional + :param act_as_subject: _description_, defaults to None + :type act_as_subject: Subject | None, optional + :raises ValueError: _description_ + :return: _description_ + :rtype: dict[str, Any] + """ + request: dict[str, Any] = { + # "WsRestAssignAttributesLiteRequest": { + # "wsAttributeDefNameName": attribute_def_name_name, + "attributeAssignType": attribute_assign_type, + "attributeAssignOperation": assign_operation, + } + # } + if attribute_assign_type == "group": + if owner_name: + request["wsOwnerGroupLookups"] = [{"groupName": owner_name}] + else: + request["wsOwnerGroupLookups"] = [] + # request["wsOwnerGroupName"] = owner_name + # elif attribute_assign_type == "member": + # request["wsOwnerSubjectIdentifier"] = owner_name + # elif attribute_assign_type == "stem": + # request["wsOwnerStemName"] = owner_name + # elif attribute_assign_type == "attr_def": + # request["wsOwnerAttributeDefName"] = owner_name + else: + raise ValueError("Unknown or unsupported attributeAssignType given") + if attribute_assign_id: + request["wsAttributeAssignLookups"] = [{"uuid": attribute_assign_id}] + if attribute_def_name_name: + request["wsAttributeDefNameLookups"] = [{"name": attribute_def_name_name}] + + if value and assign_value_operation: + request["values"] = [{"valueSystem": value}] + request["attributeAssignValueOperation"] = assign_value_operation + + body = {"WsRestAssignAttributesRequest": request} + + r = client._call_grouper( + "/attributeAssignments", body, act_as_subject=act_as_subject + ) + + return r + + +def get_attribute_assignments( + attribute_assign_type: str, + client: GrouperClient, + attribute_def_name_name: str | None = None, + attribute_def_name: str | None = None, + owner_name: str | None = None, + include_assignments_on_assignments: str = "F", + # value: None | str | int | float = None, + # assign_value_operation: str | None = None, + act_as_subject: Subject | None = None, +) -> dict[str, Any]: + """Get attribute assignments. + + :param attribute_assign_type: _description_ + :type attribute_assign_type: str + :param client: _description_ + :type client: GrouperClient + :param attribute_def_name_name: _description_, defaults to None + :type attribute_def_name_name: str | None, optional + :param attribute_def_name: _description_, defaults to None + :type attribute_def_name: str | None, optional + :param owner_name: _description_, defaults to None + :type owner_name: str | None, optional + :param include_assignments_on_assignments: _description_, defaults to "F" + :type include_assignments_on_assignments: str, optional + :param act_as_subject: _description_, defaults to None + :type act_as_subject: Subject | None, optional + :raises ValueError: _description_ + :return: _description_ + :rtype: dict[str, Any] + """ + request = { + "attributeAssignType": attribute_assign_type, + "includeAssignmentsOnAssignments": include_assignments_on_assignments, + } + if owner_name: + if attribute_assign_type == "group": + request["wsOwnerGroupName"] = owner_name + elif attribute_assign_type == "member": + request["wsOwnerSubjectIdentifier"] = owner_name + elif attribute_assign_type == "stem": + request["wsOwnerStemName"] = owner_name + elif attribute_assign_type == "attr_def": + request["wsOwnerAttributeDefName"] = owner_name + else: + raise ValueError("Unknown or unsupported attributeAssignType given") + if attribute_def_name_name: + request["wsAttributeDefNameName"] = attribute_def_name_name + if attribute_def_name: + request["wsAttributeDefName"] = attribute_def_name + + body = {"WsRestGetAttributeAssignmentsLiteRequest": request} + + r = client._call_grouper( + "/attributeAssignments", body, act_as_subject=act_as_subject + ) + + return r From eee4429b1cbb2093c1be316ab063c5330250f62a Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Fri, 9 Jun 2023 10:59:30 -0500 Subject: [PATCH 04/12] rename get_privilege_on_this() make privilege plural, to get_privileges_on_this() --- grouper_python/objects/group.py | 2 +- grouper_python/objects/stem.py | 2 +- tests/test_group.py | 2 +- tests/test_privilege.py | 10 +++++----- tests/test_stem.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/grouper_python/objects/group.py b/grouper_python/objects/group.py index b208ae4..26c69aa 100644 --- a/grouper_python/objects/group.py +++ b/grouper_python/objects/group.py @@ -178,7 +178,7 @@ def delete_privilege_on_this( act_as_subject=act_as_subject, ) - def get_privilege_on_this( + def get_privileges_on_this( self, subject_id: str | None = None, subject_identifier: str | None = None, diff --git a/grouper_python/objects/stem.py b/grouper_python/objects/stem.py index 4004c7c..aacf2e8 100644 --- a/grouper_python/objects/stem.py +++ b/grouper_python/objects/stem.py @@ -98,7 +98,7 @@ def delete_privilege_on_this( act_as_subject=act_as_subject, ) - def get_privilege_on_this( + def get_privileges_on_this( self, subject_id: str | None = None, subject_identifier: str | None = None, diff --git a/tests/test_group.py b/tests/test_group.py index 88fd087..5f32e7d 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -154,7 +154,7 @@ def test_get_privilege(grouper_group: Group): json=data.get_priv_for_group_request, ).mock(return_value=Response(200, json=data.get_priv_for_group_result)) - privs = grouper_group.get_privilege_on_this() + privs = grouper_group.get_privileges_on_this() assert len(privs) == 1 diff --git a/tests/test_privilege.py b/tests/test_privilege.py index 1cfb93d..889ef76 100644 --- a/tests/test_privilege.py +++ b/tests/test_privilege.py @@ -27,7 +27,7 @@ def test_get_privilege_both_group_and_stem(grouper_subject: Subject): def test_get_privilege_both_subject_id_and_identifier(grouper_group: Group): with pytest.raises(ValueError) as excinfo: - grouper_group.get_privilege_on_this( + grouper_group.get_privileges_on_this( subject_id="abcd1234", subject_identifier="user1234" ) @@ -44,7 +44,7 @@ def test_get_privilege_subject_identifier(grouper_group: Group): json=data.get_priv_for_group_with_subject_identifier_request, ).mock(return_value=Response(200, json=data.get_priv_for_group_result)) - privs = grouper_group.get_privilege_on_this(subject_identifier="user3333") + privs = grouper_group.get_privileges_on_this(subject_identifier="user3333") assert len(privs) == 1 @@ -57,7 +57,7 @@ def test_get_privilege_with_privilege_name(grouper_group: Group): json=data.get_priv_for_group_with_privilege_name_request, ).mock(return_value=Response(200, json=data.get_priv_for_group_result)) - privs = grouper_group.get_privilege_on_this(privilege_name="admin") + privs = grouper_group.get_privileges_on_this(privilege_name="admin") assert len(privs) == 1 @@ -94,7 +94,7 @@ def test_get_privilege_subject_identifier_not_found(grouper_group: Group): ) with pytest.raises(GrouperSubjectNotFoundException) as excinfo: - grouper_group.get_privilege_on_this(subject_identifier="user3333") + grouper_group.get_privileges_on_this(subject_identifier="user3333") assert excinfo.value.subject_identifier == "user3333" @@ -106,7 +106,7 @@ def test_get_privilege_subject_id_not_found(grouper_group: Group): ) with pytest.raises(GrouperSubjectNotFoundException) as excinfo: - grouper_group.get_privilege_on_this(subject_id="abcd1234") + grouper_group.get_privileges_on_this(subject_id="abcd1234") assert excinfo.value.subject_identifier == "abcd1234" diff --git a/tests/test_stem.py b/tests/test_stem.py index 539857c..b1e3c82 100644 --- a/tests/test_stem.py +++ b/tests/test_stem.py @@ -35,7 +35,7 @@ def test_get_privilege(grouper_stem: Stem): json=data.get_priv_for_stem_request, ).mock(return_value=Response(200, json=data.get_priv_for_stem_result)) - privs = grouper_stem.get_privilege_on_this() + privs = grouper_stem.get_privileges_on_this() assert len(privs) == 1 From 732e9e6b2833fbbb5f92f7651d93360e70c4d55d Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Fri, 9 Jun 2023 11:00:26 -0500 Subject: [PATCH 05/12] test case where no privileges are returned --- tests/data.py | 4 ++++ tests/test_privilege.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/data.py b/tests/data.py index 2bc2657..9b87366 100644 --- a/tests/data.py +++ b/tests/data.py @@ -626,3 +626,7 @@ "resultMetadata": {"success": "F", "resultCode": "STEM_NOT_FOUND"} } } + +get_priv_for_group_result_none_found = { + "WsGetGrouperPrivilegesLiteResult": {"resultMetadata": {"success": "T"}} +} diff --git a/tests/test_privilege.py b/tests/test_privilege.py index 889ef76..b1205e9 100644 --- a/tests/test_privilege.py +++ b/tests/test_privilege.py @@ -49,6 +49,19 @@ def test_get_privilege_subject_identifier(grouper_group: Group): assert len(privs) == 1 +@respx.mock +def test_get_privilege_subject_identifier_none_found(grouper_group: Group): + respx.route( + method="POST", + url=data.URI_BASE + "/grouperPrivileges", + json=data.get_priv_for_group_with_subject_identifier_request, + ).mock(return_value=Response(200, json=data.get_priv_for_group_result_none_found)) + + privs = grouper_group.get_privileges_on_this(subject_identifier="user3333") + + assert len(privs) == 0 + + @respx.mock def test_get_privilege_with_privilege_name(grouper_group: Group): respx.route( From 83b96aaa565d7a95af0e4708e017d80af242f78f Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Fri, 9 Jun 2023 11:02:05 -0500 Subject: [PATCH 06/12] attribute work Make objects for attribute returns Get definitions and definition names --- grouper_python/attribute.py | 359 +++++++++++++++++++++++++--- grouper_python/objects/__init__.py | 10 + grouper_python/objects/attribute.py | 171 +++++++++++++ 3 files changed, 502 insertions(+), 38 deletions(-) create mode 100644 grouper_python/objects/attribute.py diff --git a/grouper_python/attribute.py b/grouper_python/attribute.py index d1df3e5..88b833a 100644 --- a/grouper_python/attribute.py +++ b/grouper_python/attribute.py @@ -1,75 +1,79 @@ """grouper-python.attribute - functions to interact with grouper attributess. These are "helper" functions that normally will not be called directly. -However these are still in development, so do not return Python types, +However these are still in development, and are not usable from any objects currently. They can be used by calling directly. """ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, overload, Literal if TYPE_CHECKING: # pragma: no cover - # from .objects.group import CreateGroup, Group from .objects.client import GrouperClient from .objects.subject import Subject -# from .objects.exceptions import ( -# GrouperGroupNotFoundException, -# GrouperSuccessException, -# GrouperStemNotFoundException, -# GrouperPermissionDenied, -# ) + from .objects.attribute import ( + AttributeDefinition, + AttributeDefinitionName, + AttributeAssignment, + ) def assign_attribute( attribute_assign_type: str, assign_operation: str, client: GrouperClient, - # owner_id: str | None = None, attribute_assign_id: str | None = None, owner_name: str | None = None, attribute_def_name_name: str | None = None, value: None | str | int | float = None, assign_value_operation: str | None = None, + *, + # raw: bool = False, act_as_subject: Subject | None = None, ) -> dict[str, Any]: """Assign an attribute. - :param attribute_assign_type: _description_ + :param attribute_assign_type: Type of attribute assignment, + currently only "group" is supported :type attribute_assign_type: str - :param assign_operation: _description_ + :param assign_operation: Assignment operation, one of + "assign_attr", "add_attr", "remove_attr", "replace_attrs" :type assign_operation: str - :param client: _description_ + :param client: The GrouperClient to use :type client: GrouperClient - :param attribute_assign_id: _description_, defaults to None + :param attribute_assign_id: Existing assignment id, if modifying + an existing assignment, defaults to None :type attribute_assign_id: str | None, optional - :param owner_name: _description_, defaults to None + :param owner_name: Name of owner to assign attribute to, + defaults to None :type owner_name: str | None, optional - :param attribute_def_name_name: _description_, defaults to None + :param attribute_def_name_name: Attribute definition name name to assign, + defaults to None :type attribute_def_name_name: str | None, optional - :param value: _description_, defaults to None + :param value: Value to assign, also requires assign_value_operation, + defaults to None :type value: None | str | int | float, optional - :param assign_value_operation: _description_, defaults to None + :param assign_value_operation: Value assignment operation, one of + "assign_value", "add_value", "remove_value", "replace_values", + requires value to be specified as well, defaults to None :type assign_value_operation: str | None, optional - :param act_as_subject: _description_, defaults to None + :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional - :raises ValueError: _description_ - :return: _description_ + :raises ValueError: An unknown or unsupported attribute_assign_type is given + :return: The raw result from the web services of the operation :rtype: dict[str, Any] """ request: dict[str, Any] = { - # "WsRestAssignAttributesLiteRequest": { - # "wsAttributeDefNameName": attribute_def_name_name, "attributeAssignType": attribute_assign_type, "attributeAssignOperation": assign_operation, } - # } if attribute_assign_type == "group": if owner_name: request["wsOwnerGroupLookups"] = [{"groupName": owner_name}] else: request["wsOwnerGroupLookups"] = [] - # request["wsOwnerGroupName"] = owner_name + # # These need to be converted from their "lite" equivalent # elif attribute_assign_type == "member": # request["wsOwnerSubjectIdentifier"] = owner_name # elif attribute_assign_type == "stem": @@ -92,10 +96,28 @@ def assign_attribute( r = client._call_grouper( "/attributeAssignments", body, act_as_subject=act_as_subject ) + # if raw: + # return r return r +@overload +def get_attribute_assignments( + attribute_assign_type: str, + client: GrouperClient, + attribute_def_name_name: str | None = None, + attribute_def_name: str | None = None, + owner_name: str | None = None, + include_assignments_on_assignments: str = "F", + *, + raw: Literal[False] = False, + act_as_subject: Subject | None = None, +) -> list[AttributeAssignment]: + ... + + +@overload def get_attribute_assignments( attribute_assign_type: str, client: GrouperClient, @@ -103,30 +125,64 @@ def get_attribute_assignments( attribute_def_name: str | None = None, owner_name: str | None = None, include_assignments_on_assignments: str = "F", - # value: None | str | int | float = None, - # assign_value_operation: str | None = None, + *, + raw: Literal[True], act_as_subject: Subject | None = None, ) -> dict[str, Any]: + ... + + +def get_attribute_assignments( + attribute_assign_type: str, + client: GrouperClient, + attribute_def_name_name: str | None = None, + attribute_def_name: str | None = None, + owner_name: str | None = None, + include_assignments_on_assignments: str = "F", + *, + raw: bool = False, + act_as_subject: Subject | None = None, +) -> list[AttributeAssignment] | dict[str, Any]: """Get attribute assignments. - :param attribute_assign_type: _description_ + :param attribute_assign_type: Type of attribute assignment, + must be a direct assignment type, one of + "group", "member", "stem", "any_mem", "imm_mem", "attr_def" :type attribute_assign_type: str - :param client: _description_ + :param client: The GrouperClient to use :type client: GrouperClient - :param attribute_def_name_name: _description_, defaults to None + :param attribute_def_name_name: Name of attribute definition name + to retrieve assignments for, defaults to None :type attribute_def_name_name: str | None, optional - :param attribute_def_name: _description_, defaults to None + :param attribute_def_name: Name of attribute defition + to retrieve assignments for, defaults to None :type attribute_def_name: str | None, optional - :param owner_name: _description_, defaults to None + :param owner_name: Owner to retrieve assignments for, + if specified, only group, member, stem, or attr_def + are allowed for attribute_assign_type, defaults to None :type owner_name: str | None, optional - :param include_assignments_on_assignments: _description_, defaults to "F" + :param include_assignments_on_assignments: Specify "T" to get + assignments on assignments, defaults to "F" :type include_assignments_on_assignments: str, optional - :param act_as_subject: _description_, defaults to None + :param raw: Whether to return a raw dictionary of results instead + of Python objects, defaults to False + :type raw: bool, optional + :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional - :raises ValueError: _description_ - :return: _description_ - :rtype: dict[str, Any] + :raises ValueError: An unknown or unsupported attribute_assign_type is given + :raises ValueError: The given attribute_assign_type is not supported as a Python + object, specify raw instead + :return:a list of AttributeAssignments or the raw dictionary result + from Grouper, depending on the value of raw + :rtype: list[AttributeAssignment] | dict[str, Any] """ + from .objects.attribute import ( + AttributeDefinition, + AttributeDefinitionName, + AttributeAssignment, + ) + from .objects.group import Group + request = { "attributeAssignType": attribute_assign_type, "includeAssignmentsOnAssignments": include_assignments_on_assignments, @@ -152,5 +208,232 @@ def get_attribute_assignments( r = client._call_grouper( "/attributeAssignments", body, act_as_subject=act_as_subject ) + if raw: + return r - return r + if "WsGetAttributeAssignmentsResults" not in r: + return [] + + results = r["WsGetAttributeAssignmentsResults"] + + ws_attribute_defs = results["wsAttributeDefs"] + ws_attribute_def_names = results["wsAttributeDefNames"] + attribute_defs = { + ws_attr_def["uuid"]: AttributeDefinition(client, ws_attr_def) + for ws_attr_def in ws_attribute_defs + } + attribute_def_names = { + ws_attr_def_name["uuid"]: AttributeDefinitionName( + client, ws_attr_def_name, attribute_defs[ws_attr_def_name["attributeDefId"]] + ) + for ws_attr_def_name in ws_attribute_def_names + } + + if attribute_assign_type == "group": + groups = {group["uuid"]: Group(client, group) for group in results["wsGroups"]} + return [ + AttributeAssignment( + client, + assg, + attribute_defs[assg["attributeDefId"]], + attribute_def_names[assg["attributeDefNameId"]], + group=groups[assg["ownerGroupId"]], + ) + for assg in results["wsAttributeAssigns"] + ] + else: + raise ValueError("Unknown or unsupported attributeAssignType given, use raw") + + +@overload +def get_attribute_definitions( + attribute_def_name: str, + client: GrouperClient, + scope: str | None = None, + split_scope: str = "F", + parent_stem_id: str | None = None, + stem_scope: str | None = None, + *, + raw: Literal[False] = False, + act_as_subject: Subject | None = None, +) -> list[AttributeDefinition]: + ... + + +@overload +def get_attribute_definitions( + attribute_def_name: str, + client: GrouperClient, + scope: str | None = None, + split_scope: str = "F", + parent_stem_id: str | None = None, + stem_scope: str | None = None, + *, + raw: Literal[True], + act_as_subject: Subject | None = None, +) -> dict[str, Any]: + ... + + +def get_attribute_definitions( + attribute_def_name: str, + client: GrouperClient, + scope: str | None = None, + split_scope: str = "F", + parent_stem_id: str | None = None, + stem_scope: str | None = None, + *, + raw: bool = False, + act_as_subject: Subject | None = None, +) -> list[AttributeDefinition] | dict[str, Any]: + """Get attribute definitions. + + Note: it appears that if the given attribute_def_name cannot + be found exactly, the endpoint will return all definitions that + match the remaining criteria. + + :param attribute_def_name: Name of attribute definition to get + :type attribute_def_name: str + :param client: The GrouperClient to use + :type client: GrouperClient + :param scope: Search string with % as wildcards will search name, + display name, description, defaults to None + :type scope: str | None, optional + :param split_scope: "T" or "F", if T will split the scope by whitespace, + and find attribute defs with each token, defaults to "F" + :type split_scope: str, optional + :param parent_stem_id: ID of parent stem to limit search for, + if specified stem_scope is also required, defaults to None + :type parent_stem_id: str | None, optional + :param stem_scope: Scope in stem to search for, + if specified must be one of "ONE_LEVEL" or "ALL_IN_SUBTREE", + and parent_stem_id must also be specified, defaults to None + :type stem_scope: str | None, optional + :param raw: Whether to return a raw dictionary of results instead + of Python objects, defaults to False + :type raw: bool, optional + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + :return: a list of AttributeDefinitions or the raw dictionary result + from Grouper, depending on the value of raw + :rtype: list[AttributeDefinition] | dict[str, Any] + """ + from .objects.attribute import AttributeDefinition + + body = { + "WsRestFindAttributeDefsLiteRequest": { + "nameOfAttributeDef": attribute_def_name, + } + } + if scope: + body["WsRestFindAttributeDefsLiteRequest"]["scope"] = scope + body["WsRestFindAttributeDefsLiteRequest"]["splitScope"] = split_scope + + if parent_stem_id and stem_scope: + body["WsRestFindAttributeDefsLiteRequest"]["parentStemId"] = parent_stem_id + body["WsRestFindAttributeDefsLiteRequest"]["stemScope"] = stem_scope + + r = client._call_grouper("/attributeDefs", body, act_as_subject=act_as_subject) + if raw: + return r + results = r["WsFindAttributeDefsResults"] + + if "attributeDefResults" in results: + return [ + AttributeDefinition(client, attribute_def_body) + for attribute_def_body in results["attributeDefResults"] + ] + else: + return [] + + +@overload +def get_attribute_definition_names( + client: GrouperClient, + attribute_def_name_name: str | None = None, + name_of_attribute_def: str | None = None, + scope: str | None = None, + *, + raw: Literal[False] = False, + act_as_subject: Subject | None = None, +) -> list[AttributeDefinitionName]: + ... + + +@overload +def get_attribute_definition_names( + client: GrouperClient, + attribute_def_name_name: str | None = None, + name_of_attribute_def: str | None = None, + scope: str | None = None, + *, + raw: Literal[True], + act_as_subject: Subject | None = None, +) -> dict[str, Any]: + ... + + +def get_attribute_definition_names( + client: GrouperClient, + attribute_def_name_name: str | None = None, + name_of_attribute_def: str | None = None, + scope: str | None = None, + *, + raw: bool = False, + act_as_subject: Subject | None = None, +) -> list[AttributeDefinitionName] | dict[str, Any]: + """Get Attribute Definition Names. + + :param client: The GrouperClient to use + :type client: GrouperClient + :param attribute_def_name_name: The name of the attribute definition name + to retrieve, defaults to None + :type attribute_def_name_name: str | None, optional + :param name_of_attribute_def: The name of the attribute definition to + retrieve attribute definition names for, defaults to None + :type name_of_attribute_def: str | None, optional + :param scope: Search string with % as wildcards, + will search name, display name, description, defaults to None + :type scope: str | None, optional + :param raw: Whether to return a raw dictionary of results instead + of Python objects, defaults to False + :type raw: bool, optional + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + :return: a list of AttributeDefinitionNames or the raw dictionary result + from Grouper, depending on the value of raw + :rtype: list[AttributeDefinitionName] | dict[str, Any] + """ + from .objects.attribute import AttributeDefinitionName, AttributeDefinition + + request: dict[str, str] = {} + if attribute_def_name_name: + request["attributeDefNameName"] = attribute_def_name_name + if name_of_attribute_def: + request["nameOfAttributeDef"] = name_of_attribute_def + if scope: + request["scope"] = scope + body = {"WsRestFindAttributeDefNamesLiteRequest": request} + + r = client._call_grouper("/attributeDefNames", body, act_as_subject=act_as_subject) + if raw: + return r + results = r["WsFindAttributeDefNamesResults"] + + if "attributeDefNameResults" not in results: + return [] + + ws_attribute_defs = results.get("attributeDefs", []) + ws_attribute_def_names = results["attributeDefNameResults"] + + attribute_defs = { + ws_attr_def["uuid"]: AttributeDefinition(client, ws_attr_def) + for ws_attr_def in ws_attribute_defs + } + + return [ + AttributeDefinitionName( + client, attr_name, attribute_defs[attr_name["attributeDefId"]] + ) + for attr_name in ws_attribute_def_names + ] diff --git a/grouper_python/objects/__init__.py b/grouper_python/objects/__init__.py index f3de13d..db5dbc2 100644 --- a/grouper_python/objects/__init__.py +++ b/grouper_python/objects/__init__.py @@ -6,6 +6,12 @@ from .subject import Subject from .privilege import Privilege from .membership import Membership, MemberType, MembershipType +from .attribute import ( + AttributeDefinition, + AttributeDefinitionName, + AttributeAssignment, + AttributeAssignmentValue +) __all__ = [ "Group", @@ -18,4 +24,8 @@ "Membership", "MemberType", "MembershipType", + "AttributeDefinition", + "AttributeDefinitionName", + "AttributeAssignment", + "AttributeAssignmentValue", ] diff --git a/grouper_python/objects/attribute.py b/grouper_python/objects/attribute.py new file mode 100644 index 0000000..317e6d0 --- /dev/null +++ b/grouper_python/objects/attribute.py @@ -0,0 +1,171 @@ +"""grouper_python.objects.attribute - Objects related to Grouper attributes.""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: # pragma: no cover + from .group import Group + from .stem import Stem + from .client import GrouperClient +from dataclasses import dataclass +from .base import GrouperEntity, GrouperBase + + +@dataclass(slots=True, eq=False) +class AttributeDefinition(GrouperEntity): + """AttributeDefinition object representing a Grouper attribute definition. + + :param client: A GrouperClient object containing connection information + :type client: GrouperClient + :param attribute_def_body: Body of the attribute definition + as returned by the Grouper API + :type attribute_def_body: dict[str, str] + """ + + extension: str + attributeDefType: str + valueType: str + assignToAttributeDef: str + assignToStem: str + assignToGroup: str + assignToMember: str + assignToEffectiveMembership: str + assignToImmediateMembership: str + assignToAttributeDefAssignment: str + assignToStemAssignment: str + assignToGroupAssignment: str + assignToMemberAssignment: str + assignToEffectiveMembershipAssignment: str + assignToImmediateMembershipAssignment: str + multiAssignable: str + multiValued: str + idIndex: str + + def __init__( + self, + client: GrouperClient, + attribute_def_body: dict[str, str] + ) -> None: + """Construct an AttributeDefinition.""" + self.client = client + self.description = attribute_def_body.get("description", "") + self.id = attribute_def_body["uuid"] + for key, value in attribute_def_body.items(): + if key not in ["description", "uuid"]: + setattr(self, key, value) + + +@dataclass(slots=True, eq=False) +class AttributeDefinitionName(GrouperEntity): + """AttributeDefinitionName object representing a Grouper attribute definition name. + + :param client: A GrouperClient object containing connection information + :type client: GrouperClient + :param attribute_def_name_body: Body of the attribute definition name + as returned by the Grouper API + :type attribute_def_name_body: dict[str, str] + :param attribute_def: AttributeDefinition object associated with this + attribute definition name + :type attribute_def: AttributeDefinition + """ + + displayExtension: str + extension: str + displayName: str + idIndex: str + attribute_definition: AttributeDefinition + + def __init__( + self, + client: GrouperClient, + attribute_def_name_body: dict[str, str], + attribute_def: AttributeDefinition + ) -> None: + """Construct an AttributeDefinitionName.""" + self.client = client + self.displayExtension = attribute_def_name_body["displayExtension"] + self.extension = attribute_def_name_body["extension"] + self.displayName = attribute_def_name_body["displayName"] + self.idIndex = attribute_def_name_body["idIndex"] + self.name = attribute_def_name_body["name"] + self.description = attribute_def_name_body.get("description", "") + self.id = attribute_def_name_body["uuid"] + self.attribute_definition = attribute_def + + +@dataclass +class AttributeAssignmentValue: + """Class representing an attribute assignment value.""" + + id: str + valueSystem: str + + +@dataclass(slots=True, eq=False) +class AttributeAssignment(GrouperBase): + """AttributeAssignemtn object representing a Grouper attribute assignment. + + :param client: A GrouperClient object containing connection information + :type client: GrouperClient + :param assign_body: Body of the assignment as returned by the Grouper API + :type assign_body: dict[str, Any] + :param attribute_def: AttributeDefinition object associated with this assignment + :type attribute_def: AttributeDefinition + :param attribute_def_name: AttributeDefinitionName object + associated with this assignment + :type attribute_def_name: AttributeDefinitionName + :param group: owner group of this assignment, defaults to None + :type group: Group | None, optional + :param stem: owner stem of this assignment, defaults to None + :type stem: Stem | None, optional + """ + + attributeAssignDelegatable: str + disallowed: str + createdOn: str + enabled: str + attributeAssignType: str + lastUpdated: str + attributeAssignActionId: str + attributeAssignActionName: str + attributeAssignActionType: str + owner: Group | Stem + group: Group | None + stem: Stem | None + values: list[AttributeAssignmentValue] + attribute_definition: AttributeDefinition + attribute_definition_name: AttributeDefinitionName + + def __init__( + self, + client: GrouperClient, + assign_body: dict[str, Any], + attribute_def: AttributeDefinition, + attribute_def_name: AttributeDefinitionName, + *, + group: Group | None = None, + stem: Stem | None = None + ) -> None: + """Construct an AttributeAssignment.""" + self.client = client + self.attributeAssignDelegatable = assign_body["attributeAssignDelegatable"] + self.disallowed = assign_body["disallowed"] + self.createdOn = assign_body["createdOn"] + self.enabled = assign_body["enabled"] + self.attributeAssignType = assign_body["attributeAssignType"] + self.lastUpdated = assign_body["lastUpdated"] + self.attributeAssignActionId = assign_body["attributeAssignActionId"] + self.attributeAssignActionName = assign_body["attributeAssignActionName"] + self.attributeAssignActionType = assign_body["attributeAssignActionType"] + self.group = group + self.stem = stem + if group: + self.owner = group + if stem: + self.owner = stem + self.values = [ + AttributeAssignmentValue(val["id"], val["valueSystem"]) + for val in assign_body["wsAttributeAssignValues"] + ] if "wsAttributeAssignValues" in assign_body else [] + self.attribute_definition = attribute_def + self.attribute_definition_name = attribute_def_name From ab7e0f51997ffde611c06b38eb5d9055822cf77b Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Fri, 9 Jun 2023 11:29:31 -0500 Subject: [PATCH 07/12] fix attribute bugs --- grouper_python/attribute.py | 6 +++--- grouper_python/objects/attribute.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/grouper_python/attribute.py b/grouper_python/attribute.py index 88b833a..35da99c 100644 --- a/grouper_python/attribute.py +++ b/grouper_python/attribute.py @@ -211,11 +211,11 @@ def get_attribute_assignments( if raw: return r - if "WsGetAttributeAssignmentsResults" not in r: - return [] - results = r["WsGetAttributeAssignmentsResults"] + if "wsAttributeAssigns" not in results: + return [] + ws_attribute_defs = results["wsAttributeDefs"] ws_attribute_def_names = results["wsAttributeDefNames"] attribute_defs = { diff --git a/grouper_python/objects/attribute.py b/grouper_python/objects/attribute.py index 317e6d0..769afaf 100644 --- a/grouper_python/objects/attribute.py +++ b/grouper_python/objects/attribute.py @@ -129,6 +129,7 @@ class AttributeAssignment(GrouperBase): attributeAssignActionId: str attributeAssignActionName: str attributeAssignActionType: str + id: str owner: Group | Stem group: Group | None stem: Stem | None @@ -157,6 +158,7 @@ def __init__( self.attributeAssignActionId = assign_body["attributeAssignActionId"] self.attributeAssignActionName = assign_body["attributeAssignActionName"] self.attributeAssignActionType = assign_body["attributeAssignActionType"] + self.id = assign_body["id"] self.group = group self.stem = stem if group: From 9bb65c2f18317c4ada6e4345f26ed5e60ca763f2 Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Fri, 9 Jun 2023 13:44:57 -0500 Subject: [PATCH 08/12] make get_attribute_assignments more robust Convert from Lite to regular request Allows for querying multiple defs and def_names and owners at the same time, will be useful later on --- grouper_python/attribute.py | 85 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/grouper_python/attribute.py b/grouper_python/attribute.py index 35da99c..260b2d7 100644 --- a/grouper_python/attribute.py +++ b/grouper_python/attribute.py @@ -106,9 +106,9 @@ def assign_attribute( def get_attribute_assignments( attribute_assign_type: str, client: GrouperClient, - attribute_def_name_name: str | None = None, - attribute_def_name: str | None = None, - owner_name: str | None = None, + attribute_def_name_names: list[str] = [], + attribute_def_names: list[str] = [], + owner_names: list[str] = [], include_assignments_on_assignments: str = "F", *, raw: Literal[False] = False, @@ -121,9 +121,9 @@ def get_attribute_assignments( def get_attribute_assignments( attribute_assign_type: str, client: GrouperClient, - attribute_def_name_name: str | None = None, - attribute_def_name: str | None = None, - owner_name: str | None = None, + attribute_def_name_names: list[str] = [], + attribute_def_names: list[str] = [], + owner_names: list[str] = [], include_assignments_on_assignments: str = "F", *, raw: Literal[True], @@ -135,9 +135,9 @@ def get_attribute_assignments( def get_attribute_assignments( attribute_assign_type: str, client: GrouperClient, - attribute_def_name_name: str | None = None, - attribute_def_name: str | None = None, - owner_name: str | None = None, + attribute_def_name_names: list[str] = [], + attribute_def_names: list[str] = [], + owner_names: list[str] = [], include_assignments_on_assignments: str = "F", *, raw: bool = False, @@ -151,16 +151,16 @@ def get_attribute_assignments( :type attribute_assign_type: str :param client: The GrouperClient to use :type client: GrouperClient - :param attribute_def_name_name: Name of attribute definition name - to retrieve assignments for, defaults to None - :type attribute_def_name_name: str | None, optional - :param attribute_def_name: Name of attribute defition - to retrieve assignments for, defaults to None - :type attribute_def_name: str | None, optional - :param owner_name: Owner to retrieve assignments for, + :param attribute_def_name_names: List of names of attribute definition names + to retrieve assignments for, defaults to [] + :type attribute_def_name_names: list[str], optional + :param attribute_def_names: List of names of attribute defitions + to retrieve assignments for, defaults to [] + :type attribute_def_names: list[str], optional + :param owner_names: List of owners to retrieve assignments for, if specified, only group, member, stem, or attr_def - are allowed for attribute_assign_type, defaults to None - :type owner_name: str | None, optional + are allowed for attribute_assign_type, defaults to [] + :type owner_names: list[str], optional :param include_assignments_on_assignments: Specify "T" to get assignments on assignments, defaults to "F" :type include_assignments_on_assignments: str, optional @@ -183,27 +183,30 @@ def get_attribute_assignments( ) from .objects.group import Group - request = { + request: dict[str, Any] = { "attributeAssignType": attribute_assign_type, "includeAssignmentsOnAssignments": include_assignments_on_assignments, } - if owner_name: - if attribute_assign_type == "group": - request["wsOwnerGroupName"] = owner_name - elif attribute_assign_type == "member": - request["wsOwnerSubjectIdentifier"] = owner_name - elif attribute_assign_type == "stem": - request["wsOwnerStemName"] = owner_name - elif attribute_assign_type == "attr_def": - request["wsOwnerAttributeDefName"] = owner_name - else: - raise ValueError("Unknown or unsupported attributeAssignType given") - if attribute_def_name_name: - request["wsAttributeDefNameName"] = attribute_def_name_name - if attribute_def_name: - request["wsAttributeDefName"] = attribute_def_name - body = {"WsRestGetAttributeAssignmentsLiteRequest": request} + if attribute_assign_type == "group": + request["wsOwnerGroupLookups"] = [{"groupName": name} for name in owner_names] + elif attribute_assign_type == "member": + request["wsOwnerSubjectLookups"] = [ + {"identifier": name} for name in owner_names + ] + elif attribute_assign_type == "stem": + request["wsOwnerStemLookups"] = [{"stemName": name} for name in owner_names] + elif attribute_assign_type == "attr_def": + request["wsOwnerAttributeLookups"] = [{"name": name} for name in owner_names] + else: + raise ValueError("Unknown or unsupported attributeAssignType given") + + request["wsAttributeDefNameLookups"] = [ + {"name": name} for name in attribute_def_name_names + ] + request["wsAttributeDefLookups"] = [{"name": name} for name in attribute_def_names] + + body = {"WsRestGetAttributeAssignmentsRequest": request} r = client._call_grouper( "/attributeAssignments", body, act_as_subject=act_as_subject @@ -218,13 +221,15 @@ def get_attribute_assignments( ws_attribute_defs = results["wsAttributeDefs"] ws_attribute_def_names = results["wsAttributeDefNames"] - attribute_defs = { + _attribute_defs = { ws_attr_def["uuid"]: AttributeDefinition(client, ws_attr_def) for ws_attr_def in ws_attribute_defs } - attribute_def_names = { + _attribute_def_names = { ws_attr_def_name["uuid"]: AttributeDefinitionName( - client, ws_attr_def_name, attribute_defs[ws_attr_def_name["attributeDefId"]] + client, + ws_attr_def_name, + _attribute_defs[ws_attr_def_name["attributeDefId"]], ) for ws_attr_def_name in ws_attribute_def_names } @@ -235,8 +240,8 @@ def get_attribute_assignments( AttributeAssignment( client, assg, - attribute_defs[assg["attributeDefId"]], - attribute_def_names[assg["attributeDefNameId"]], + _attribute_defs[assg["attributeDefId"]], + _attribute_def_names[assg["attributeDefNameId"]], group=groups[assg["ownerGroupId"]], ) for assg in results["wsAttributeAssigns"] From b090cf6484eac8d298dcc2285ba1400411f9a673 Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Tue, 13 Jun 2023 10:33:25 -0500 Subject: [PATCH 09/12] Attributes Assign attribute now returns an Attribute assignment add some class methods using attributes --- grouper_python/attribute.py | 102 ++++++++++++++++++++++++++-- grouper_python/objects/attribute.py | 19 ++++++ grouper_python/objects/group.py | 68 +++++++++++++++++++ 3 files changed, 183 insertions(+), 6 deletions(-) diff --git a/grouper_python/attribute.py b/grouper_python/attribute.py index 260b2d7..ec2710d 100644 --- a/grouper_python/attribute.py +++ b/grouper_python/attribute.py @@ -19,6 +19,24 @@ ) +@overload +def assign_attribute( + attribute_assign_type: str, + assign_operation: str, + client: GrouperClient, + attribute_assign_id: str | None = None, + owner_name: str | None = None, + attribute_def_name_name: str | None = None, + value: None | str | int | float = None, + assign_value_operation: str | None = None, + *, + raw: Literal[False] = False, + act_as_subject: Subject | None = None, +) -> list[AttributeAssignment]: + ... + + +@overload def assign_attribute( attribute_assign_type: str, assign_operation: str, @@ -29,9 +47,25 @@ def assign_attribute( value: None | str | int | float = None, assign_value_operation: str | None = None, *, - # raw: bool = False, + raw: Literal[True], act_as_subject: Subject | None = None, ) -> dict[str, Any]: + ... + + +def assign_attribute( + attribute_assign_type: str, + assign_operation: str, + client: GrouperClient, + attribute_assign_id: str | None = None, + owner_name: str | None = None, + attribute_def_name_name: str | None = None, + value: None | str | int | float = None, + assign_value_operation: str | None = None, + *, + raw: bool = False, + act_as_subject: Subject | None = None, +) -> list[AttributeAssignment] | dict[str, Any]: """Assign an attribute. :param attribute_assign_type: Type of attribute assignment, @@ -58,12 +92,23 @@ def assign_attribute( "assign_value", "add_value", "remove_value", "replace_values", requires value to be specified as well, defaults to None :type assign_value_operation: str | None, optional + :param raw: Whether to return a raw dictionary of results instead + of Python objects, defaults to False + :type raw: bool, optional :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional :raises ValueError: An unknown or unsupported attribute_assign_type is given - :return: The raw result from the web services of the operation - :rtype: dict[str, Any] + :return: A list of modified AttributeAssignments or the raw dictionary result + from Grouper, depending on the value of raw + :rtype: list[AttributeAssignment] | dict[str, Any] """ + from .objects.attribute import ( + AttributeDefinition, + AttributeDefinitionName, + AttributeAssignment, + ) + from .objects.group import Group + request: dict[str, Any] = { "attributeAssignType": attribute_assign_type, "attributeAssignOperation": assign_operation, @@ -96,10 +141,55 @@ def assign_attribute( r = client._call_grouper( "/attributeAssignments", body, act_as_subject=act_as_subject ) - # if raw: - # return r + if raw: + return r + + results = r["WsAssignAttributesResults"] + + ws_attribute_defs = results["wsAttributeDefs"] + ws_attribute_def_names = results["wsAttributeDefNames"] + _attribute_defs = { + ws_attr_def["uuid"]: AttributeDefinition(client, ws_attr_def) + for ws_attr_def in ws_attribute_defs + } + _attribute_def_names = { + ws_attr_def_name["uuid"]: AttributeDefinitionName( + client, + ws_attr_def_name, + _attribute_defs[ws_attr_def_name["attributeDefId"]], + ) + for ws_attr_def_name in ws_attribute_def_names + } + + r_list: list[AttributeAssignment] = [] + + if attribute_assign_type == "group": + groups = {group["uuid"]: Group(client, group) for group in results["wsGroups"]} + for assign_result in results["wsAttributeAssignResults"]: + for assg in assign_result["wsAttributeAssigns"]: + r_list.append( + AttributeAssignment( + client, + assg, + _attribute_defs[assg["attributeDefId"]], + _attribute_def_names[assg["attributeDefNameId"]], + group=groups[assg["ownerGroupId"]], + ) + ) + # return [ + # AttributeAssignment( + # client, + # assg, + # _attribute_defs[assg["attributeDefId"]], + # _attribute_def_names[assg["attributeDefNameId"]], + # group=groups[assg["ownerGroupId"]], + # ) + # for assg in results["wsAttributeAssigns"] + # ] + else: + raise ValueError("Unknown or unsupported attributeAssignType given, use raw") - return r + return r_list @overload diff --git a/grouper_python/objects/attribute.py b/grouper_python/objects/attribute.py index 769afaf..a7c4c13 100644 --- a/grouper_python/objects/attribute.py +++ b/grouper_python/objects/attribute.py @@ -7,8 +7,10 @@ from .group import Group from .stem import Stem from .client import GrouperClient + from .subject import Subject from dataclasses import dataclass from .base import GrouperEntity, GrouperBase +from ..attribute import assign_attribute @dataclass(slots=True, eq=False) @@ -171,3 +173,20 @@ def __init__( ] if "wsAttributeAssignValues" in assign_body else [] self.attribute_definition = attribute_def self.attribute_definition_name = attribute_def_name + + def delete( + self, + act_as_subject: Subject | None = None, + ) -> None: + """Delete this attribute assignment in Grouper. + + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + """ + assign_attribute( + attribute_assign_type=self.attributeAssignType, + assign_operation="remove_attr", + client=self.client, + attribute_assign_id=self.id, + act_as_subject=act_as_subject, + ) diff --git a/grouper_python/objects/group.py b/grouper_python/objects/group.py index 26c69aa..7bf707f 100644 --- a/grouper_python/objects/group.py +++ b/grouper_python/objects/group.py @@ -7,6 +7,7 @@ from .membership import Membership, HasMember from .client import GrouperClient from .privilege import Privilege + from .attribute import AttributeAssignment from .subject import Subject from dataclasses import dataclass from ..membership import ( @@ -16,6 +17,7 @@ delete_members_from_group, has_members, ) +from ..attribute import assign_attribute, get_attribute_assignments from ..privilege import assign_privilege, get_privileges from ..group import delete_groups @@ -321,6 +323,72 @@ def delete( act_as_subject=act_as_subject, ) + def assign_attribute_on_this( + self, + assign_operation: str, + attribute_def_name_name: str, + value: None | str | int | float = None, + assign_value_operation: str | None = None, + act_as_subject: Subject | None = None, + ) -> list[AttributeAssignment]: + """Assign an attribute on this group. + + :param assign_operation: Assignment operation, one of + "assign_attr", "add_attr", "remove_attr", "replace_attrs" + :type assign_operation: str + :param attribute_def_name_name: Attribute definition name name to assign + :type attribute_def_name_name: str + :param value: Value to assign, also requires assign_value_operation, + defaults to None + :type value: None | str | int | float, optional + :param assign_value_operation: Value assignment operation, one of + "assign_value", "add_value", "remove_value", "replace_values", + requires value to be specified as well, defaults to None + :type assign_value_operation: str | None, optional + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + :return: A list of modified AttributeAssignments + :rtype: list[AttributeAssignment] + """ + return assign_attribute( + attribute_assign_type="group", + assign_operation=assign_operation, + client=self.client, + owner_name=self.name, + attribute_def_name_name=attribute_def_name_name, + value=value, + assign_value_operation=assign_value_operation, + act_as_subject=act_as_subject, + ) + + def get_attribute_assignments_on_this( + self, + attribute_def_name_names: list[str] = [], + attribute_def_names: list[str] = [], + act_as_subject: Subject | None = None, + ) -> list[AttributeAssignment]: + """Get attribute assignments on this group. + + :param attribute_def_name_names: List of names of attribute definition names + to retrieve assignments for, defaults to [] + :type attribute_def_name_names: list[str], optional + :param attribute_def_names: List of names of attribute defitions + to retrieve assignments for, defaults to [] + :type attribute_def_names: list[str], optional + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + :return: A list of AttributeAssignments on this group + :rtype: list[AttributeAssignment] + """ + return get_attribute_assignments( + attribute_assign_type="group", + client=self.client, + attribute_def_name_names=attribute_def_name_names, + attribute_def_names=attribute_def_names, + owner_names=[self.name], + act_as_subject=act_as_subject, + ) + @dataclass class CreateGroup: From 1156509f0d25f090adf53b4e7d199e91898ccc68 Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Tue, 13 Jun 2023 15:18:59 -0500 Subject: [PATCH 10/12] add attribute methods to stems also add some tests --- grouper_python/attribute.py | 95 +++++++++++++++--------- grouper_python/objects/stem.py | 68 ++++++++++++++++++ tests/data.py | 128 +++++++++++++++++++++++++++++++++ tests/test_attribute.py | 99 +++++++++++++++++++++++++ 4 files changed, 355 insertions(+), 35 deletions(-) create mode 100644 tests/test_attribute.py diff --git a/grouper_python/attribute.py b/grouper_python/attribute.py index ec2710d..597f4ad 100644 --- a/grouper_python/attribute.py +++ b/grouper_python/attribute.py @@ -32,7 +32,7 @@ def assign_attribute( *, raw: Literal[False] = False, act_as_subject: Subject | None = None, -) -> list[AttributeAssignment]: +) -> list[AttributeAssignment]: # pragma: no cover ... @@ -49,7 +49,7 @@ def assign_attribute( *, raw: Literal[True], act_as_subject: Subject | None = None, -) -> dict[str, Any]: +) -> dict[str, Any]: # pragma: no cover ... @@ -108,6 +108,7 @@ def assign_attribute( AttributeAssignment, ) from .objects.group import Group + from .objects.stem import Stem request: dict[str, Any] = { "attributeAssignType": attribute_assign_type, @@ -118,14 +119,22 @@ def assign_attribute( request["wsOwnerGroupLookups"] = [{"groupName": owner_name}] else: request["wsOwnerGroupLookups"] = [] - # # These need to be converted from their "lite" equivalent - # elif attribute_assign_type == "member": - # request["wsOwnerSubjectIdentifier"] = owner_name - # elif attribute_assign_type == "stem": - # request["wsOwnerStemName"] = owner_name - # elif attribute_assign_type == "attr_def": - # request["wsOwnerAttributeDefName"] = owner_name - else: + elif attribute_assign_type == "stem": + if owner_name: + request["wsOwnerStemLookups"] = [{"stemName": owner_name}] + else: + request["wsOwnerStemLookups"] = [] + elif attribute_assign_type == "member": # pragma: no cover + if owner_name: + request["wsOwnerSubjectLookups"] = [{"identifier": owner_name}] + else: + request["wsOwnerSubjectLookups"] = [] + elif attribute_assign_type == "attr_def": # pragma: no cover + if owner_name: + request["wsOwnerAttributeLookups"] = [{"name": owner_name}] + else: + request["wsOwnerAttributeLookups"] = [] + else: # pragma: no cover raise ValueError("Unknown or unsupported attributeAssignType given") if attribute_assign_id: request["wsAttributeAssignLookups"] = [{"uuid": attribute_assign_id}] @@ -141,7 +150,7 @@ def assign_attribute( r = client._call_grouper( "/attributeAssignments", body, act_as_subject=act_as_subject ) - if raw: + if raw: # pragma: no cover return r results = r["WsAssignAttributesResults"] @@ -176,17 +185,20 @@ def assign_attribute( group=groups[assg["ownerGroupId"]], ) ) - # return [ - # AttributeAssignment( - # client, - # assg, - # _attribute_defs[assg["attributeDefId"]], - # _attribute_def_names[assg["attributeDefNameId"]], - # group=groups[assg["ownerGroupId"]], - # ) - # for assg in results["wsAttributeAssigns"] - # ] - else: + elif attribute_assign_type == "stem": + stems = {stem["uuid"]: Stem(client, stem) for stem in results["wsStems"]} + for assign_result in results["wsAttributeAssignResults"]: + for assg in assign_result["wsAttributeAssigns"]: + r_list.append( + AttributeAssignment( + client, + assg, + _attribute_defs[assg["attributeDefId"]], + _attribute_def_names[assg["attributeDefNameId"]], + stem=stems[assg["ownerStemId"]], + ) + ) + else: # pragma: no cover raise ValueError("Unknown or unsupported attributeAssignType given, use raw") return r_list @@ -203,7 +215,7 @@ def get_attribute_assignments( *, raw: Literal[False] = False, act_as_subject: Subject | None = None, -) -> list[AttributeAssignment]: +) -> list[AttributeAssignment]: # pragma: no cover ... @@ -218,7 +230,7 @@ def get_attribute_assignments( *, raw: Literal[True], act_as_subject: Subject | None = None, -) -> dict[str, Any]: +) -> dict[str, Any]: # pragma: no cover ... @@ -272,6 +284,7 @@ def get_attribute_assignments( AttributeAssignment, ) from .objects.group import Group + from .objects.stem import Stem request: dict[str, Any] = { "attributeAssignType": attribute_assign_type, @@ -280,15 +293,15 @@ def get_attribute_assignments( if attribute_assign_type == "group": request["wsOwnerGroupLookups"] = [{"groupName": name} for name in owner_names] - elif attribute_assign_type == "member": + elif attribute_assign_type == "stem": + request["wsOwnerStemLookups"] = [{"stemName": name} for name in owner_names] + elif attribute_assign_type == "member": # pragma: no cover request["wsOwnerSubjectLookups"] = [ {"identifier": name} for name in owner_names ] - elif attribute_assign_type == "stem": - request["wsOwnerStemLookups"] = [{"stemName": name} for name in owner_names] - elif attribute_assign_type == "attr_def": + elif attribute_assign_type == "attr_def": # pragma: no cover request["wsOwnerAttributeLookups"] = [{"name": name} for name in owner_names] - else: + else: # pragma: no cover raise ValueError("Unknown or unsupported attributeAssignType given") request["wsAttributeDefNameLookups"] = [ @@ -301,7 +314,7 @@ def get_attribute_assignments( r = client._call_grouper( "/attributeAssignments", body, act_as_subject=act_as_subject ) - if raw: + if raw: # pragma: no cover return r results = r["WsGetAttributeAssignmentsResults"] @@ -336,7 +349,19 @@ def get_attribute_assignments( ) for assg in results["wsAttributeAssigns"] ] - else: + elif attribute_assign_type == "stem": + stems = {stem["uuid"]: Stem(client, stem) for stem in results["wsStems"]} + return [ + AttributeAssignment( + client, + assg, + _attribute_defs[assg["attributeDefId"]], + _attribute_def_names[assg["attributeDefNameId"]], + stem=stems[assg["ownerStemId"]], + ) + for assg in results["wsAttributeAssigns"] + ] + else: # pragma: no cover raise ValueError("Unknown or unsupported attributeAssignType given, use raw") @@ -351,7 +376,7 @@ def get_attribute_definitions( *, raw: Literal[False] = False, act_as_subject: Subject | None = None, -) -> list[AttributeDefinition]: +) -> list[AttributeDefinition]: # pragma: no cover ... @@ -366,7 +391,7 @@ def get_attribute_definitions( *, raw: Literal[True], act_as_subject: Subject | None = None, -) -> dict[str, Any]: +) -> dict[str, Any]: # pragma: no cover ... @@ -451,7 +476,7 @@ def get_attribute_definition_names( *, raw: Literal[False] = False, act_as_subject: Subject | None = None, -) -> list[AttributeDefinitionName]: +) -> list[AttributeDefinitionName]: # pragma: no cover ... @@ -464,7 +489,7 @@ def get_attribute_definition_names( *, raw: Literal[True], act_as_subject: Subject | None = None, -) -> dict[str, Any]: +) -> dict[str, Any]: # pragma: no cover ... diff --git a/grouper_python/objects/stem.py b/grouper_python/objects/stem.py index aacf2e8..cc1e6be 100644 --- a/grouper_python/objects/stem.py +++ b/grouper_python/objects/stem.py @@ -7,10 +7,12 @@ from .subject import Subject from .group import Group from .privilege import Privilege + from .attribute import AttributeAssignment from ..privilege import assign_privilege, get_privileges from ..stem import create_stems, get_stems_by_parent, delete_stems from ..group import create_groups, get_groups_by_parent +from ..attribute import assign_attribute, get_attribute_assignments from .client import GrouperClient from dataclasses import dataclass from .base import GrouperEntity @@ -258,6 +260,72 @@ def delete( stem_names=[self.name], client=self.client, act_as_subject=act_as_subject ) + def assign_attribute_on_this( + self, + assign_operation: str, + attribute_def_name_name: str, + value: None | str | int | float = None, + assign_value_operation: str | None = None, + act_as_subject: Subject | None = None, + ) -> list[AttributeAssignment]: + """Assign an attribute on this stem. + + :param assign_operation: Assignment operation, one of + "assign_attr", "add_attr", "remove_attr", "replace_attrs" + :type assign_operation: str + :param attribute_def_name_name: Attribute definition name name to assign + :type attribute_def_name_name: str + :param value: Value to assign, also requires assign_value_operation, + defaults to None + :type value: None | str | int | float, optional + :param assign_value_operation: Value assignment operation, one of + "assign_value", "add_value", "remove_value", "replace_values", + requires value to be specified as well, defaults to None + :type assign_value_operation: str | None, optional + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + :return: A list of modified AttributeAssignments + :rtype: list[AttributeAssignment] + """ + return assign_attribute( + attribute_assign_type="stem", + assign_operation=assign_operation, + client=self.client, + owner_name=self.name, + attribute_def_name_name=attribute_def_name_name, + value=value, + assign_value_operation=assign_value_operation, + act_as_subject=act_as_subject, + ) + + def get_attribute_assignments_on_this( + self, + attribute_def_name_names: list[str] = [], + attribute_def_names: list[str] = [], + act_as_subject: Subject | None = None, + ) -> list[AttributeAssignment]: + """Get attribute assignments on this stem. + + :param attribute_def_name_names: List of names of attribute definition names + to retrieve assignments for, defaults to [] + :type attribute_def_name_names: list[str], optional + :param attribute_def_names: List of names of attribute defitions + to retrieve assignments for, defaults to [] + :type attribute_def_names: list[str], optional + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + :return: A list of AttributeAssignments on this stem + :rtype: list[AttributeAssignment] + """ + return get_attribute_assignments( + attribute_assign_type="stem", + client=self.client, + attribute_def_name_names=attribute_def_name_names, + attribute_def_names=attribute_def_names, + owner_names=[self.name], + act_as_subject=act_as_subject, + ) + @dataclass class CreateStem: diff --git a/tests/data.py b/tests/data.py index 9b87366..f885cd9 100644 --- a/tests/data.py +++ b/tests/data.py @@ -630,3 +630,131 @@ get_priv_for_group_result_none_found = { "WsGetGrouperPrivilegesLiteResult": {"resultMetadata": {"success": "T"}} } + +get_attribute_assignment_result_no_assignments = { + "WsGetAttributeAssignmentsResults": { + "resultMetadata": {"success": "T"}, + } +} + +attribute_def = { + "attributeDefType": "attr", + "assignToAttributeDef": "F", + "assignToStemAssignment": "F", + "extension": "prov_to_def", + "assignToMemberAssignment": "F", + "assignToEffectiveMembership": "F", + "uuid": "24b93ca5c9234d1ab8da393afcc24c60", + "assignToImmediateMembershipAssignment": "F", + "assignToEffectiveMembershipAssignment": "F", + "assignToStem": "F", + "assignToGroupAssignment": "F", + "assignToMember": "F", + "multiAssignable": "F", + "valueType": "string", + "name": "etc:attr_def", + "assignToAttributeDefAssignment": "F", + "idIndex": "1000017", + "multiValued": "F", + "assignToGroup": "F", + "assignToImmediateMembership": "F", +} + +attribute_def_name = { + "attributeDefId": "24b93ca5c9234d1ab8da393afcc24c60", + "displayExtension": "attr", + "extension": "attr", + "displayName": "attr", + "name": "etc:attr", + "attributeDefName": "etc:attr_def", + "idIndex": "1000076", + "uuid": "04d2177a412648b7a084f72f503bba4d", +} + +attribute_assignment_group = { + "attributeAssignDelegatable": "FALSE", + "disallowed": "F", + "createdOn": "2023/06/12 09:53:52.253", + "enabled": "T", + "attributeAssignType": "group", + "attributeDefId": "24b93ca5c9234d1ab8da393afcc24c60", + "lastUpdated": "2023/06/12 09:53:52.253", + "attributeAssignActionId": "ae72ff8bf5414933a9bf7fb6fdf04a28", + "ownerGroupName": "test:GROUP1", + "id": "48e69cfa6b2340b5907662a3b5e0d330", + "wsAttributeAssignValues": [ + {"id": "3017a079b5cc48e2a0126f8ba6f303d4", "valueSystem": "value"} + ], + "ownerGroupId": "1ab0482715c74f51bc32822a70bf8f77", + "attributeDefName": "etc:provision_to_def", + "attributeDefNameName": "etc:provision_to", + "attributeAssignActionName": "assign", + "attributeDefNameId": "04d2177a412648b7a084f72f503bba4d", + "attributeAssignActionType": "immediate", +} + +attribute_assignment_stem = { + "attributeAssignDelegatable": "FALSE", + "disallowed": "F", + "createdOn": "2023/06/12 09:53:52.253", + "enabled": "T", + "attributeAssignType": "stem", + "attributeDefId": "24b93ca5c9234d1ab8da393afcc24c60", + "lastUpdated": "2023/06/12 09:53:52.253", + "attributeAssignActionId": "ae72ff8bf5414933a9bf7fb6fdf04a28", + "ownerStemName": "test:child", + "id": "48e69cfa6b2340b5907662a3b5e0d330", + "wsAttributeAssignValues": [ + {"id": "3017a079b5cc48e2a0126f8ba6f303d4", "valueSystem": "value"} + ], + "ownerStemId": "e2c91c056fb746cca551d6887c722215", + "attributeDefName": "etc:provision_to_def", + "attributeDefNameName": "etc:provision_to", + "attributeAssignActionName": "assign", + "attributeDefNameId": "04d2177a412648b7a084f72f503bba4d", + "attributeAssignActionType": "immediate", +} + +get_attribute_assignment_result_group = { + "WsGetAttributeAssignmentsResults": { + "resultMetadata": {"success": "T"}, + "wsAttributeAssigns": [attribute_assignment_group], + "wsAttributeDefs": [attribute_def], + "wsAttributeDefNames": [attribute_def_name], + "wsGroups": [grouper_group_result1], + } +} + +assign_attribute_result_group = { + "WsAssignAttributesResults": { + "resultMetadata": {"success": "T"}, + "wsAttributeDefs": [attribute_def], + "wsAttributeDefNames": [attribute_def_name], + "wsGroups": [grouper_group_result1], + "wsAttributeAssignResults": [ + {"wsAttributeAssigns": [attribute_assignment_group]} + ], + } +} + +get_attribute_assignment_result_stem = { + "WsGetAttributeAssignmentsResults": { + "resultMetadata": {"success": "T"}, + "wsAttributeAssigns": [attribute_assignment_stem], + "wsAttributeDefs": [attribute_def], + "wsAttributeDefNames": [attribute_def_name], + "wsStems": [grouper_stem_1], + } +} + +assign_attribute_result_stem = { + "WsAssignAttributesResults": { + "resultMetadata": {"success": "T"}, + "wsAttributeDefs": [attribute_def], + "wsAttributeDefNames": [attribute_def_name], + "wsStems": [grouper_stem_1], + "wsAttributeAssignResults": [ + {"wsAttributeAssigns": [attribute_assignment_stem]} + ], + } +} diff --git a/tests/test_attribute.py b/tests/test_attribute.py new file mode 100644 index 0000000..1f63f7a --- /dev/null +++ b/tests/test_attribute.py @@ -0,0 +1,99 @@ +# mypy: allow_untyped_defs +from __future__ import annotations +from grouper_python.objects import Group, Stem +from . import data +# import pytest +import respx +from httpx import Response + + +@respx.mock +def test_group_get_attribute_assignments_none(grouper_group: Group): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + side_effect=[ + Response(200, json=data.get_attribute_assignment_result_no_assignments) + ] + ) + assgs = grouper_group.get_attribute_assignments_on_this() + + assert len(assgs) == 0 + + +@respx.mock +def test_group_get_attribute_assignments(grouper_group: Group): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + side_effect=[ + Response(200, json=data.get_attribute_assignment_result_group) + ] + ) + assgs = grouper_group.get_attribute_assignments_on_this() + + assert len(assgs) == 1 + + +@respx.mock +def test_group_assign_attribute(grouper_group: Group): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + side_effect=[ + Response(200, json=data.assign_attribute_result_group) + ] + ) + assgs = grouper_group.assign_attribute_on_this("assign_attr", "etc:attr") + assert len(assgs) == 1 + + +@respx.mock +def test_group_assign_attribute_with_value(grouper_group: Group): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + side_effect=[ + Response(200, json=data.assign_attribute_result_group) + ] + ) + assgs = grouper_group.assign_attribute_on_this( + "assign_attr", "etc:attr", "value", "assign_value") + assert len(assgs) == 1 + + +@respx.mock +def test_delete_group_attribute_assignment(grouper_group: Group): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + return_value=Response(200, json=data.assign_attribute_result_group) + ) + assgs = grouper_group.assign_attribute_on_this("assign_attr", "etc:attr") + assert len(assgs) == 1 + # If we've gotten this far, we have an assignment that we can then delete + assgs[0].delete() + + +@respx.mock +def test_stem_get_attribute_assignments(grouper_stem: Stem): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + side_effect=[ + Response(200, json=data.get_attribute_assignment_result_stem) + ] + ) + assgs = grouper_stem.get_attribute_assignments_on_this() + + assert len(assgs) == 1 + + +@respx.mock +def test_stem_assign_attribute(grouper_stem: Stem): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + side_effect=[ + Response(200, json=data.assign_attribute_result_stem) + ] + ) + assgs = grouper_stem.assign_attribute_on_this("assign_attr", "etc:attr") + assert len(assgs) == 1 + + +@respx.mock +def test_delete_stem_attribute_assignment(grouper_stem: Stem): + respx.post(url=data.URI_BASE + "/attributeAssignments").mock( + return_value=Response(200, json=data.assign_attribute_result_stem) + ) + assgs = grouper_stem.assign_attribute_on_this("assign_attr", "etc:attr") + assert len(assgs) == 1 + # If we've gotten this far, we have an assignment that we can then delete + assgs[0].delete() From b74a38217b8020ef2e63aaf1496afea573eafc81 Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Wed, 14 Jun 2023 10:20:45 -0500 Subject: [PATCH 11/12] swap assign privileges to non-lite endpoint allows specifying multiple entities and multiple permissions, so update methods and tests appropriately --- grouper_python/objects/group.py | 70 ++++++++++++++++++++++++++++----- grouper_python/objects/stem.py | 70 ++++++++++++++++++++++++++++----- grouper_python/privilege.py | 49 ++++++++++++----------- tests/data.py | 34 ++++++++-------- tests/test_group.py | 2 + tests/test_stem.py | 2 + tests/test_util.py | 6 +-- 7 files changed, 172 insertions(+), 61 deletions(-) diff --git a/grouper_python/objects/group.py b/grouper_python/objects/group.py index 7bf707f..08fb635 100644 --- a/grouper_python/objects/group.py +++ b/grouper_python/objects/group.py @@ -18,7 +18,7 @@ has_members, ) from ..attribute import assign_attribute, get_attribute_assignments -from ..privilege import assign_privilege, get_privileges +from ..privilege import assign_privileges, get_privileges from ..group import delete_groups @@ -145,11 +145,11 @@ def create_privilege_on_this( :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional """ - assign_privilege( - target=self.name, + assign_privileges( + target_name=self.name, target_type="group", - privilege_name=privilege_name, - entity_identifier=entity_identifier, + privilege_names=[privilege_name], + entity_identifiers=[entity_identifier], allowed="T", client=self.client, act_as_subject=act_as_subject, @@ -170,11 +170,63 @@ def delete_privilege_on_this( :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional """ - assign_privilege( - target=self.name, + assign_privileges( + target_name=self.name, target_type="group", - privilege_name=privilege_name, - entity_identifier=entity_identifier, + privilege_names=[privilege_name], + entity_identifiers=[entity_identifier], + allowed="F", + client=self.client, + act_as_subject=act_as_subject, + ) + + def create_privileges_on_this( + self, + entity_identifiers: list[str], + privilege_names: list[str], + act_as_subject: Subject | None = None, + ) -> None: + """Create privileges on this Group. + + :param entity_identifiers: List of identifiers of the entities + to receive the permissions + :type entity_identifiers: list[str] + :param privilege_names: List of names of the privileges to assign + :type privilege_names: list[str] + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + """ + assign_privileges( + target_name=self.name, + target_type="group", + privilege_names=privilege_names, + entity_identifiers=entity_identifiers, + allowed="T", + client=self.client, + act_as_subject=act_as_subject, + ) + + def delete_privileges_on_this( + self, + entity_identifiers: list[str], + privilege_names: list[str], + act_as_subject: Subject | None = None, + ) -> None: + """Delete privileges on this Group. + + :param entity_identifiers: List of identifiers of the entities + to receive the permissions + :type entity_identifiers: list[str] + :param privilege_names: List of names of the privileges to assign + :type privilege_names: list[str] + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + """ + assign_privileges( + target_name=self.name, + target_type="group", + privilege_names=privilege_names, + entity_identifiers=entity_identifiers, allowed="F", client=self.client, act_as_subject=act_as_subject, diff --git a/grouper_python/objects/stem.py b/grouper_python/objects/stem.py index cc1e6be..c3907bd 100644 --- a/grouper_python/objects/stem.py +++ b/grouper_python/objects/stem.py @@ -9,7 +9,7 @@ from .privilege import Privilege from .attribute import AttributeAssignment -from ..privilege import assign_privilege, get_privileges +from ..privilege import assign_privileges, get_privileges from ..stem import create_stems, get_stems_by_parent, delete_stems from ..group import create_groups, get_groups_by_parent from ..attribute import assign_attribute, get_attribute_assignments @@ -65,11 +65,11 @@ def create_privilege_on_this( :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional """ - assign_privilege( - target=self.name, + assign_privileges( + target_name=self.name, target_type="stem", - privilege_name=privilege_name, - entity_identifier=entity_identifier, + privilege_names=[privilege_name], + entity_identifiers=[entity_identifier], allowed="T", client=self.client, act_as_subject=act_as_subject, @@ -90,11 +90,63 @@ def delete_privilege_on_this( :param act_as_subject: Optional subject to act as, defaults to None :type act_as_subject: Subject | None, optional """ - assign_privilege( - target=self.name, + assign_privileges( + target_name=self.name, target_type="stem", - privilege_name=privilege_name, - entity_identifier=entity_identifier, + privilege_names=[privilege_name], + entity_identifiers=[entity_identifier], + allowed="F", + client=self.client, + act_as_subject=act_as_subject, + ) + + def create_privileges_on_this( + self, + entity_identifiers: list[str], + privilege_names: list[str], + act_as_subject: Subject | None = None, + ) -> None: + """Create privileges on this Stem. + + :param entity_identifiers: List of identifiers of the entities + to receive the permissions + :type entity_identifiers: list[str] + :param privilege_names: List of names of the privileges to assign + :type privilege_names: list[str] + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + """ + assign_privileges( + target_name=self.name, + target_type="stem", + privilege_names=privilege_names, + entity_identifiers=entity_identifiers, + allowed="T", + client=self.client, + act_as_subject=act_as_subject, + ) + + def delete_privileges_on_this( + self, + entity_identifiers: list[str], + privilege_names: list[str], + act_as_subject: Subject | None = None, + ) -> None: + """Delete privileges on this Stem. + + :param entity_identifiers: List of identifiers of the entities + to receive the permissions + :type entity_identifiers: list[str] + :param privilege_names: List of names of the privileges to assign + :type privilege_names: list[str] + :param act_as_subject: Optional subject to act as, defaults to None + :type act_as_subject: Subject | None, optional + """ + assign_privileges( + target_name=self.name, + target_type="stem", + privilege_names=privilege_names, + entity_identifiers=entity_identifiers, allowed="F", client=self.client, act_as_subject=act_as_subject, diff --git a/grouper_python/privilege.py b/grouper_python/privilege.py index 6cbe32c..522ffe1 100644 --- a/grouper_python/privilege.py +++ b/grouper_python/privilege.py @@ -8,7 +8,7 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover from .objects.client import GrouperClient @@ -22,26 +22,27 @@ ) -def assign_privilege( - target: str, +def assign_privileges( + target_name: str, target_type: str, - privilege_name: str, - entity_identifier: str, + privilege_names: list[str], + entity_identifiers: list[str], allowed: str, client: GrouperClient, act_as_subject: Subject | None = None, ) -> None: - """Assign (or remove) a permission. + """Assign (or remove) permissions. - :param target: Identifier of the target of the permission - :type target: str + :param target_name: Name of the target of the permission + :type target_name: str :param target_type: Type of target, either "stem" or "group" :type target_type: str - :param privilege_name: Name of the privilege to assign - :type privilege_name: str - :param entity_identifier: Identifier of the entity to receive the permission - :type entity_identifier: str - :param allowed: "T" to add the permission, "F" to remove it + :param privilege_names: List of names of the privileges to assign + :type privilege_names: list[str] + :param entity_identifiers: List of identifiers of the entities + to receive the permissions + :type entity_identifiers: list[str] + :param allowed: "T" to add the permissions, "F" to remove them :type allowed: str :param client: The GrouperClient to use :type client: GrouperClient @@ -50,23 +51,25 @@ def assign_privilege( :raises ValueError: An unknown/unsupported target_type is specified :raises GrouperSuccessException: An otherwise unhandled issue with the result """ - body = { - "WsRestAssignGrouperPrivilegesLiteRequest": { - "allowed": allowed, - "privilegeName": privilege_name, - "subjectIdentifier": entity_identifier, - } + request: dict[str, Any] = { + "allowed": allowed, + "privilegeNames": privilege_names, } + request["wsSubjectLookups"] = [ + {"subjectIdentifier": identifier} for identifier in entity_identifiers + ] if target_type == "stem": - body["WsRestAssignGrouperPrivilegesLiteRequest"]["stemName"] = target - body["WsRestAssignGrouperPrivilegesLiteRequest"]["privilegeType"] = "naming" + request["wsStemLookup"] = {"stemName": target_name} + request["privilegeType"] = "naming" elif target_type == "group": - body["WsRestAssignGrouperPrivilegesLiteRequest"]["groupName"] = target - body["WsRestAssignGrouperPrivilegesLiteRequest"]["privilegeType"] = "access" + request["wsGroupLookup"] = {"groupName": target_name} + request["privilegeType"] = "access" else: raise ValueError( f"Target type must be either 'stem' or 'group', but got '{target_type}'." ) + body = {"WsRestAssignGrouperPrivilegesRequest": request} + print(body) client._call_grouper( "/grouperPrivileges", body, diff --git a/tests/data.py b/tests/data.py index f885cd9..b8a4414 100644 --- a/tests/data.py +++ b/tests/data.py @@ -320,27 +320,27 @@ } create_priv_group_request = { - "WsRestAssignGrouperPrivilegesLiteRequest": { + "WsRestAssignGrouperPrivilegesRequest": { "allowed": "T", - "privilegeName": "update", - "subjectIdentifier": "user3333", - "groupName": "test:GROUP1", + "privilegeNames": ["update"], + "wsSubjectLookups": [{"subjectIdentifier": "user3333"}], + "wsGroupLookup": {"groupName": "test:GROUP1"}, "privilegeType": "access", } } delete_priv_group_request = { - "WsRestAssignGrouperPrivilegesLiteRequest": { + "WsRestAssignGrouperPrivilegesRequest": { "allowed": "F", - "privilegeName": "update", - "subjectIdentifier": "user3333", - "groupName": "test:GROUP1", + "privilegeNames": ["update"], + "wsSubjectLookups": [{"subjectIdentifier": "user3333"}], + "wsGroupLookup": {"groupName": "test:GROUP1"}, "privilegeType": "access", } } assign_priv_result_valid = { - "WsAssignGrouperPrivilegesLiteResult": {"resultMetadata": {"success": "T"}} + "WsAssignGrouperPrivilegesResults": {"resultMetadata": {"success": "T"}} } add_member_result_valid = { @@ -469,21 +469,21 @@ } create_priv_stem_request = { - "WsRestAssignGrouperPrivilegesLiteRequest": { + "WsRestAssignGrouperPrivilegesRequest": { "allowed": "T", - "privilegeName": "stemAttrRead", - "subjectIdentifier": "user3333", - "stemName": "test:child", + "privilegeNames": ["stemAttrRead"], + "wsSubjectLookups": [{"subjectIdentifier": "user3333"}], + "wsStemLookup": {"stemName": "test:child"}, "privilegeType": "naming", } } delete_priv_stem_request = { - "WsRestAssignGrouperPrivilegesLiteRequest": { + "WsRestAssignGrouperPrivilegesRequest": { "allowed": "F", - "privilegeName": "stemAttrRead", - "subjectIdentifier": "user3333", - "stemName": "test:child", + "privilegeNames": ["stemAttrRead"], + "wsSubjectLookups": [{"subjectIdentifier": "user3333"}], + "wsStemLookup": {"stemName": "test:child"}, "privilegeType": "naming", } } diff --git a/tests/test_group.py b/tests/test_group.py index 5f32e7d..6ffc3e6 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -133,6 +133,7 @@ def test_create_privilege(grouper_group: Group): ).mock(Response(200, json=data.assign_priv_result_valid)) grouper_group.create_privilege_on_this("user3333", "update") + grouper_group.create_privileges_on_this(["user3333"], ["update"]) @respx.mock @@ -144,6 +145,7 @@ def test_delete_privilege(grouper_group: Group): ).mock(return_value=Response(200, json=data.assign_priv_result_valid)) grouper_group.delete_privilege_on_this("user3333", "update") + grouper_group.delete_privileges_on_this(["user3333"], ["update"]) @respx.mock diff --git a/tests/test_stem.py b/tests/test_stem.py index b1e3c82..65b0c4c 100644 --- a/tests/test_stem.py +++ b/tests/test_stem.py @@ -14,6 +14,7 @@ def test_create_privilege(grouper_stem: Stem): ).mock(Response(200, json=data.assign_priv_result_valid)) grouper_stem.create_privilege_on_this("user3333", "stemAttrRead") + grouper_stem.create_privileges_on_this(["user3333"], ["stemAttrRead"]) @respx.mock @@ -25,6 +26,7 @@ def test_delete_privilege(grouper_stem: Stem): ).mock(return_value=Response(200, json=data.assign_priv_result_valid)) grouper_stem.delete_privilege_on_this("user3333", "stemAttrRead") + grouper_stem.delete_privileges_on_this(["user3333"], ["stemAttrRead"]) @respx.mock diff --git a/tests/test_util.py b/tests/test_util.py index ca52aaf..c9b81ff 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -12,7 +12,7 @@ from . import data import pytest from grouper_python.util import call_grouper -from grouper_python.privilege import assign_privilege +from grouper_python.privilege import assign_privileges from grouper_python.membership import has_members, get_members_for_groups from grouper_python.objects.exceptions import ( GrouperAuthException, @@ -174,8 +174,8 @@ def test_grouper_auth_exception(grouper_client: GrouperClient): @respx.mock def test_assign_privilege_unknown_target_type(grouper_client: GrouperClient): with pytest.raises(ValueError) as excinfo: - assign_privilege( - "target:name", "type", "update", "user1234", "T", grouper_client + assign_privileges( + "target:name", "type", ["update"], ["user1234"], "T", grouper_client ) assert ( excinfo.value.args[0] From 24a79180683d5c65e64ab89a50a9b08267e7f91a Mon Sep 17 00:00:00 2001 From: Peter Bajurny Date: Wed, 14 Jun 2023 12:43:24 -0500 Subject: [PATCH 12/12] Update privilege.py remove print statement --- grouper_python/privilege.py | 1 - 1 file changed, 1 deletion(-) diff --git a/grouper_python/privilege.py b/grouper_python/privilege.py index 522ffe1..5a1acd3 100644 --- a/grouper_python/privilege.py +++ b/grouper_python/privilege.py @@ -69,7 +69,6 @@ def assign_privileges( f"Target type must be either 'stem' or 'group', but got '{target_type}'." ) body = {"WsRestAssignGrouperPrivilegesRequest": request} - print(body) client._call_grouper( "/grouperPrivileges", body,