Skip to content

Commit 0143ac1

Browse files
authored
Merge pull request #227 from preset-io/rls-fix
fix(export-rls): Support Preset and older Superset versions
2 parents d2bf719 + a9902b3 commit 0143ac1

File tree

3 files changed

+233
-10
lines changed

3 files changed

+233
-10
lines changed

src/preset_cli/api/clients/superset.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class RuleType(TypedDict):
191191
Schema for an RLS rule.
192192
"""
193193

194-
name: str
194+
name: Optional[str]
195195
description: Optional[str]
196196
filter_type: str
197197
tables: List[str]
@@ -690,6 +690,12 @@ def import_zip(
690690

691691
return payload["message"] == "OK"
692692

693+
def get_rls(self, **kwargs: str) -> List[Any]:
694+
"""
695+
Return RLS rules, possibly filtered.
696+
"""
697+
return self.get_resources("rowlevelsecurity", **kwargs)
698+
693699
def export_users(self) -> Iterator[UserType]:
694700
"""
695701
Return all users.
@@ -808,9 +814,9 @@ def export_roles(self) -> Iterator[RoleType]: # pylint: disable=too-many-locals
808814
"users": users,
809815
}
810816

811-
def export_rls(self) -> Iterator[RuleType]:
817+
def export_rls_legacy(self) -> Iterator[RuleType]:
812818
"""
813-
Return all RLS rules.
819+
Return all RLS rules from legacy endpoint.
814820
"""
815821
page = 0
816822
while True:
@@ -857,6 +863,12 @@ def export_rls(self) -> Iterator[RuleType]:
857863
("group_key", str),
858864
("clause", str),
859865
]
866+
867+
# Before Superset 2.1.0, RLS dont have name and description
868+
if table.find("th").text.strip() == "Filter Type":
869+
keys.remove(("name", str))
870+
keys.remove(("description", str))
871+
860872
yield cast(
861873
RuleType,
862874
{
@@ -865,6 +877,41 @@ def export_rls(self) -> Iterator[RuleType]:
865877
},
866878
)
867879

880+
def export_rls(self) -> Iterator[RuleType]:
881+
"""
882+
Return all RLS rules.
883+
"""
884+
url = self.baseurl / "api/v1/rowlevelsecurity/"
885+
response = self.session.get(url)
886+
if response.status_code == 200:
887+
for rule in self.get_rls():
888+
keys = [
889+
"name",
890+
"description",
891+
"filter_type",
892+
"tables",
893+
"roles",
894+
"group_key",
895+
"clause",
896+
]
897+
data = {}
898+
for key in keys:
899+
if key == "tables":
900+
data[key] = [
901+
f"{inner_item['schema']}.{inner_item['table_name']}"
902+
for inner_item in rule.get(key, [])
903+
]
904+
elif key == "roles":
905+
data[key] = [
906+
inner_item["name"] for inner_item in rule.get(key, [])
907+
]
908+
else:
909+
data[key] = rule.get(key)
910+
yield cast(RuleType, data)
911+
912+
else:
913+
yield from self.export_rls_legacy()
914+
868915
def import_role(self, role: RoleType) -> None: # pylint: disable=too-many-locals
869916
"""
870917
Import a given role.

src/preset_cli/cli/superset/export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def export_rls(ctx: click.core.Context, path: str) -> None:
241241
client = SupersetClient(url, auth)
242242

243243
with open(path, "w", encoding="utf-8") as output:
244-
yaml.dump(list(client.export_rls()), output)
244+
yaml.dump(list(client.export_rls()), output, sort_keys=False)
245245

246246

247247
@click.command()

tests/api/clients/superset_test.py

Lines changed: 182 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,18 @@ def test_import_zip_error(requests_mock: Mocker) -> None:
12351235
)
12361236

12371237

1238+
def test_get_rls(mocker: MockerFixture) -> None:
1239+
"""
1240+
Test the ``get_rls`` method.
1241+
"""
1242+
auth = Auth()
1243+
client = SupersetClient("https://superset.example.org/", auth)
1244+
get_resources = mocker.patch.object(client, "get_resources")
1245+
1246+
client.get_rls()
1247+
get_resources.assert_called_with("rowlevelsecurity")
1248+
1249+
12381250
def test_export_users(requests_mock: Mocker) -> None:
12391251
"""
12401252
Test ``export_users``.
@@ -1668,9 +1680,9 @@ def test_export_roles_anchor_role_id(
16681680
]
16691681

