Skip to content

Commit d4874d2

Browse files
Add regex for testing permission strings (#681)
1 parent 5d1ae40 commit d4874d2

File tree

3 files changed

+85
-33
lines changed

3 files changed

+85
-33
lines changed

aiohttp_admin/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import secrets
23
from typing import Optional
34

@@ -87,5 +88,21 @@ def value(r: web.RouteDef) -> tuple[str, str]:
8788

8889
setup_routes(admin)
8990
setup_resources(admin, schema)
91+
92+
resource_patterns = []
93+
for r, state in admin["state"]["resources"].items():
94+
fields = state["fields"].keys()
95+
resource_patterns.append(
96+
r"(?#Resource name){r}"
97+
r"(?#Optional field name)(\.({f}))?"
98+
r"(?#Permission type)\.(view|edit|add|delete|\*)"
99+
r"(?#No filters if negated)(?(2)$|"
100+
r'(?#Optional filters)\|({f})=(?#JSON number or str)(\".*?\"|\d+))*'.format(
101+
r=r, f="|".join(fields)))
102+
p_re = (r"(?#Global admin permission)~?admin\.(view|edit|add|delete|\*)"
103+
r"|"
104+
r"(?#Resource permission)(~)?admin\.({})").format("|".join(resource_patterns))
105+
admin["permission_re"] = re.compile(p_re)
106+
90107
prefixed_subapp = app.add_subapp(path, admin)
91108
return admin

examples/permissions.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Example to demonstrate usage of permissions.
22
33
When running this file, admin will be accessible at /admin.
4-
Check below for valid usernames (and their respective permissions),
4+
Check near the bottom of the file for valid usernames (and their respective permissions),
55
login will work with any password.
66
"""
77

@@ -46,37 +46,6 @@ async def create_app() -> web.Application:
4646
# Create some sample data
4747
async with engine.begin() as conn:
4848
await conn.run_sync(Base.metadata.create_all)
49-
async with session.begin() as sess:
50-
# Users with various permissions.
51-
sess.add(User(username="admin", permissions=json.dumps(tuple(Permissions))))
52-
sess.add(User(username="view", permissions=json.dumps((Permissions.view,))))
53-
sess.add(User(username="add", permissions=json.dumps(
54-
(Permissions.view, Permissions.add,))))
55-
sess.add(User(username="edit", permissions=json.dumps(
56-
(Permissions.view, Permissions.edit))))
57-
sess.add(User(username="delete", permissions=json.dumps(
58-
(Permissions.view, Permissions.delete))))
59-
sess.add(User(username="simple", permissions=json.dumps(("admin.simple.*",))))
60-
sess.add(User(username="mixed", permissions=json.dumps(
61-
("admin.simple.view", "admin.simple.edit", "admin.parent.view"))))
62-
sess.add(User(username="negated", permissions=json.dumps(
63-
("admin.*", "~admin.parent.*", "~admin.simple.edit"))))
64-
sess.add(User(username="field", permissions=json.dumps(
65-
("admin.*", "~admin.simple.optional_num.*"))))
66-
sess.add(User(username="field_edit", permissions=json.dumps(
67-
("admin.*", "~admin.simple.optional_num.edit"))))
68-
sess.add(User(username="filter", permissions=json.dumps(
69-
("admin.*", "admin.simple.*|num=5"))))
70-
sess.add(User(username="filter_edit", permissions=json.dumps(
71-
("admin.*", "admin.simple.edit|num=5"))))
72-
sess.add(User(username="filter_add", permissions=json.dumps(
73-
("admin.*", "admin.simple.add|num=5"))))
74-
sess.add(User(username="filter_delete", permissions=json.dumps(
75-
("admin.*", "admin.simple.delete|num=5"))))
76-
sess.add(User(username="filter_field", permissions=json.dumps(
77-
("admin.*", "admin.simple.optional_num.*|num=5"))))
78-
sess.add(User(username="filter_field_edit", permissions=json.dumps(
79-
("admin.*", "admin.simple.optional_num.edit|num=5"))))
8049
async with session.begin() as sess:
8150
sess.add(Simple(num=5, value="first"))
8251
p = Simple(num=82, optional_num=12, value="with child")
@@ -104,7 +73,35 @@ async def create_app() -> web.Application:
10473
{"model": SAResource(engine, SimpleParent)}
10574
)
10675
}
107-
aiohttp_admin.setup(app, schema)
76+
admin = aiohttp_admin.setup(app, schema)
77+
78+
# Create users with various permissions.
79+
async with session.begin() as sess:
80+
sess.add(User(username="admin", permissions=json.dumps(tuple(Permissions))))
81+
sess.add(User(username="view", permissions=json.dumps((Permissions.view,))))
82+
sess.add(User(username="add", permissions=json.dumps(
83+
(Permissions.view, Permissions.add,))))
84+
sess.add(User(username="edit", permissions=json.dumps(
85+
(Permissions.view, Permissions.edit))))
86+
sess.add(User(username="delete", permissions=json.dumps(
87+
(Permissions.view, Permissions.delete))))
88+
users = {
89+
"simple": ("admin.simple.*",),
90+
"mixed": ("admin.simple.view", "admin.simple.edit", "admin.parent.view"),
91+
"negated": ("admin.*", "~admin.parent.*", "~admin.simple.edit"),
92+
"field": ("admin.*", "~admin.simple.optional_num.*"),
93+
"field_edit": ("admin.*", "~admin.simple.optional_num.edit"),
94+
"filter": ("admin.*", "admin.simple.*|num=5"),
95+
"filter_edit": ("admin.*", "admin.simple.edit|num=5"),
96+
"filter_add": ("admin.*", "admin.simple.add|num=5"),
97+
"filter_delete": ("admin.*", "admin.simple.delete|num=5"),
98+
"filter_field": ("admin.*", "admin.simple.optional_num.*|num=5"),
99+
"filter_field_edit": ("admin.*", "admin.simple.optional_num.edit|num=5")
100+
}
101+
for name, permissions in users.items():
102+
if any(admin["permission_re"].fullmatch(p) is None for p in permissions):
103+
raise ValueError("Not a valid permission.")
104+
sess.add(User(username=name, permissions=json.dumps(permissions)))
108105

109106
return app
110107

tests/test_admin.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from aiohttp import web
2+
from sqlalchemy.ext.asyncio import AsyncEngine
3+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
24

35
import aiohttp_admin
46
from _auth import check_credentials
7+
from aiohttp_admin.backends.sqlalchemy import SAResource
58

69

710
def test_path() -> None:
@@ -15,3 +18,38 @@ def test_path() -> None:
1518
admin = aiohttp_admin.setup(app, schema, path="/another/admin")
1619

1720
assert str(admin.router["index"].url_for()) == "/another/admin"
21+
22+
23+
def test_re(base: type[DeclarativeBase], mock_engine: AsyncEngine) -> None:
24+
class TestRE(base): # type: ignore[misc,valid-type]
25+
__tablename__ = "testre"
26+
id: Mapped[int] = mapped_column(primary_key=True)
27+
value: Mapped[str]
28+
29+
app = web.Application()
30+
schema: aiohttp_admin.Schema = {"security": {"check_credentials": check_credentials},
31+
"resources": ({"model": SAResource(mock_engine, TestRE)},)}
32+
admin = aiohttp_admin.setup(app, schema)
33+
r = admin["permission_re"]
34+
35+
assert r.fullmatch("admin.*")
36+
assert r.fullmatch("admin.view")
37+
assert r.fullmatch("~admin.edit")
38+
assert r.fullmatch("admin.testre.*")
39+
assert r.fullmatch("admin.testre.add")
40+
assert r.fullmatch("admin.testre.id.*")
41+
assert r.fullmatch("admin.testre.value.edit")
42+
assert r.fullmatch("~admin.testre.id.edit")
43+
assert r.fullmatch("admin.testre.edit|id=5")
44+
assert r.fullmatch('admin.testre.add|id=1|value="4"|value="7"')
45+
assert r.fullmatch('admin.testre.value.*|value="foo"')
46+
assert r.fullmatch("admin.testre.value.delete|id=5|id=3")
47+
48+
assert r.fullmatch("testre.edit") is None
49+
assert r.fullmatch("admin.create") is None
50+
assert r.fullmatch("admin.nottest.*") is None
51+
assert r.fullmatch("admin.*|id=1") is None
52+
assert r.fullmatch("admin.testre.edit|other=5") is None
53+
assert r.fullmatch("admin.testre.value.*|value=unquoted") is None
54+
assert r.fullmatch("~admin.testre.edit|id=5") is None
55+
assert r.fullmatch('~admin.testre.value.delete|value="1"') is None

0 commit comments

Comments
 (0)