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

Implement List SCIM groups #143

Merged
merged 27 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ec1ce4
Implementing the ability to list SCIM groups and their membership
Vitor-Avila Nov 6, 2022
8fb7625
Removing un-unsed import
Vitor-Avila Nov 6, 2022
0730f32
Merge pull request #1 from Vitor-Avila/list-groups
Vitor-Avila Nov 6, 2022
7ff0509
Add remaining tests
Vitor-Avila Nov 8, 2022
c8be195
Better exception handling
Vitor-Avila Nov 8, 2022
edf6258
trying to fix the tests
Vitor-Avila Nov 11, 2022
8b658a0
Update main_test.py
Vitor-Avila Nov 11, 2022
a58519b
Update python-package-daily.yml
Vitor-Avila Nov 11, 2022
e03b5fb
Merge pull request #2 from Vitor-Avila/list-groups
Vitor-Avila Nov 11, 2022
021bd56
Update main.py
Vitor-Avila Nov 11, 2022
6b3a128
a
Vitor-Avila Nov 11, 2022
221d6ab
Update main.py
Vitor-Avila Nov 12, 2022
7945fd8
Update main_test.py
Vitor-Avila Nov 12, 2022
a950508
Merge pull request #3 from preset-io/main
Vitor-Avila Nov 17, 2022
44ebc41
fixing tests
Vitor-Avila Nov 12, 2022
8f38d4f
test
Vitor-Avila Nov 12, 2022
07699b6
Adding more test coverage
Vitor-Avila Nov 15, 2022
ee23b7b
finished adding test coverage:
Vitor-Avila Nov 15, 2022
00618c9
Update src/preset_cli/cli/main.py
Vitor-Avila Nov 16, 2022
92d4404
Update src/preset_cli/cli/main.py
Vitor-Avila Nov 16, 2022
eaa1522
Update README.rst
Vitor-Avila Nov 16, 2022
a4d6840
Update src/preset_cli/api/clients/preset.py
Vitor-Avila Nov 16, 2022
f4a7df1
Improving code
Vitor-Avila Nov 17, 2022
6529d17
This was incorrectly modified to quickly test something
Vitor-Avila Nov 17, 2022
3e554db
Updating readme
Vitor-Avila Nov 17, 2022
63fe07d
Fixing tests
Vitor-Avila Nov 17, 2022
267b4e3
Merge branch 'main' into list-groups
betodealmeida Nov 30, 2022
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
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 troubleshooting), 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}``.
22 changes: 21 additions & 1 deletion src/preset_cli/api/clients/preset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import logging
from enum import Enum
from typing import Any, Iterator, List, Optional, Union
from typing import Any, Dict, Iterator, List, Optional, Union

from bs4 import BeautifulSoup
from yarl import URL
Expand Down Expand Up @@ -242,3 +242,23 @@ 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,
) -> Dict[str, Any]:
"""
Lists all user/SCIM groups associated with a team
"""
url = (
self.get_base_url()
/ "teams"
/ team_name
/ "scim/v2/Groups"
% {"startIndex": str(page)}
)
self.session.headers["Accept"] = "application/scim+json"
_logger.debug("GET %s", url)
response = self.session.get(url)
return response.json()
135 changes: 135 additions & 0 deletions src/preset_cli/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
Main entry point for the CLI.
"""

import csv
import getpass
import logging
import os.path
import sys
import webbrowser
from collections import defaultdict
Expand Down Expand Up @@ -325,6 +327,138 @@ 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: str,
) -> 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.casefold() not in {"yaml", "csv"}:
click.echo(
click.style(
"Invalid option. Please use --save-report=csv or --save-report=yaml",
fg="bright_red",
),
)
sys.exit(1)

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"## 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:
print_group_membership(groups)

# write report to a YAML file
elif save_report.casefold() == "yaml":
export_group_membership_yaml(groups, team)

# write report to a CSV file
else:
export_group_membership_csv(groups, team)

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

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


def print_group_membership(groups: Dict[str, Any]) -> None:
"""
Print group membership on the terminal
"""
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\n")


def export_group_membership_yaml(groups: Dict[str, Any], team: str) -> None:
"""
Export group membership to a YAML file
"""
yaml_name = team + "_user_group_membership.yaml"
with open(
yaml_name,
"a+",
encoding="UTF8",
) as yaml_creator:
yaml.dump(groups, yaml_creator)


def export_group_membership_csv(groups: Dict[str, Any], team: str) -> None:
"""
Export group membership to a CSV file
"""
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"):

# Assure we just 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"],
},
)


@click.command()
@click.option("--teams", callback=split_comma)
@click.argument(
Expand Down Expand Up @@ -503,3 +637,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)
104 changes: 104 additions & 0 deletions tests/api/clients/preset_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,107 @@ 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,
}
Loading