Skip to content

Commit

Permalink
Merge pull request #1 from Vitor-Avila/list-groups
Browse files Browse the repository at this point in the history
List groups
  • Loading branch information
Vitor-Avila authored Nov 6, 2022
2 parents 3ceedca + 8fb7625 commit 0730f32
Show file tree
Hide file tree
Showing 4 changed files with 205 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()
85 changes: 85 additions & 0 deletions src/preset_cli/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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 +327,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 +587,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 0730f32

Please sign in to comment.