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

feat: export roles #75

Merged
merged 1 commit into from
Sep 22, 2022
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
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ The following commands are currently available:
- ``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.
- ``preset-cli superset export-rls``: export RLS rules into a YAML file.
- ``preset-cli superset export-roles``: export user roles into a YAML file.
- ``preset-cli superset export-users``: export users (name, username, email, roles) into a YAML file.
- ``preset-cli superset sync native``: synchronize the workspace from a directory of templated configuration files.
- ``preset-cli superset sync dbt-core``: synchronize the workspace from a dbt Core project.
Expand Down Expand Up @@ -427,6 +428,8 @@ Exporting users

The ``preset-cli superset export-users`` command can be used to export a list of users. These users can then be imported to Preset via the ``preset-cli import-users`` command.

You can also export roles via ``preset-cli superset export-roles``, and import with ``import-roles``.

Exporting RLS rules
~~~~~~~~~~~~~~~~~~~

Expand Down
59 changes: 58 additions & 1 deletion src/preset_cli/api/clients/superset.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ def parse_html_array(value: str) -> List[str]:
return [part for part in parts if part.strip()]


class RoleType(TypedDict):
"""
Schema for a role.
"""

name: str
permissions: List[str]


class RuleType(TypedDict):
"""
Schema for an RLS rule.
Expand Down Expand Up @@ -677,6 +686,51 @@ def _export_users_superset(
"role": parse_html_array(tds[6].text.strip()),
}

def export_roles(self) -> Iterator[RoleType]:
"""
Return all roles.
"""
session = self.auth.get_session()
headers = self.auth.get_headers()
headers["Referer"] = str(self.baseurl)

page = 0
while True:
params = {
"psize_RoleModelView": MAX_PAGE_SIZE,
"page_RoleModelView": page,
}
url = self.baseurl / "roles/list/"
page += 1

response = session.get(url, params=params, headers=headers)
soup = BeautifulSoup(response.text, features="html.parser")
table = soup.find_all("table")[1]
trs = table.find_all("tr")
if len(trs) == 1:
break

for tr in trs[1:]: # pylint: disable=invalid-name
tds = tr.find_all("td")

role_id = int(tds[0].find("input").attrs["id"])
role_url = self.baseurl / "roles/show" / str(role_id)

response = session.get(role_url, headers=headers)
soup = BeautifulSoup(response.text, features="html.parser")
table = soup.find_all("table")[-1]
keys: List[Tuple[str, Callable[[Any], Any]]] = [
("name", str),
("permissions", parse_html_array),
]
yield cast(
RoleType,
{
key: parse(tr.find("td").text.strip())
for (key, parse), tr in zip(keys, table.find_all("tr"))
},
)

def export_rls(self) -> Iterator[RuleType]:
"""
Return all RLS rules.
Expand All @@ -696,7 +750,10 @@ def export_rls(self) -> Iterator[RuleType]:

response = session.get(url, params=params, headers=headers)
soup = BeautifulSoup(response.text, features="html.parser")
table = soup.find_all("table")[1]
try:
table = soup.find_all("table")[1]
except IndexError:
return
trs = table.find_all("tr")
if len(trs) == 1:
break
Expand Down
19 changes: 19 additions & 0 deletions src/preset_cli/cli/superset/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ def export_users(ctx: click.core.Context, path: str) -> None:
yaml.dump(users, output)


@click.command()
@click.argument(
"path",
type=click.Path(resolve_path=True),
default="roles.yaml",
)
@click.pass_context
def export_roles(ctx: click.core.Context, path: str) -> None:
"""
Export roles to a YAML file.
"""
auth = ctx.obj["AUTH"]
url = URL(ctx.obj["INSTANCE"])
client = SupersetClient(url, auth)

with open(path, "w", encoding="utf-8") as output:
yaml.dump(list(client.export_roles()), output)


@click.command()
@click.argument(
"path",
Expand Down
2 changes: 2 additions & 0 deletions src/preset_cli/cli/superset/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
export_assets,
export_ownership,
export_rls,
export_roles,
export_users,
)
from preset_cli.cli.superset.import_ import import_ownership, import_rls
Expand Down Expand Up @@ -56,6 +57,7 @@ def superset_cli(
superset_cli.add_command(export_assets, name="export") # for backwards compatibility
superset_cli.add_command(export_users)
superset_cli.add_command(export_rls)
superset_cli.add_command(export_roles)
superset_cli.add_command(export_ownership)
superset_cli.add_command(import_rls)
superset_cli.add_command(import_ownership)
Expand Down
138 changes: 138 additions & 0 deletions tests/api/clients/superset_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,115 @@ def test_export_users_preset(requests_mock: Mocker) -> None:
]