16701682

1671-
def test_export_rls(requests_mock: Mocker) -> None:
1683+
def test_export_rls_legacy(requests_mock: Mocker) -> None:
16721684
"""
1673-
Test ``export_rls``.
1685+
Test ``export_rls_legacy``.
16741686
"""
16751687
requests_mock.get(
16761688
(
@@ -1766,7 +1778,7 @@ def test_export_rls(requests_mock: Mocker) -> None:
17661778

17671779
auth = Auth()
17681780
client = SupersetClient("https://superset.example.org/", auth)
1769-
assert list(client.export_rls()) == [
1781+
assert list(client.export_rls_legacy()) == [
17701782
{
17711783
"name": "My Rule",
17721784
"description": "This is a rule. There are many others like it, but this one is mine.",
@@ -1779,9 +1791,173 @@ def test_export_rls(requests_mock: Mocker) -> None:
17791791
]
17801792

17811793

1782-
def test_export_rls_no_rules(requests_mock: Mocker) -> None:
1794+
def test_export_rls_legacy_older_superset(requests_mock: Mocker) -> None:
1795+
"""
1796+
Test ``export_rls_legacy`` with older Superset version.
1797+
"""
1798+
requests_mock.get(
1799+
(
1800+
"https://superset.example.org/rowlevelsecurityfiltersmodelview/list/?"
1801+
"psize_RowLevelSecurityFiltersModelView=100&"
1802+
"page_RowLevelSecurityFiltersModelView=0"
1803+
),
1804+
text="""
1805+
<!DOCTYPE html>
1806+
<html lang="en">
1807+
<head>
1808+
<meta charset="utf-8">
1809+
</head>
1810+
<body>
1811+
<table></table>
1812+
<table>
1813+
<tr>
1814+
<th></th>
1815+
<th>Filter Type</th>
1816+
<th>Tables</th>
1817+
<th>Roles</th>
1818+
<th>Clause</th>
1819+
<th>Creator</th>
1820+
<th>Modified</th>
1821+
</tr>
1822+
<tr>
1823+
<td><input id="1" /></td>
1824+
<td>Regular</td>
1825+
<td>[main.test_table]</td>
1826+
<td>client_id = 9</td>
1827+
<td>admin admin</td>
1828+
<td>35 minutes ago</td>
1829+
</tr>
1830+
</table>
1831+
</body>
1832+
</html>
1833+
""",
1834+
)
1835+
requests_mock.get(
1836+
(
1837+
"https://superset.example.org/rowlevelsecurityfiltersmodelview/list/?"
1838+
"psize_RowLevelSecurityFiltersModelView=100&"
1839+
"page_RowLevelSecurityFiltersModelView=1"
1840+
),
1841+
text="""
1842+
<!DOCTYPE html>
1843+
<html lang="en">
1844+
<head>
1845+
<meta charset="utf-8">
1846+
</head>
1847+
<body>
1848+
<table></table>
1849+
<table>
1850+
<tr>
1851+
<th></th>
1852+
<th>Filter Type</th>
1853+
<th>Tables</th>
1854+
<th>Roles</th>
1855+
<th>Clause</th>
1856+
<th>Creator</th>
1857+
<th>Modified</th>
1858+
</tr>
1859+
</table>
1860+
</body>
1861+
</html>
1862+
""",
1863+
)
1864+
requests_mock.get(
1865+
"https://superset.example.org/rowlevelsecurityfiltersmodelview/show/1",
1866+
text="""
1867+
<!DOCTYPE html>
1868+
<html lang="en">
1869+
<head>
1870+
<meta charset="utf-8">
1871+
</head>
1872+
<body>
1873+
<table>
1874+
<tr><th>Filter Type</th><td>Regular</td></tr>
1875+
<tr><th>Tables</th><td>[main.test_table]</td></tr>
1876+
<tr><th>Roles</th><td>[Gamma]</td></tr>
1877+
<tr><th>Group Key</th><td>department</td></tr>
1878+
<tr><th>Clause</th><td>client_id = 9</td></tr>
1879+
</table>
1880+
</body>
1881+
</html>
1882+
""",
1883+
)
1884+
1885+
auth = Auth()
1886+
client = SupersetClient("https://superset.example.org/", auth)
1887+
assert list(client.export_rls_legacy()) == [
1888+
{
1889+
"filter_type": "Regular",
1890+
"tables": ["main.test_table"],
1891+
"roles": ["Gamma"],
1892+
"group_key": "department",
1893+
"clause": "client_id = 9",
1894+
},
1895+
]
1896+
1897+
1898+
def test_export_rls_legacy_route(requests_mock: Mocker, mocker: MockerFixture) -> None:
1899+
"""
1900+
Test ``export_rls`` going through the legacy route
1901+
"""
1902+
requests_mock.get(
1903+
"https://superset.example.org/api/v1/rowlevelsecurity/",
1904+
status_code=404,
1905+
)
1906+
1907+
auth = Auth()
1908+
client = SupersetClient("https://superset.example.org/", auth)
1909+
1910+
export_rls_legacy = mocker.patch.object(client, "export_rls_legacy")
1911+
list(client.export_rls())
1912+
export_rls_legacy.assert_called_once()
1913+
1914+
1915+
def test_export_rls(requests_mock: Mocker, mocker: MockerFixture) -> None:
1916+
"""
1917+
Test ``export_rls``
1918+
"""
1919+
requests_mock.get(
1920+
"https://superset.example.org/api/v1/rowlevelsecurity/",
1921+
status_code=200,
1922+
)
1923+
1924+
auth = Auth()
1925+
client = SupersetClient("https://superset.example.org/", auth)
1926+
1927+
get_rls = mocker.patch.object(client, "get_rls")
1928+
get_rls.return_value = [
1929+
{
1930+
"changed_on_delta_humanized": "2 days ago",
1931+
"clause": "client_id = 9",
1932+
"description": "This is a rule. There are many others like it, but this one is mine.",
1933+
"filter_type": "Regular",
1934+
"group_key": "department",
1935+
"id": 9,
1936+
"name": "My Rule",
1937+
"roles": [{"id": 1, "name": "Admin"}, {"id": 2, "name": "Gamma"}],
1938+
"tables": [
1939+
{"id": 18, "schema": "main", "table_name": "test_table"},
1940+
{"id": 20, "schema": "main", "table_name": "second_test"},
1941+
],
1942+
},
1943+
]
1944+
1945+
assert list(client.export_rls()) == [
1946+
{
1947+
"name": "My Rule",
1948+
"description": "This is a rule. There are many others like it, but this one is mine.",
1949+
"filter_type": "Regular",
1950+
"tables": ["main.test_table", "main.second_test"],
1951+
"roles": ["Admin", "Gamma"],
1952+
"group_key": "department",
1953+
"clause": "client_id = 9",
1954+
},
1955+
]
1956+
1957+
1958+
def test_export_rls_legacy_no_rules(requests_mock: Mocker) -> None:
17831959
"""
1784-
Test ``export_rls``.
1960+
Test ``export_rls_legacy`` with no rows returned.
17851961
"""
17861962
requests_mock.get(
17871963
(
@@ -1805,7 +1981,7 @@ def test_export_rls_no_rules(requests_mock: Mocker) -> None:
18051981

18061982
auth = Auth()
18071983
client = SupersetClient("https://superset.example.org/", auth)
1808-
assert list(client.export_rls()) == []
1984+
assert list(client.export_rls_legacy()) == []
18091985

18101986

18111987
def test_export_ownership(mocker: MockerFixture) -> None:

0 commit comments

Comments
 (0)