Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow service principals and nested groups to be returned in membership attributes #1507

Merged
merged 8 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 48 additions & 13 deletions plugins/modules/azure_rm_adgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
- The azure ad objects asserted to not be owners of the group.
type: list
elements: str
raw_membership:
description:
- By default the group_members return property is flattened and partially filtered of non-User objects before return. \
This argument disables those transformations.
default: false
type: bool
description:
description:
- An optional description for the group.
Expand Down Expand Up @@ -109,6 +115,15 @@
- "{{ ad_object_1_object_id }}"
- "{{ ad_object_2_object_id }}"

- name: Ensure Users are Members of a Group using object_id. Specify the group_membership return should be unfiltered
azure_rm_adgroup:
object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
state: 'present'
present_members:
- "{{ ad_object_1_object_id }}"
- "{{ ad_object_2_object_id }}"
raw_membership: true

- name: Ensure Users are not Members of a Group using display_name and mail_nickname
azure_rm_adgroup:
display_name: "Group-Name"
Expand All @@ -117,7 +132,7 @@
absent_members:
- "{{ ad_object_1_object_id }}"

- name: Ensure Users are Members of a Group using object_id
- name: Ensure Users are not Members of a Group using object_id
azure_rm_adgroup:
object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
state: 'present'
Expand Down Expand Up @@ -150,7 +165,7 @@
- "{{ ad_object_1_object_id }}"
- "{{ ad_object_2_object_id }}"

- name: Ensure Users are Owners of a Group using object_id
- name: Ensure Users are not Owners of a Group using object_id
azure_rm_adgroup:
object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
state: 'present'
Expand Down Expand Up @@ -203,7 +218,7 @@
type: list
group_members:
description:
- The members of the group.
- The members of the group. If raw_membership is false, this contains the transitive members property. Otherwise, it contains the members property.
returned: always
type: list
description:
Expand All @@ -222,6 +237,7 @@
from msgraph.generated.models.group import Group
from msgraph.generated.groups.item.transitive_members.transitive_members_request_builder import \
TransitiveMembersRequestBuilder
from msgraph.generated.groups.item.group_item_request_builder import GroupItemRequestBuilder
from msgraph.generated.models.reference_create import ReferenceCreate
except ImportError:
# This is handled in azure_rm_common
Expand All @@ -239,6 +255,7 @@ def __init__(self):
present_owners=dict(type='list', elements='str'),
absent_members=dict(type='list', elements='str'),
absent_owners=dict(type='list', elements='str'),
raw_membership=dict(type='bool', default=False),
description=dict(type='str'),
state=dict(
type='str',
Expand All @@ -257,6 +274,7 @@ def __init__(self):
self.state = None
self.results = dict(changed=False)
self._client = None
self.raw_membership = False

super(AzureRMADGroup, self).__init__(derived_arg_spec=self.module_arg_spec,
supports_check_mode=False,
Expand All @@ -267,9 +285,6 @@ def exec_module(self, **kwargs):
for key in list(self.module_arg_spec.keys()):
setattr(self, key, kwargs[key])

# TODO remove ad_groups return. Returns as one object always
ad_groups = []

try:
self._client = self.get_msgraph_client()
ad_groups = []
Expand Down Expand Up @@ -325,7 +340,7 @@ def update_members(self, group_id):

if self.present_members or self.absent_members:
ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(group_id))
current_members = [object.id for object in ret.value]
current_members = [object.id for object in ret]

if self.present_members:
present_members_by_object_id = self.dictionary_from_object_urls(self.present_members)
Expand Down Expand Up @@ -361,15 +376,15 @@ def update_owners(self, group_id):
if owners_to_add:
for owner_object_id in owners_to_add:
asyncio.get_event_loop().run_until_complete(
self.add_gropup_owner(group_id, present_owners_by_object_id[owner_object_id]))
self.add_group_owner(group_id, present_owners_by_object_id[owner_object_id]))
self.results["changed"] = True

if self.absent_owners:
owners_to_remove = list(set(self.absent_owners).intersection(set(current_owners)))

if owners_to_remove:
for owner in owners_to_remove:
asyncio.get_event_loop().run_until_complete(self.remove_gropup_owner(group_id, owner))
asyncio.get_event_loop().run_until_complete(self.remove_group_owner(group_id, owner))
self.results["changed"] = True

def dictionary_from_object_urls(self, object_urls):
Expand Down Expand Up @@ -439,7 +454,7 @@ def set_results(self, object):

if results["object_id"] and (self.present_members or self.absent_members):
ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(results["object_id"]))
results["group_members"] = [self.result_to_dict(object) for object in ret.value]
results["group_members"] = [self.result_to_dict(object) for object in ret]

return results

Expand Down Expand Up @@ -469,15 +484,35 @@ async def get_group_list(self, filter=None):
return []

async def get_group_members(self, group_id, filters=None):
if self.raw_membership:
return await self.get_raw_group_members(group_id, filters)
else:
return await self.get_transitive_group_members(group_id, filters)

