Skip to content

Commit

Permalink
Implementing the ability to list SCIM groups and their membership
Browse files Browse the repository at this point in the history
  • Loading branch information
Vitor-Avila committed Nov 6, 2022
1 parent 3ceedca commit 1ec1ce4
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ The following commands are currently available:
- ``preset-cli auth``: store authentication credentials.
- ``preset-cli invite-users``: invite users to Preset.
- ``preset-cli import-users``: automatically add users to Preset.
- ``preset-cli list-group-membership``: List SCIM groups from a team and their memberships.
- ``preset-cli superset sql``: run SQL interactively or programmatically against an analytical database.
- ``preset-cli superset export-assets``: export resources (databases, datasets, charts, dashboards) into a directory as YAML files.
- ``preset-cli superset export-ownership``: export resource ownership (UUID -> email) into a YAML file.
Expand Down Expand Up @@ -470,3 +471,7 @@ Exporting ownership
~~~~~~~~~~~~~~~~~~~
The ``preset-cli superset export-ownership`` command generates a YAML file with information about ownership of different resources. The file maps resource UUIDs to user email address, and in the future will be used to recreate ownership on a different instance of Superset.
Listing SCIM Groups
~~~~~~~~~~~~~~~~~~~
The ``preset-cli list-group-membership`` command prints all SCIM groups (including membership) associated with a Preset team. Instead of printing the results on the terminal (whcih can be useful for quick troulbehsooting), it's possible to use ``--save-report=yaml`` or ``--save-report=csv`` to write results to a file. The file name would be ``{TeamSlug}__user_group_membership.{FileExtension}``.
14 changes: 14 additions & 0 deletions src/preset_cli/api/clients/preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,17 @@ def get_base_url(self, version: Optional[str] = "v1") -> URL:
Return the base URL for API calls.
"""
return self.baseurl / version

def get_group_membership(
self,
team_name: str,
page: int,
) -> json:
"""
Lists all user/SCIM groups associated with a team
"""
url = f'{self.get_base_url()}/teams/{team_name}/scim/v2/Groups?startIndex={page}'
self.session.headers["Accept"] = "application/scim+json"
_logger.debug("GET %s", url)
response = self.session.get(url)
return response.json()
86 changes: 86 additions & 0 deletions src/preset_cli/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
"""

import getpass
import json
import logging
import sys
import webbrowser
import csv
import os.path
from collections import defaultdict
from typing import Any, DefaultDict, Dict, List, Optional, Set, cast

Expand Down Expand Up @@ -325,6 +328,88 @@ def invite_users(ctx: click.core.Context, teams: List[str], path: str) -> None:
client.invite_users(teams, emails)


@click.command()
@click.option("--teams", callback=split_comma)
@click.option(
"--save-report",
help="Save results to a YAML or CSV file instead of priting on the terminal"
)
@click.pass_context
def list_group_membership(ctx: click.core.Context, teams: List[str], save_report: bool = False) -> None:
"""
List SCIM/user groups from Preset team(s)
"""
client = PresetClient(ctx.obj["MANAGER_URL"], ctx.obj["AUTH"])
if not teams:
# prompt the user to specify the team(s), in case not specified via the `--teams` option
teams = get_teams(client)

# in case --save-report was used, confirm if a valid option was used before sending requests
if save_report and save_report != 'yaml' and save_report != 'csv':
click.echo('Invalid option. Please use --save-report=csv or --save-report=yaml')

else:
for team in teams:
# print the team name in case multiple teams were provided and it's not an export
if not save_report and len(teams) > 1:
click.echo(f'\n## Team {team} ##')

# defining default start_at and group_count to execute it at least once
start_at = 1
group_count = 100

# account for pagination
while start_at < group_count:

groups = client.get_group_membership(team, start_at)
group_count = groups['totalResults']

if group_count > 0:

# print groups in console
if not save_report:
for group in groups['Resources']:
click.echo(f'\nName: {group["displayName"]} ID: {group["id"]}')
if group.get('members'):
for member in group['members']:
click.echo(f'# User: {member["display"]} Username: {member["value"]}')
else:
click.echo('# Group with no users.')

# write report to a file
else:

# write YAML
if save_report.casefold() == "yaml":
yaml_name = team + '_user_group_membership.yaml'
with open(yaml_name, 'a+', encoding='UTF8') as yaml_creator:
yaml.dump(groups, yaml_creator)

# write CSV
elif save_report.casefold() == "csv":
csv_name = team + '_user_group_membership.csv'
for group in groups['Resources']:

# CSV report would include a group only in case it has members
if group.get('members'):

# Due to pagination, we're going to touch the file more than once, but we only want to write headers once
file_exists = os.path.isfile(csv_name)

with open(csv_name, 'a+', encoding='UTF8') as csv_writer:
writer = csv.DictWriter(csv_writer, delimiter=',', fieldnames=['Group Name', 'Group ID', 'User', 'Username'])
if not file_exists:
writer.writeheader()
for member in group['members']:
writer.writerow({'Group Name': group["displayName"], 'Group ID': group["id"], 'User': member["display"], 'Username': member["value"]})

else:
click.echo(f'Team {team} has no SCIM groups.')

# increment start_at in case a new page is needed
start_at = start_at + 100


@click.command()
@click.option("--teams", callback=split_comma)
@click.argument(
Expand Down Expand Up @@ -503,3 +588,4 @@ def sync_user_role_to_workspace(
preset_cli.add_command(import_users)
preset_cli.add_command(sync_roles)
preset_cli.add_command(superset)
preset_cli.add_command(list_group_membership)
101 changes: 101 additions & 0 deletions tests/api/clients/preset_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,104 @@ def test_change_workspace_role(requests_mock: Mocker) -> None:
"role_identifier": "PresetAlpha",
"user_id": 2,
}

def test_get_group_membership(requests_mock: Mocker) -> None:
"""
Test the ``get_groups`` method.
"""
requests_mock.get(
"https://ws.preset.io/v1/teams/testSlug/scim/v2/Groups",
json={
"Resources": [
{
"displayName": "SCIM First Test Group",
"id": "b2a691ca-0ef8-464c-9601-9c50158c5426",
"members": [
{
"display": "Test Account 01",
"value": "samlp|example|testaccount01@example.com"
},
{
"display": "Test Account 02",
"value": "samlp|example|testaccount02@example.com"
}],
"meta":
{
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
},
{
"displayName": "SCIM Second Test Group",
"id": "fba067fc-506a-452b-8cf4-7d98f6960a6b",
"members": [
{
"display": "Test Account 02",
"value": "samlp|example|testaccount02@example.com"
}],
"meta":
{
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
}],
"itemsPerPage": 100,
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"startIndex": 1,
"totalResults": 2
},
)

auth = Auth()
client = PresetClient("https://ws.preset.io/", auth)
assert client.get_group_membership("testSlug", 1) == {
"Resources": [
{
"displayName": "SCIM First Test Group",
"id": "b2a691ca-0ef8-464c-9601-9c50158c5426",
"members": [
{
"display": "Test Account 01",
"value": "samlp|example|testaccount01@example.com"
},
{
"display": "Test Account 02",
"value": "samlp|example|testaccount02@example.com"
}],
"meta":
{
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
},
{
"displayName": "SCIM Second Test Group",
"id": "fba067fc-506a-452b-8cf4-7d98f6960a6b",
"members": [
{
"display": "Test Account 02",
"value": "samlp|example|testaccount02@example.com"
}],
"meta":
{
"resourceType": "Group"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
]
}],
"itemsPerPage": 100,
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"startIndex": 1,
"totalResults": 2
}

0 comments on commit 1ec1ce4

Please sign in to comment.