def test_export_roles(requests_mock: Mocker) -> None:
"""
Test ``export_roles``.
"""
requests_mock.get(
(
"https://superset.example.org/roles/list/?"
"psize_RoleModelView=100&"
"page_RoleModelView=0"
),
text="""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<table></table>
<table>
<tr>
<th></th>
<th>Name</th>
</tr>
<tr>
<td><input id="1" /></td>
<td>Admin</td>
</tr>
<tr>
<td><input id="2" /></td>
<td>Public</td>
</tr>
</table>
</body>
</html>
""",
)
requests_mock.get(
(
"https://superset.example.org/roles/list/?"
"psize_RoleModelView=100&"
"page_RoleModelView=1"
),
text="""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<table></table>
<table>
<tr>
<th></th>
<th>Name</th>
</tr>
</table>
</body>
</html>
""",
)
requests_mock.get(
"https://superset.example.org/roles/show/1",
text="""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<table>
<tr><th>Name</th><td>Admin</td></tr>
<tr><th>Permissions</th><td>[can this, can that]</td></tr>
</table>
</body>
</html>
""",
)
requests_mock.get(
"https://superset.example.org/roles/show/2",
text="""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<table>
<tr><th>Name</th><td>Public</td></tr>
<tr><th>Permissions</th><td>[]</td></tr>
</table>
</body>
</html>
""",
)

auth = Auth()
client = SupersetClient("https://superset.example.org/", auth)
assert list(client.export_roles()) == [
{
"name": "Admin",
"permissions": ["can this", "can that"],
},
{
"name": "Public",
"permissions": [],
},
]


def test_export_rls(requests_mock: Mocker) -> None:
"""
Test ``export_rls``.
Expand Down Expand Up @@ -1498,6 +1607,35 @@ def test_export_rls(requests_mock: Mocker) -> None:
]


def test_export_rls_no_rules(requests_mock: Mocker) -> None:
"""
Test ``export_rls``.
"""
requests_mock.get(
(
"https://superset.example.org/rowlevelsecurityfiltersmodelview/list/?"
"psize_RowLevelSecurityFiltersModelView=100&"
"page_RowLevelSecurityFiltersModelView=0"
),
text="""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<table></table>
No records found
</body>
</html>
""",
)

auth = Auth()
client = SupersetClient("https://superset.example.org/", auth)
assert list(client.export_rls()) == []


def test_export_ownership(mocker: MockerFixture) -> None:
"""
Test ``export_ownership``.
Expand Down
34 changes: 33 additions & 1 deletion tests/cli/superset/export_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,41 @@ def test_export_users(mocker: MockerFixture, fs: FakeFilesystem) -> None:
]


def test_export_roles(mocker: MockerFixture, fs: FakeFilesystem) -> None:
"""
Test the ``export_roles`` command.
"""
mocker.patch("preset_cli.cli.superset.main.UsernamePasswordAuth")
SupersetClient = mocker.patch("preset_cli.cli.superset.export.SupersetClient")
client = SupersetClient()
client.export_roles.return_value = [
{
"name": "Public",
"permissions": [],
},
]

runner = CliRunner()
result = runner.invoke(
superset_cli,
["https://superset.example.org/", "export-roles", "roles.yaml"],
catch_exceptions=False,
)
assert result.exit_code == 0

with open("roles.yaml", encoding="utf-8") as input_:
contents = yaml.load(input_, Loader=yaml.SafeLoader)
assert contents == [
{
"name": "Public",
"permissions": [],
},
]


def test_export_rls(mocker: MockerFixture, fs: FakeFilesystem) -> None:
"""
Test the ``export_users`` command.
Test the ``export_rls`` command.
"""
mocker.patch("preset_cli.cli.superset.main.UsernamePasswordAuth")
SupersetClient = mocker.patch("preset_cli.cli.superset.export.SupersetClient")
Expand Down
1 change: 1 addition & 0 deletions tests/cli/superset/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def test_superset() -> None:
export-assets
export-ownership
export-rls
export-roles
export-users
import-assets
import-ownership
Expand Down