async def get_transitive_group_members(self, group_id, filters=None):
request_configuration = TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetRequestConfiguration(
query_parameters=TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetQueryParameters(
count=True,
),
)
if filters:
request_configuration.query_parameters.filter = filters
return await self._client.groups.by_group_id(group_id).transitive_members.get(
response = await self._client.groups.by_group_id(group_id).transitive_members.get(
request_configuration=request_configuration)
return response.value

async def get_raw_group_members(self, group_id, filters=None):
request_configuration = GroupItemRequestBuilder.GroupItemRequestBuilderGetRequestConfiguration(
query_parameters=GroupItemRequestBuilder.GroupItemRequestBuilderGetQueryParameters(
# this ensures service principals are returned
# see https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0&tabs=http
expand=["members"]
),
)
if filters:
request_configuration.query_parameters.filter = filters
group = await self._client.groups.by_group_id(group_id).get(request_configuration=request_configuration)
return group.members

async def add_group_member(self, group_id, obj_id):
request_body = ReferenceCreate(
Expand All @@ -496,13 +531,13 @@ async def get_group_owners(self, group_id):
)
return await self._client.groups.by_group_id(group_id).owners.get(request_configuration=request_configuration)

async def add_gropup_owner(self, group_id, obj_id):
async def add_group_owner(self, group_id, obj_id):
request_body = ReferenceCreate(
odata_id="https://graph.microsoft.com/v1.0/users/{0}".format(obj_id),
)
await self._client.groups.by_group_id(group_id).owners.ref.post(body=request_body)

async def remove_gropup_owner(self, group_id, obj_id):
async def remove_group_owner(self, group_id, obj_id):
await self._client.groups.by_group_id(group_id).owners.by_directory_object_id(obj_id).ref.delete()


Expand Down
46 changes: 39 additions & 7 deletions plugins/modules/azure_rm_adgroup_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@
- Indicate whether the groups in which a groups is a member should be returned with the returned groups.
default: False
type: bool
raw_membership:
description:
- By default the group_members return property is flattened and partially filtered of non-User objects before return.\
This argument disables those transformations.
default: false
type: bool
all:
description:
- If True, will return all groups in tenant.
Expand Down Expand Up @@ -84,6 +90,12 @@
object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return_owners: true
return_group_members: true
- name: Return a specific group using object_id and return the owners and members of the group. Return service principals and nested groups.
azure_rm_adgroup_info:
object_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
return_owners: true
return_group_members: true
raw_membership: true

- name: Return a specific group using object_id and return the groups the group is a member of
azure_rm_adgroup_info:
Expand Down Expand Up @@ -153,7 +165,7 @@
type: list
group_members:
description:
- The members of the group.
- The members of the group. If raw_membership is set, this field may contain non-user objects (groups, service principals, etc)
returned: always
type: list
description:
Expand All @@ -173,6 +185,7 @@
TransitiveMembersRequestBuilder
from msgraph.generated.groups.item.get_member_groups.get_member_groups_post_request_body import \
GetMemberGroupsPostRequestBody
from msgraph.generated.groups.item.group_item_request_builder import GroupItemRequestBuilder
except ImportError:
# This is handled in azure_rm_common
pass
Expand All @@ -190,6 +203,7 @@ def __init__(self):
return_owners=dict(type='bool', default=False),
return_group_members=dict(type='bool', default=False),
return_member_groups=dict(type='bool', default=False),
raw_membership=dict(type='bool', default=False),
all=dict(type='bool', default=False),
)

Expand All @@ -201,6 +215,7 @@ def __init__(self):
self.return_owners = False
self.return_group_members = False
self.return_member_groups = False
self.raw_membership = False
self.all = False

self.results = dict(changed=False)
Expand Down Expand Up @@ -301,7 +316,7 @@ def set_results(self, object):

if results["object_id"] and self.return_group_members:
ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(results["object_id"]))
results["group_members"] = [self.result_to_dict(object) for object in ret.value]
results["group_members"] = [self.result_to_dict(object) for object in ret]

if results["object_id"] and self.return_member_groups:
ret = asyncio.get_event_loop().run_until_complete(self.get_member_groups(results["object_id"]))
Expand All @@ -310,7 +325,7 @@ def set_results(self, object):
if results["object_id"] and self.check_membership:
filter = "id eq '{0}' ".format(self.check_membership)
ret = asyncio.get_event_loop().run_until_complete(self.get_group_members(results["object_id"], filter))
results["is_member_of"] = True if ret.value and len(ret.value) != 0 else False
results["is_member_of"] = True if ret and len(ret) != 0 else False

return results

Expand Down Expand Up @@ -352,17 +367,34 @@ async def get_group_owners(self, group_id):
return await self._client.groups.by_group_id(group_id).owners.get(request_configuration=request_configuration)

async def get_group_members(self, group_id, filters=None):
if self.raw_membership:
return await self.get_raw_group_members(group_id, filters)
else:
return await self.get_transitive_group_members(group_id, filters)

async def get_transitive_group_members(self, group_id, filters=None):
request_configuration = TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetRequestConfiguration(
query_parameters=TransitiveMembersRequestBuilder.TransitiveMembersRequestBuilderGetQueryParameters(
count=True,
select=['id', 'displayName', 'userPrincipalName', 'mailNickname', 'mail', 'accountEnabled', 'userType',
'appId', 'appRoleAssignmentRequired']

),
)
if filters:
request_configuration.query_parameters.filter = filters
return await self._client.groups.by_group_id(group_id).transitive_members.get(
response = await self._client.groups.by_group_id(group_id).transitive_members.get(
request_configuration=request_configuration)
return response.value

async def get_raw_group_members(self, group_id, filters=None):
request_configuration = GroupItemRequestBuilder.GroupItemRequestBuilderGetRequestConfiguration(
query_parameters=GroupItemRequestBuilder.GroupItemRequestBuilderGetQueryParameters(
# this ensures service principals are returned
# see https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0&tabs=http
expand=["members"]
),
)
if filters:
request_configuration.query_parameters.filter = filters
return await self._client.groups.by_group_id(group_id).members.get(
request_configuration=request_configuration)

async def get_member_groups(self, obj_id):
Expand Down