Querying group memberships
This guide demonstrates how to transitively query group memberships and retrieve the membership graph of a member.
In addition to listing the direct members of a group, you can transitively search for both direct and indirect memberships and view the membership graph of a specific member. These capabilities address the following use cases:
- Resource owners can make more informed decisions about resource ACL changes by understanding which groups and members are affected by the changes.
- Group owners can assess the impact of adding or removing a group from a group related to ACL control, and more easily resolve membership concerns.
- Security auditors can more effectively audit access policy because the expanded membership structure of their entire organization is visible.
- Security auditors can assess the security risk of a member by viewing all of their direct and indirect group memberships, or checking if a member belongs to a specific group.
A group membership can belong to an individual, a service account, or another group.
The user or service account that is making the query must have permission to view the memberships of all groups that are part of the query, otherwise the request will fail. If the query returns an "PERMISSION_DENIED" error, it's likely that you don't have the correct permissions for one of the nested groups, especially if one of them is a group owned by another organization.
Before you begin
Enable the Cloud Identity API.
Searching for all memberships in a group
This code returns all memberships of a group. The response includes the type of membership (direct, indirect, or both) for each membership.
REST
To get a list of all of the memberships in a group, call
groups.memberships.searchTransitiveMemberships()
with the ID of the parent group.
Python
To authenticate to Cloud Identity, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.
import googleapiclient.discovery
from urllib.parse import urlencode
def search_transitive_memberships(service, parent, page_size):
try:
memberships = []
next_page_token = ''
while True:
query_params = urlencode(
{
"page_size": page_size,
"page_token": next_page_token
}
)
request = service.groups().memberships().searchTransitiveMemberships(parent=parent)
request.uri += "&" + query_params
response = request.execute()
if 'memberships' in response:
memberships += response['memberships']
if 'nextPageToken' in response:
next_page_token = response['nextPageToken']
else:
next_page_token = ''
if len(next_page_token) == 0:
break;
print(memberships)
except Exception as e:
print(e)
def main():
service = googleapiclient.discovery.build('cloudidentity', 'v1')
# Return results with a page size of 50
search_transitive_memberships(service, 'groups/GROUP_ID', 50)
if __name__ == '__main__':
main()
Searching for all group memberships of a member
REST
To find all of the groups that a member belongs to, call
groups.memberships.searchTransitiveGroups()
with the member key (for example, the email address of the member).
Python
To authenticate to Cloud Identity, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.
This code returns all groups that a member belongs to (except identity-mapped groups), both directly and indirectly.
import googleapiclient.discovery
from urllib.parse import urlencode
def search_transitive_groups(service, member, page_size):
try:
groups = []
next_page_token = ''
while True:
query_params = urlencode(
{
"query": "member_key_id == '{}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels".format(member),
"page_size": page_size,
"page_token": next_page_token
}
)
request = service.groups().memberships().searchTransitiveGroups(parent='groups/-')
request.uri += "&" + query_params
response = request.execute()
if 'memberships' in response:
groups += response['memberships']
if 'nextPageToken' in response:
next_page_token = response['nextPageToken']
else:
next_page_token = ''
if len(next_page_token) == 0:
break;
print(groups)
except Exception as e:
print(e)
def main():
service = googleapiclient.discovery.build('cloudidentity', 'v1')
# Return results with a page size of 50
search_transitive_groups(service, 'MEMBER_EMAIL_ADDRESS', 50)
if __name__ == '__main__':
main()
Checking membership in a group
REST
To check whether a member belongs to a specific group (either directly or
indirectly), call
checkTransitiveMembership()
with the ID of the parent group and the member key (for example, the email
address of the member).
Python
To authenticate to Cloud Identity, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.
The following code determines whether the member belongs to a specific group:
import googleapiclient.discovery
from urllib.parse import urlencode
def check_transitive_membership(service, parent, member):
try:
query_params = urlencode(
{
"query": "member_key_id == '{}'".format(member)
}
)
request = service.groups().memberships().checkTransitiveMembership(parent=parent)
request.uri += "&" + query_params
response = request.execute()
print(response['hasMembership'])
except Exception as e:
print(e)
def main():
service = googleapiclient.discovery.build('cloudidentity', 'v1')
check_transitive_membership(service, 'groups/GROUP_ID', 'MEMBER_EMAIL_ADDRESS')
if __name__ == '__main__':
main()
Retrieving the membership graph for a member
REST
To get the membership graph of a member (all the groups that a member belongs
to, along with the path information), call
groups.memberships.getMembershipGraph()
with the ID of the parent group and the member key (for example, the email
address of the member). The graph is returned as an adjacency list.
Python
To authenticate to Cloud Identity, set up Application Default Credentials. For more information, see Set up authentication for a local development environment.
The following code returns the membership graph of a specified member in a Google Group (this query is filtered by group type using the label):
import googleapiclient.discovery
from urllib.parse import urlencode
def get_membership_graph(service, parent, member):
try:
query_params = urlencode(
{
"query": "member_key_id == '{}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels".format(member)
}
)
request = service.groups().memberships().getMembershipGraph(parent=parent)
request.uri += "&" + query_params
response = request.execute()
print(response['response'])
except Exception as e:
print(e)
def main()
service = googleapiclient.discovery.build('cloudidentity', 'v1')
# Specify parent group as 'groups/-' to get ALL the groups of a member
# along with path information
get_membership_graph(service, 'groups/GROUP_ID', 'MEMBER_KEY')
if __name__ == '__main__':
main()
Creating a visual representation of the membership graph
The following is a sample response from the Python code above. In this example, groups 000, 111, and 222 are connected as follows (arrows are from parent to child): 000 -> 111 -> 222. A call to the sample code to retrieve the complete graph for group 222:
get_membership_graph(service, 'groups/-', '[email protected]')
results in the following response:
{
"@type": "type.googleapis.com/google.apps.cloudidentity.groups.v1.GetMembershipGraphResponse",
"adjacencyList": [
{
"edges": [
{
"name": "groups/000/memberships/111",
"preferredMemberKey": {
"id": "[email protected]"
},
"roles": [
{
"name": "MEMBER"
}
]
}
],
"group": "groups/000"
},
{
"edges": [
{
"name": "groups/111/memberships/222",
"preferredMemberKey": {
"id": "[email protected]"
},
"roles": [
{
"name": "MEMBER"
}
]
}
],
"group": "groups/111"
}
],
"groups": [
{
"name": "groups/000",
"groupKey": {
"id": "[email protected]"
},
"displayName": "Group - 0",
"description": "Group - 0",
"labels": {
"cloudidentity.googleapis.com/groups.discussion_forum": ""
}
},
{
"name": "groups/111",
"groupKey": {
"id": "[email protected]"
},
"displayName": "Group - 1",
"description": "Group - 1",
"labels": {
"cloudidentity.googleapis.com/groups.discussion_forum": ""
}
},
{
"name": "groups/222",
"groupKey": {
"id": "[email protected]"
},
"displayName": "Group - 2",
"description": "Group - 2",
"labels": {
"cloudidentity.googleapis.com/groups.discussion_forum": ""
}
}
]
}
Each item in the adjacency list represents a group and its direct members (edges) and the response also includes details of all groups in the membership graph. It can be parsed to generate alternative representations (for example, a DOT graph) which can be used to visualize the membership graph.
This sample script can be used convert the response to a DOT graph:
#
# Generates output in a dot format. Invoke this method using
# response['response'] from get_membership_graph()
#
# Save the output to a .dot file (say graph.dot)
# Use the dot tool to generate a visualization of the graph
# Example:
# dot -Tpng -o graph.png graph.dot
#
# Generates output like below:
#
# digraph {
# 'group0' [label='groups/000 (GROUP 0)'];
# 'group1' [label='groups/111 (GROUP 1)'];
# 'group2' [label='groups/222 (GROUP 2)'];
# 'group3' [label='groups/333 (GROUP 3)'];
# 'group4' [label='groups/444 (GROUP 4)'];
#
# 'group0' -> 'group1' [label='[email protected] (MEMBER)'];
# 'group0' -> 'group2' [label='[email protected] (MEMBER)'];
# 'group1' -> 'group3' [label='[email protected] (MEMBER)'];
# 'group3' -> 'group4' [label='[email protected] (MEMBER)'];
# 'group2' -> 'group3' [label='[email protected] (MEMBER)'];
# }
#
def convert_to_dot_format(graph):
output = "digraph {\n"
try:
# Generate labels for the group nodes
for group in graph['groups']:
if 'displayName' in group:
label = '{} ({})'.format(group['name'], group['displayName'])
else:
label = group['name']
output += ' "{}" [label="{}"];\n'.format(group['name'].split('/')[1], label)
output += '\n'
# Generate edges
for item in graph['adjacencyList']:
group_id = item['group'].split('/')[1]
for edge in item['edges']:
edge_to = edge['name'].split('/')[3]
edge_key = edge['preferredMemberKey']['id']
# Collect the roles
roles = []
for role in edge['roles']:
roles.append(role['name'])
output += ' "{}" -> "{}" [label="{} ({})"];\n'.format(group_id,
edge_to,
edge_key,
','.join(roles))
output += "}\n"
print(output)
except Exception as e:
print(e)
The following is the resulting visual hierarchy for the sample response: