From 75831e1f5be73f0ef8953f18bc91818616364c2a Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 6 Nov 2023 21:28:32 +0000 Subject: [PATCH 01/17] Use AppKey in aiohttp 3.9 --- aiohttp_admin/__init__.py | 17 ++-- aiohttp_admin/routes.py | 18 ++-- aiohttp_admin/types.py | 12 ++- aiohttp_admin/views.py | 9 +- examples/permissions.py | 16 ++-- requirements.txt | 2 +- setup.py | 2 +- tests/conftest.py | 52 +++++------ tests/test_admin.py | 14 +-- tests/test_backends_abc.py | 3 +- tests/test_backends_sqlalchemy.py | 39 ++++---- tests/test_security.py | 144 ++++++++++++++++-------------- tests/test_views.py | 111 ++++++++++++----------- 13 files changed, 237 insertions(+), 202 deletions(-) diff --git a/aiohttp_admin/__init__.py b/aiohttp_admin/__init__.py index 746cc338..ab94d2ce 100644 --- a/aiohttp_admin/__init__.py +++ b/aiohttp_admin/__init__.py @@ -11,9 +11,9 @@ from .routes import setup_resources, setup_routes from .security import AdminAuthorizationPolicy, Permissions, TokenIdentityPolicy, check -from .types import Schema, UserDetails +from .types import Schema, State, UserDetails, check_credentials_key, permission_re_key, state_key -__all__ = ("Permissions", "Schema", "UserDetails", "setup") +__all__ = ("Permissions", "Schema", "UserDetails", "permission_re_key", "setup") __version__ = "0.1.0a2" @@ -51,7 +51,7 @@ async def on_startup(admin: web.Application) -> None: enclosing scope later. """ storage._cookie_params["path"] = prefixed_subapp.canonical - admin["state"]["urls"] = { + admin[state_key]["urls"] = { "token": str(admin.router["token"].url_for()), "logout": str(admin.router["logout"].url_for()) } @@ -65,7 +65,7 @@ def value(r: web.RouteDef) -> tuple[str, str]: for res in schema["resources"]: m = res["model"] - admin["state"]["resources"][m.name]["urls"] = {key(r): value(r) for r in m.routes} + admin[state_key]["resources"][m.name]["urls"].update((key(r), value(r)) for r in m.routes) schema = check(Schema, schema) if secret is None: @@ -74,9 +74,8 @@ def value(r: web.RouteDef) -> tuple[str, str]: admin = web.Application() admin.middlewares.append(pydantic_middleware) admin.on_startup.append(on_startup) - admin["check_credentials"] = schema["security"]["check_credentials"] - admin["identity_callback"] = schema["security"].get("identity_callback") - admin["state"] = {"view": schema.get("view", {}), "js_module": schema.get("js_module")} + admin[check_credentials_key] = schema["security"]["check_credentials"] + admin[state_key] = State({"view": schema.get("view", {}), "js_module": schema.get("js_module"), "urls": {}, "resources": {}}) max_age = schema["security"].get("max_age") secure = schema["security"].get("secure", True) @@ -90,7 +89,7 @@ def value(r: web.RouteDef) -> tuple[str, str]: setup_resources(admin, schema) resource_patterns = [] - for r, state in admin["state"]["resources"].items(): + for r, state in admin[state_key]["resources"].items(): fields = state["fields"].keys() resource_patterns.append( r"(?#Resource name){r}" @@ -102,7 +101,7 @@ def value(r: web.RouteDef) -> tuple[str, str]: p_re = (r"(?#Global admin permission)~?admin\.(view|edit|add|delete|\*)" r"|" r"(?#Resource permission)(~)?admin\.({})").format("|".join(resource_patterns)) - admin["permission_re"] = re.compile(p_re) + admin[permission_re_key] = re.compile(p_re) prefixed_subapp = app.add_subapp(path, admin) return admin diff --git a/aiohttp_admin/routes.py b/aiohttp_admin/routes.py index d164971c..a8a40185 100644 --- a/aiohttp_admin/routes.py +++ b/aiohttp_admin/routes.py @@ -6,16 +6,15 @@ from aiohttp import web from . import views -from .types import Schema +from .types import _ResourceState, Schema, resources_key, state_key def setup_resources(admin: web.Application, schema: Schema) -> None: - admin["resources"] = [] - admin["state"]["resources"] = {} + admin[resources_key] = [] for r in schema["resources"]: m = r["model"] - admin["resources"].append(m) + admin[resources_key].append(m) admin.router.add_routes(m.routes) try: @@ -47,11 +46,12 @@ def setup_resources(admin: web.Application, schema: Schema) -> None: for name, props in r.get("field_props", {}).items(): fields[name]["props"].update(props) - state = {"fields": fields, "inputs": inputs, "list_omit": tuple(omit_fields), - "repr": repr_field, "label": r.get("label"), "icon": r.get("icon"), - "bulk_update": r.get("bulk_update", {}), - "show_actions": r.get("show_actions", ())} - admin["state"]["resources"][m.name] = state + state: _ResourceState = { + "fields": fields, "inputs": inputs, "list_omit": tuple(omit_fields), + "repr": repr_field, "label": r.get("label"), "icon": r.get("icon"), + "bulk_update": r.get("bulk_update", {}), "urls": {}, + "show_actions": r.get("show_actions", ())} + admin[state_key]["resources"][m.name] = state def setup_routes(admin: web.Application) -> None: diff --git a/aiohttp_admin/types.py b/aiohttp_admin/types.py index 6c2c64a6..e750111e 100644 --- a/aiohttp_admin/types.py +++ b/aiohttp_admin/types.py @@ -1,7 +1,10 @@ +import re import sys from collections.abc import Callable, Collection, Sequence from typing import Any, Awaitable, Literal, Mapping, Optional +from aiohttp.web import AppKey + if sys.version_info >= (3, 12): from typing import TypedDict else: @@ -110,7 +113,6 @@ class Schema(_Schema): class _ResourceState(TypedDict): - display: Sequence[str] fields: dict[str, ComponentState] inputs: dict[str, InputState] show_actions: Sequence[ComponentState] @@ -118,6 +120,8 @@ class _ResourceState(TypedDict): icon: Optional[str] urls: dict[str, tuple[str, str]] # (method, url) bulk_update: dict[str, dict[str, Any]] + list_omit: tuple[str, ...] + label: Optional[str] class State(TypedDict): @@ -146,3 +150,9 @@ def func(name: str, args: Optional[Sequence[object]] = None) -> FunctionState: def regex(value: str) -> RegexState: """Convert value to a RegExp object on the frontend.""" return {"__type__": "regexp", "value": value} + + +check_credentials_key = AppKey[Callable[[str, str], Awaitable[bool]]]("check_credentials") +permission_re_key = AppKey("permission_re", re.Pattern[str]) +resources_key = AppKey("resources", list[Any]) # TODO(pydantic): AbstractAdminResource +state_key = AppKey("state", State) diff --git a/aiohttp_admin/views.py b/aiohttp_admin/views.py index 750bc36d..1c23a51e 100644 --- a/aiohttp_admin/views.py +++ b/aiohttp_admin/views.py @@ -7,6 +7,7 @@ from pydantic import Json from .security import check +from .types import check_credentials_key, state_key if sys.version_info >= (3, 12): from typing import TypedDict @@ -38,15 +39,15 @@ async def index(request: web.Request) -> web.Response: """Root page which loads react-admin.""" static = request.app.router["static"] js = static.url_for(filename="admin.js") - state = json.dumps(request.app["state"]) + state = json.dumps(request.app[state_key]) # __package__ can be None, despite what the documentation claims. package_name = __main__.__package__ or "My" # Common convention is to have _app suffix for package name, so try and strip that. package_name = package_name.removesuffix("_app").replace("_", " ").title() - name = request.app["state"]["view"].get("name", package_name) + name = request.app[state_key]["view"].get("name", package_name) - icon = request.app["state"]["view"].get("icon", static.url_for(filename="favicon.svg")) + icon = request.app[state_key]["view"].get("icon", static.url_for(filename="favicon.svg")) output = INDEX_TEMPLATE.format(name=name, icon=icon, js=js, state=state) return web.Response(text=output, content_type="text/html") @@ -56,7 +57,7 @@ async def token(request: web.Request) -> web.Response: """Validate user credentials and log the user in.""" data = check(Json[_Login], await request.read()) - check_credentials = request.app["check_credentials"] + check_credentials = request.app[check_credentials_key] if not await check_credentials(data["username"], data["password"]): raise web.HTTPUnauthorized(text="Wrong username or password") diff --git a/examples/permissions.py b/examples/permissions.py index dced00b0..a07c784f 100644 --- a/examples/permissions.py +++ b/examples/permissions.py @@ -11,13 +11,15 @@ import sqlalchemy as sa from aiohttp import web -from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column import aiohttp_admin -from aiohttp_admin import Permissions, UserDetails +from aiohttp_admin import Permissions, UserDetails, permission_re_key from aiohttp_admin.backends.sqlalchemy import SAResource, permission_for as p +db = web.AppKey("db", async_sessionmaker[AsyncSession]) + class Base(DeclarativeBase): """Base model.""" @@ -49,14 +51,16 @@ class User(Base): async def check_credentials(app: web.Application, username: str, password: str) -> bool: """Allow login to any user account regardless of password.""" - async with app["db"]() as sess: + async with app[db]() as sess: user = await sess.get(User, username.lower()) return user is not None async def identity_callback(app: web.Application, identity: str) -> UserDetails: - async with app["db"]() as sess: + async with app[db]() as sess: user = await sess.get(User, identity) + if not user: + raise ValueError("No user found for given identity") return {"permissions": json.loads(user.permissions), "fullName": user.username.title()} @@ -79,7 +83,7 @@ async def create_app() -> web.Application: sess.add(SimpleParent(id=p_simple.id, date=datetime(2023, 2, 13, 19, 4))) app = web.Application() - app["db"] = session + app[db] = session # This is the setup required for aiohttp-admin. schema: aiohttp_admin.Schema = { @@ -123,7 +127,7 @@ async def create_app() -> web.Application: filters={Simple.num: 5})) } for name, permissions in users.items(): - if any(admin["permission_re"].fullmatch(p) is None for p in permissions): + if any(admin[permission_re_key].fullmatch(p) is None for p in permissions): raise ValueError("Not a valid permission.") sess.add(User(username=name, permissions=json.dumps(permissions))) diff --git a/requirements.txt b/requirements.txt index de1b7fe2..3c83cb61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -e . -aiohttp==3.8.6 +aiohttp==3.9.0b1 aiohttp-security==0.4.0 aiohttp-session[secure]==2.12.0 aiosqlite==0.19.0 diff --git a/setup.py b/setup.py index 279a08d6..be4ba837 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def read_version(): download_url="https://github.com/aio-libs/aiohttp-admin", license="Apache 2", packages=find_packages(), - install_requires=("aiohttp>=3.8.2", "aiohttp_security", "aiohttp_session", + install_requires=("aiohttp>=3.9", "aiohttp_security", "aiohttp_session", "cryptography", "pydantic>2,<3", 'typing_extensions>=3.10; python_version<"3.12"'), extras_require={"sa": ["sqlalchemy>=2.0.4,<3"]}, diff --git a/tests/conftest.py b/tests/conftest.py index a4871d3f..5288759f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ from collections.abc import Awaitable, Callable -from typing import Optional +from typing import Optional, Type from unittest.mock import AsyncMock, create_autospec import pytest from aiohttp import web from aiohttp.test_utils import TestClient -from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column import aiohttp_admin @@ -14,13 +14,24 @@ _IdentityCallback = Callable[[str], Awaitable[aiohttp_admin.UserDetails]] +class Base(DeclarativeBase): + """Base model.""" -@pytest.fixture -def base() -> type[DeclarativeBase]: - class Base(DeclarativeBase): - """Base model.""" +class DummyModel(Base): + __tablename__ = "dummy" + + id: Mapped[int] = mapped_column(primary_key=True) + +class Dummy2Model(Base): + __tablename__ = "dummy2" + + id: Mapped[int] = mapped_column(primary_key=True) + msg: Mapped[Optional[str]] - return Base +model = web.AppKey[type[DummyModel]]("model") +model2 = web.AppKey[type[Dummy2Model]]("model2") +db = web.AppKey("db", async_sessionmaker[AsyncSession]) +admin = web.AppKey("admin", web.Application) @pytest.fixture @@ -30,28 +41,17 @@ def mock_engine() -> AsyncMock: @pytest.fixture def create_admin_client( - base: DeclarativeBase, aiohttp_client: Callable[[web.Application], Awaitable[TestClient]] + aiohttp_client: Callable[[web.Application], Awaitable[TestClient]] ) -> Callable[[Optional[_IdentityCallback]], Awaitable[TestClient]]: async def admin_client(identity_callback: Optional[_IdentityCallback] = None) -> TestClient: - class DummyModel(base): # type: ignore[misc,valid-type] - __tablename__ = "dummy" - - id: Mapped[int] = mapped_column(primary_key=True) - - class Dummy2Model(base): # type: ignore[misc,valid-type] - __tablename__ = "dummy2" - - id: Mapped[int] = mapped_column(primary_key=True) - msg: Mapped[Optional[str]] - app = web.Application() - app["model"] = DummyModel - app["model2"] = Dummy2Model + app[model] = DummyModel + app[model2] = Dummy2Model engine = create_async_engine("sqlite+aiosqlite:///:memory:") - app["db"] = async_sessionmaker(engine, expire_on_commit=False) + app[db] = async_sessionmaker(engine, expire_on_commit=False) async with engine.begin() as conn: - await conn.run_sync(base.metadata.create_all) - async with app["db"].begin() as sess: + await conn.run_sync(Base.metadata.create_all) + async with app[db].begin() as sess: sess.add(DummyModel()) sess.add(Dummy2Model(msg="Test")) sess.add(Dummy2Model(msg="Test")) @@ -69,7 +69,7 @@ class Dummy2Model(base): # type: ignore[misc,valid-type] } if identity_callback: schema["security"]["identity_callback"] = identity_callback - app["admin"] = aiohttp_admin.setup(app, schema) + app[admin] = aiohttp_admin.setup(app, schema) return await aiohttp_client(app) @@ -85,7 +85,7 @@ async def admin_client(create_admin_client: Callable[[], Awaitable[TestClient]]) def login() -> Callable[[TestClient], Awaitable[dict[str, str]]]: async def do_login(admin_client: TestClient) -> dict[str, str]: assert admin_client.app - url = admin_client.app["admin"].router["token"].url_for() + url = admin_client.app[admin].router["token"].url_for() login = {"username": "admin", "password": "admin123"} async with admin_client.post(url, json=login) as resp: assert resp.status == 200 diff --git a/tests/test_admin.py b/tests/test_admin.py index 082549b6..06b23ab2 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -4,7 +4,7 @@ import aiohttp_admin from _auth import check_credentials from _resources import DummyResource -from aiohttp_admin.types import comp, func +from aiohttp_admin.types import comp, func, permission_re_key, state_key def test_path() -> None: @@ -26,7 +26,7 @@ def test_js_module() -> None: "resources": (), "js_module": "/custom_js.js"} admin = aiohttp_admin.setup(app, schema) - assert admin["state"]["js_module"] == "/custom_js.js" + assert admin[state_key]["js_module"] == "/custom_js.js" def test_no_js_module() -> None: @@ -35,7 +35,7 @@ def test_no_js_module() -> None: "resources": ()} admin = aiohttp_admin.setup(app, schema) - assert admin["state"]["js_module"] is None + assert admin[state_key]["js_module"] is None def test_validators() -> None: @@ -51,7 +51,7 @@ def test_validators() -> None: "security": {"check_credentials": check_credentials}, "resources": ({"model": dummy, "validators": {"id": (func("minValue", (3,)),)}},)} admin = aiohttp_admin.setup(app, schema) - validators = admin["state"]["resources"]["dummy"]["inputs"]["id"]["props"]["validate"] + validators = admin[state_key]["resources"]["dummy"]["inputs"]["id"]["props"]["validate"] assert validators == (func("required", ()), func("minValue", (3,))) assert ("minValue", 3) not in dummy.inputs["id"]["props"]["validate"] # type: ignore[operator] @@ -64,7 +64,7 @@ def test_re() -> None: schema: aiohttp_admin.Schema = {"security": {"check_credentials": check_credentials}, "resources": ({"model": test_re},)} admin = aiohttp_admin.setup(app, schema) - r = admin["permission_re"] + r = admin[permission_re_key] assert r.fullmatch("admin.*") assert r.fullmatch("admin.view") @@ -102,7 +102,7 @@ def test_display() -> None: admin = aiohttp_admin.setup(app, schema) - test_state = admin["state"]["resources"]["test"] + test_state = admin[state_key]["resources"]["test"] assert test_state["list_omit"] == ("id",) assert test_state["inputs"]["id"]["props"] == {"validate": (func("required", ()),)} assert test_state["inputs"]["foo"]["props"] == {"alwaysOn": "alwaysOn"} @@ -136,7 +136,7 @@ def test_extra_props() -> None: admin = aiohttp_admin.setup(app, schema) - test_state = admin["state"]["resources"]["test"] + test_state = admin[state_key]["resources"]["test"] assert test_state["fields"]["id"]["props"] == {"textAlign": "left", "placeholder": "foo", "label": "Spam"} assert test_state["inputs"]["id"]["props"] == {"alwaysOn": "alwaysOn", "type": "email", diff --git a/tests/test_backends_abc.py b/tests/test_backends_abc.py index 059ed68d..e14121c9 100644 --- a/tests/test_backends_abc.py +++ b/tests/test_backends_abc.py @@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable from aiohttp.test_utils import TestClient +from conftest import admin _Login = Callable[[TestClient], Awaitable[dict[str, str]]] @@ -9,7 +10,7 @@ async def test_create_with_null(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() p = {"data": json.dumps({"msg": None})} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 200, await resp.text() diff --git a/tests/test_backends_sqlalchemy.py b/tests/test_backends_sqlalchemy.py index 44330405..0a260131 100644 --- a/tests/test_backends_sqlalchemy.py +++ b/tests/test_backends_sqlalchemy.py @@ -16,10 +16,19 @@ from _auth import check_credentials from aiohttp_admin.backends.sqlalchemy import FIELD_TYPES, SAResource, permission_for from aiohttp_admin.types import comp, func, regex +from conftest import admin _Login = Callable[[TestClient], Awaitable[dict[str, str]]] +@pytest.fixture +def base() -> type[DeclarativeBase]: + class Base(DeclarativeBase): + """Base model.""" + + return Base + + def test_no_subtypes() -> None: """We don't want any subtypes in the lookup, as this would depend on test ordering.""" assert all({TypeEngine, TypeDecorator} & set(t.__bases__) for t in FIELD_TYPES) @@ -109,13 +118,13 @@ class TestModel(base): # type: ignore[misc,valid-type] }, "resources": ({"model": SAResource(engine, TestModel)},) } - app["admin"] = aiohttp_admin.setup(app, schema) + app[admin] = aiohttp_admin.setup(app, schema) admin_client = await aiohttp_client(app) assert admin_client.app h = await login(admin_client) - url = app["admin"].router["test_get_one"].url_for() + url = app[admin].router["test_get_one"].url_for() async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "1", "binary": "foo"}} @@ -178,13 +187,13 @@ class TestModelParent(base): # type: ignore[misc,valid-type] "resources": ({"model": SAResource(engine, TestModel)}, {"model": SAResource(engine, TestModelParent)}) } - app["admin"] = aiohttp_admin.setup(app, schema) + app[admin] = aiohttp_admin.setup(app, schema) admin_client = await aiohttp_client(app) assert admin_client.app h = await login(admin_client) - url = app["admin"].router["parent_get_one"].url_for() + url = app[admin].router["parent_get_one"].url_for() async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 # child_id must be converted to str ID. @@ -376,13 +385,13 @@ class TestModel(base): # type: ignore[misc,valid-type] }, "resources": ({"model": SAResource(engine, TestModel)},) } - app["admin"] = aiohttp_admin.setup(app, schema) + app[admin] = aiohttp_admin.setup(app, schema) admin_client = await aiohttp_client(app) assert admin_client.app h = await login(admin_client) - url = app["admin"].router["test_get_list"].url_for() + url = app[admin].router["test_get_list"].url_for() p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} async with admin_client.get(url, params=p, headers=h) as resp: @@ -390,24 +399,24 @@ class TestModel(base): # type: ignore[misc,valid-type] assert await resp.json() == {"data": [{"id": "8", "num": 8, "other": "bar"}, {"id": "5", "num": 5, "other": "foo"}], "total": 2} - url = app["admin"].router["test_get_one"].url_for() + url = app[admin].router["test_get_one"].url_for() async with admin_client.get(url, params={"id": 8}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "8", "num": 8, "other": "bar"}} - url = app["admin"].router["test_get_many"].url_for() + url = app[admin].router["test_get_many"].url_for() async with admin_client.get(url, params={"ids": '["5", "8"]'}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": [{"id": "5", "num": 5, "other": "foo"}, {"id": "8", "num": 8, "other": "bar"}]} - url = app["admin"].router["test_create"].url_for() + url = app[admin].router["test_create"].url_for() p = {"data": json.dumps({"num": 12, "other": "this"})} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "12", "num": 12, "other": "this"}} - url = app["admin"].router["test_update"].url_for() + url = app[admin].router["test_update"].url_for() p1 = {"id": 5, "data": json.dumps({"id": 5, "other": "that"}), "previousData": "{}"} async with admin_client.put(url, params=p1, headers=h) as resp: assert resp.status == 200 @@ -439,19 +448,19 @@ class TestModel(base): # type: ignore[misc,valid-type] }, "resources": ({"model": SAResource(engine, TestModel)},) } - app["admin"] = aiohttp_admin.setup(app, schema) + app[admin] = aiohttp_admin.setup(app, schema) admin_client = await aiohttp_client(app) assert admin_client.app h = await login(admin_client) - url = app["admin"].router["test_get_one"].url_for() + url = app[admin].router["test_get_one"].url_for() async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "1", "date": "2023-04-23", "time": "2023-01-02 03:04:00"}} - url = app["admin"].router["test_create"].url_for() + url = app[admin].router["test_create"].url_for() p = {"data": json.dumps({"date": "2024-05-09", "time": "2020-11-12 03:04:05"})} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 200 @@ -515,13 +524,13 @@ class TestModel(base): # type: ignore[misc,valid-type] }, "resources": ({"model": SAResource(engine, TestModel)},) } - app["admin"] = aiohttp_admin.setup(app, schema) + app[admin] = aiohttp_admin.setup(app, schema) admin_client = await aiohttp_client(app) assert admin_client.app h = await login(admin_client) - url = app["admin"].router["test_create"].url_for() + url = app[admin].router["test_create"].url_for() p = {"data": json.dumps({"foo": True, "bar": 5})} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 200 diff --git a/tests/test_security.py b/tests/test_security.py index 50e4c467..ff30e67e 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -7,6 +7,7 @@ from aiohttp_security import AbstractAuthorizationPolicy from aiohttp_admin import Permissions, UserDetails +from conftest import admin, db, model, model2 _CreateClient = Callable[[AbstractAuthorizationPolicy], Awaitable[TestClient]] _Login = Callable[[TestClient], Awaitable[dict[str, str]]] @@ -14,7 +15,7 @@ async def test_no_token(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_list"].url_for() + url = admin_client.app[admin].router["dummy_get_list"].url_for() async with admin_client.get(url) as resp: assert resp.status == 401 assert await resp.text() == "401: Unauthorized" @@ -22,7 +23,7 @@ async def test_no_token(admin_client: TestClient) -> None: async def test_invalid_token(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() h = {"Authorization": "invalid"} async with admin_client.get(url, headers=h) as resp: assert resp.status @@ -30,13 +31,13 @@ async def test_invalid_token(admin_client: TestClient) -> None: async def test_valid_login_logout(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["token"].url_for() + url = admin_client.app[admin].router["token"].url_for() login = {"username": "admin", "password": "admin123"} async with admin_client.post(url, json=login) as resp: assert resp.status == 200 token = resp.headers["X-Token"] - get_one_url = admin_client.app["admin"].router["dummy_get_one"].url_for() + get_one_url = admin_client.app[admin].router["dummy_get_one"].url_for() p = {"id": 1} h = {"Authorization": token} async with admin_client.get(get_one_url, params=p, headers=h) as resp: @@ -44,7 +45,7 @@ async def test_valid_login_logout(admin_client: TestClient) -> None: assert await resp.json() == {"data": {"id": "1"}} # Continue to test logout - logout_url = admin_client.app["admin"].router["logout"].url_for() + logout_url = admin_client.app[admin].router["logout"].url_for() async with admin_client.delete(logout_url, headers=h) as resp: assert resp.status == 200 @@ -54,7 +55,7 @@ async def test_valid_login_logout(admin_client: TestClient) -> None: async def test_missing_token(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["token"].url_for() + url = admin_client.app[admin].router["token"].url_for() login = {"username": "admin", "password": "admin123"} async with admin_client.post(url, json=login) as resp: assert resp.status == 200 @@ -63,7 +64,7 @@ async def test_missing_token(admin_client: TestClient) -> None: assert len(cookies) == 1 assert cookies[0]["path"] == "/admin" - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() p = {"id": 1} async with admin_client.get(url, params=p) as resp: assert resp.status == 401 @@ -71,7 +72,7 @@ async def test_missing_token(admin_client: TestClient) -> None: async def test_missing_cookie(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["token"].url_for() + url = admin_client.app[admin].router["token"].url_for() login = {"username": "admin", "password": "admin123"} async with admin_client.post(url, json=login) as resp: assert resp.status == 200 @@ -79,7 +80,7 @@ async def test_missing_cookie(admin_client: TestClient) -> None: admin_client.session.cookie_jar.clear() - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() p = {"id": 1} h = {"Authorization": token} async with admin_client.get(url, params=p, headers=h) as resp: @@ -88,7 +89,7 @@ async def test_missing_cookie(admin_client: TestClient) -> None: async def test_login_invalid_payload(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["token"].url_for() + url = admin_client.app[admin].router["token"].url_for() async with admin_client.post(url, json={"foo": "bar", "password": None}) as resp: assert resp.status == 400 result = await resp.json() @@ -110,7 +111,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_list"].url_for() + url = admin_client.app[admin].router["dummy_get_list"].url_for() p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} h = await login(admin_client) @@ -130,7 +131,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 @@ -146,7 +147,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 @@ -162,7 +163,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 403 @@ -170,12 +171,12 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: # expected = "403: User does not have 'admin.dummy.view' permission" # assert await resp.text() == expected - url = admin_client.app["admin"].router["dummy2_get_one"].url_for() + url = admin_client.app[admin].router["dummy2_get_one"].url_for() async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "1", "msg": "Test"}} - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() p = {"data": '{"msg": "Foo"}'} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 403 @@ -192,7 +193,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_list"].url_for() + url = admin_client.app[admin].router["dummy2_get_list"].url_for() h = await login(admin_client) p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} @@ -210,7 +211,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_one"].url_for() + url = admin_client.app[admin].router["dummy2_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 @@ -226,7 +227,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_many"].url_for() + url = admin_client.app[admin].router["dummy2_get_many"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"ids": '["1"]'}, headers=h) as resp: assert resp.status == 200 @@ -242,7 +243,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() h = await login(admin_client) p = {"data": json.dumps({"msg": "ABC"})} async with admin_client.post(url, params=p, headers=h) as resp: @@ -264,7 +265,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() h = await login(admin_client) p = {"data": json.dumps({"msg": "ABC"})} async with admin_client.post(url, params=p, headers=h) as resp: @@ -286,7 +287,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update"].url_for() + url = admin_client.app[admin].router["dummy2_update"].url_for() h = await login(admin_client) p = {"id": 1, "data": json.dumps({"id": 222, "msg": "ABC"}), "previousData": "{}"} async with admin_client.put(url, params=p, headers=h) as resp: @@ -303,17 +304,18 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update"].url_for() + url = admin_client.app[admin].router["dummy2_update"].url_for() h = await login(admin_client) p = {"id": 1, "data": json.dumps({"id": 222, "msg": "ABC"}), "previousData": "{}"} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "222"}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model2"], 1) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model2], 1) assert r is None - r = await sess.get(admin_client.app["model2"], 222) + r = await sess.get(admin_client.app[model2], 222) + assert r is not None assert r.msg == "Test" @@ -326,7 +328,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update_many"].url_for() + url = admin_client.app[admin].router["dummy2_update_many"].url_for() h = await login(admin_client) p = {"ids": '["1"]', "data": json.dumps({"msg": "ABC"})} async with admin_client.put(url, params=p, headers=h) as resp: @@ -344,7 +346,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_delete"].url_for() + url = admin_client.app[admin].router["dummy2_delete"].url_for() h = await login(admin_client) p = {"id": 1, "previousData": "{}"} async with admin_client.delete(url, params=p, headers=h) as resp: @@ -358,7 +360,7 @@ async def test_permissions_cached(create_admin_client: _CreateClient, # type: i admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_list"].url_for() + url = admin_client.app[admin].router["dummy2_get_list"].url_for() h = await login(admin_client) identity_callback.assert_called_once() identity_callback.reset_mock() @@ -379,10 +381,10 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - async with admin_client.app["db"].begin() as sess: - sess.add(admin_client.app["model2"](msg="Foo")) + async with admin_client.app[db].begin() as sess: + sess.add(admin_client.app[model2](msg="Foo")) - url = admin_client.app["admin"].router["dummy2_get_list"].url_for() + url = admin_client.app[admin].router["dummy2_get_list"].url_for() p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} h = await login(admin_client) @@ -402,7 +404,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_list"].url_for() + url = admin_client.app[admin].router["dummy2_get_list"].url_for() p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} h = await login(admin_client) @@ -420,7 +422,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_one"].url_for() + url = admin_client.app[admin].router["dummy2_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 2}, headers=h) as resp: assert resp.status == 200 @@ -437,7 +439,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_one"].url_for() + url = admin_client.app[admin].router["dummy2_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 2}, headers=h) as resp: assert resp.status == 200 @@ -454,7 +456,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_many"].url_for() + url = admin_client.app[admin].router["dummy2_get_many"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"ids": '["2", "3"]'}, headers=h) as resp: assert resp.status == 200 @@ -472,7 +474,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_many"].url_for() + url = admin_client.app[admin].router["dummy2_get_many"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"ids": '["2", "3"]'}, headers=h) as resp: assert resp.status == 200 @@ -490,7 +492,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() h = await login(admin_client) p = {"data": json.dumps({"msg": "Test"})} async with admin_client.post(url, params=p, headers=h) as resp: @@ -509,7 +511,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() h = await login(admin_client) p = {"data": json.dumps({"msg": "Test"})} async with admin_client.post(url, params=p, headers=h) as resp: @@ -528,7 +530,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update"].url_for() + url = admin_client.app[admin].router["dummy2_update"].url_for() h = await login(admin_client) p = {"id": 3, "data": json.dumps({"msg": "Test"}), "previousData": "{}"} async with admin_client.put(url, params=p, headers=h) as resp: @@ -549,7 +551,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update"].url_for() + url = admin_client.app[admin].router["dummy2_update"].url_for() h = await login(admin_client) p = {"id": 3, "data": json.dumps({"msg": "Test"}), "previousData": "{}"} async with admin_client.put(url, params=p, headers=h) as resp: @@ -571,7 +573,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update_many"].url_for() + url = admin_client.app[admin].router["dummy2_update_many"].url_for() h = await login(admin_client) p = {"ids": '["3"]', "data": json.dumps({"msg": "Test"})} async with admin_client.put(url, params=p, headers=h) as resp: @@ -593,7 +595,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update_many"].url_for() + url = admin_client.app[admin].router["dummy2_update_many"].url_for() h = await login(admin_client) p = {"ids": '["3"]', "data": json.dumps({"msg": "Test"})} async with admin_client.put(url, params=p, headers=h) as resp: @@ -614,7 +616,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_delete"].url_for() + url = admin_client.app[admin].router["dummy2_delete"].url_for() h = await login(admin_client) p = {"id": 3, "previousData": "{}"} async with admin_client.delete(url, params=p, headers=h) as resp: @@ -632,7 +634,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_delete"].url_for() + url = admin_client.app[admin].router["dummy2_delete"].url_for() h = await login(admin_client) p = {"id": 3, "previousData": "{}"} async with admin_client.delete(url, params=p, headers=h) as resp: @@ -650,7 +652,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_delete_many"].url_for() + url = admin_client.app[admin].router["dummy2_delete_many"].url_for() h = await login(admin_client) p = {"ids": '["2", "3"]'} async with admin_client.delete(url, params=p, headers=h) as resp: @@ -672,7 +674,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_delete_many"].url_for() + url = admin_client.app[admin].router["dummy2_delete_many"].url_for() h = await login(admin_client) p = {"ids": '["2", "3"]'} async with admin_client.delete(url, params=p, headers=h) as resp: @@ -694,7 +696,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_list"].url_for() + url = admin_client.app[admin].router["dummy2_get_list"].url_for() p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} h = await login(admin_client) @@ -713,7 +715,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_list"].url_for() + url = admin_client.app[admin].router["dummy2_get_list"].url_for() p = {"pagination": json.dumps({"page": 1, "perPage": 10}), "sort": json.dumps({"field": "id", "order": "DESC"}), "filter": "{}"} h = await login(admin_client) @@ -732,7 +734,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_one"].url_for() + url = admin_client.app[admin].router["dummy2_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 @@ -750,7 +752,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_one"].url_for() + url = admin_client.app[admin].router["dummy2_get_one"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 @@ -768,7 +770,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_many"].url_for() + url = admin_client.app[admin].router["dummy2_get_many"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"ids": '["2", "3"]'}, headers=h) as resp: assert resp.status == 200 @@ -783,7 +785,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_get_many"].url_for() + url = admin_client.app[admin].router["dummy2_get_many"].url_for() h = await login(admin_client) async with admin_client.get(url, params={"ids": '["1", "3"]'}, headers=h) as resp: assert resp.status == 200 @@ -798,7 +800,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() h = await login(admin_client) p = {"data": json.dumps({"msg": "Spam"})} async with admin_client.post(url, params=p, headers=h) as resp: @@ -806,8 +808,9 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: async with admin_client.post(url, params={"data": "{}"}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "4"}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model2"], 4) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model2], 4) + assert r is not None assert r.msg is None @@ -819,7 +822,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_create"].url_for() + url = admin_client.app[admin].router["dummy2_create"].url_for() h = await login(admin_client) p = {"data": json.dumps({"msg": "Spam"})} async with admin_client.post(url, params=p, headers=h) as resp: @@ -827,8 +830,9 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: async with admin_client.post(url, params={"data": "{}"}, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "4", "msg": None}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model2"], 4) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model2], 4) + assert r is not None assert r.msg is None @@ -840,7 +844,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update"].url_for() + url = admin_client.app[admin].router["dummy2_update"].url_for() h = await login(admin_client) p = {"id": 3, "data": json.dumps({"msg": "Spam"}), "previousData": "{}"} async with admin_client.put(url, params=p, headers=h) as resp: @@ -854,10 +858,11 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "5"}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model2"], 2) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model2], 2) assert r is None - r = await sess.get(admin_client.app["model2"], 5) + r = await sess.get(admin_client.app[model2], 5) + assert r is not None assert r.msg == "Test" @@ -869,7 +874,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update"].url_for() + url = admin_client.app[admin].router["dummy2_update"].url_for() h = await login(admin_client) p = {"id": "3", "data": json.dumps({"msg": "Spam"}), "previousData": "{}"} async with admin_client.put(url, params=p, headers=h) as resp: @@ -883,10 +888,11 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "5", "msg": "Test"}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model2"], 2) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model2], 2) assert r is None - r = await sess.get(admin_client.app["model2"], 5) + r = await sess.get(admin_client.app[model2], 5) + assert r is not None assert r.msg == "Test" @@ -899,7 +905,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update_many"].url_for() + url = admin_client.app[admin].router["dummy2_update_many"].url_for() h = await login(admin_client) p = {"ids": '["3"]', "data": json.dumps({"msg": "Spam"})} async with admin_client.put(url, params=p, headers=h) as resp: @@ -919,7 +925,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: admin_client = await create_admin_client(identity_callback) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update_many"].url_for() + url = admin_client.app[admin].router["dummy2_update_many"].url_for() h = await login(admin_client) p = {"ids": '["3"]', "data": json.dumps({"msg": "Spam"})} async with admin_client.put(url, params=p, headers=h) as resp: diff --git a/tests/test_views.py b/tests/test_views.py index 49f409ac..40724e8c 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -6,13 +6,14 @@ from aiohttp.test_utils import TestClient from aiohttp_admin.types import comp, func +from conftest import admin, db, model, model2 _Login = Callable[[TestClient], Awaitable[dict[str, str]]] async def test_admin_view(admin_client: TestClient) -> None: assert admin_client.app - url = admin_client.app["admin"].router["index"].url_for() + url = admin_client.app[admin].router["index"].url_for() async with admin_client.get(url) as resp: assert resp.status == 200 html = await resp.text() @@ -43,11 +44,11 @@ async def test_admin_view(admin_client: TestClient) -> None: async def test_list_pagination(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(25): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_get_list"].url_for() + url = admin_client.app[admin].router["dummy_get_list"].url_for() p = {"pagination": '{"page": 1, "perPage": 30}', "sort": '{"field": "id", "order": "ASC"}', "filter": '{}'} async with admin_client.get(url, params=p, headers=h) as resp: @@ -76,11 +77,11 @@ async def test_list_pagination(admin_client: TestClient, login: _Login) -> None: async def test_list_filtering_by_pk(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(15): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_get_list"].url_for() + url = admin_client.app[admin].router["dummy_get_list"].url_for() p = {"pagination": '{"page": 1, "perPage": 10}', "sort": '{"field": "id", "order": "ASC"}', "filter": '{"id": 3}'} async with admin_client.get(url, params=p, headers=h) as resp: @@ -91,11 +92,11 @@ async def test_list_filtering_by_pk(admin_client: TestClient, login: _Login) -> async def test_list_text_like_filtering(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(15): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_get_list"].url_for() + url = admin_client.app[admin].router["dummy_get_list"].url_for() p = {"pagination": '{"page": 1, "perPage": 10}', "sort": '{"field": "id", "order": "ASC"}', "filter": '{"id": "3"}'} async with admin_client.get(url, params=p, headers=h) as resp: @@ -106,7 +107,7 @@ async def test_list_text_like_filtering(admin_client: TestClient, login: _Login) async def test_get_one(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() async with admin_client.get(url, params={"id": 1}, headers=h) as resp: assert resp.status == 200 @@ -116,7 +117,7 @@ async def test_get_one(admin_client: TestClient, login: _Login) -> None: async def test_get_one_not_exists(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_get_one"].url_for() + url = admin_client.app[admin].router["dummy_get_one"].url_for() async with admin_client.get(url, params={"id": 5}, headers=h) as resp: assert resp.status == 404 @@ -125,11 +126,11 @@ async def test_get_one_not_exists(admin_client: TestClient, login: _Login) -> No async def test_get_many(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(15): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_get_many"].url_for() + url = admin_client.app[admin].router["dummy_get_many"].url_for() p = {"ids": '["3", "7", "12"]'} async with admin_client.get(url, params=p, headers=h) as resp: assert resp.status == 200 @@ -139,11 +140,11 @@ async def test_get_many(admin_client: TestClient, login: _Login) -> None: async def test_get_many_not_exists(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(5): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_get_many"].url_for() + url = admin_client.app[admin].router["dummy_get_many"].url_for() p = {"ids": '["3", "4", "8"]'} async with admin_client.get(url, params=p, headers=h) as resp: assert resp.status == 200 @@ -157,21 +158,22 @@ async def test_get_many_not_exists(admin_client: TestClient, login: _Login) -> N async def test_create(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_create"].url_for() + url = admin_client.app[admin].router["dummy_create"].url_for() p = {"data": "{}"} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "2"}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model"], 2) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model], 2) + assert r is not None assert r.id == 2 async def test_create_duplicate_id(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_create"].url_for() + url = admin_client.app[admin].router["dummy_create"].url_for() p = {"data": '{"id": 1}'} async with admin_client.post(url, params=p, headers=h) as resp: assert resp.status == 400 @@ -180,24 +182,25 @@ async def test_create_duplicate_id(admin_client: TestClient, login: _Login) -> N async def test_update(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_update"].url_for() + url = admin_client.app[admin].router["dummy_update"].url_for() p = {"id": 1, "data": '{"id": 4}', "previousData": '{"id": 1}'} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "4"}} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model"], 4) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model], 4) + assert r is not None assert r.id == 4 - assert await sess.get(admin_client.app["model"], 1) is None - assert await sess.get(admin_client.app["model"], 2) is None + assert await sess.get(admin_client.app[model], 1) is None + assert await sess.get(admin_client.app[model], 2) is None async def test_update_deleted_entity(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_update"].url_for() + url = admin_client.app[admin].router["dummy_update"].url_for() p = {"id": 2, "data": '{"id": 4}', "previousData": '{"id": 2}'} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 404 @@ -206,7 +209,7 @@ async def test_update_deleted_entity(admin_client: TestClient, login: _Login) -> async def test_update_invalid_attributes(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_update"].url_for() + url = admin_client.app[admin].router["dummy_update"].url_for() p = {"id": 1, "data": '{"id": 4, "foo": "invalid"}', "previousData": '{"id": 1}'} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 400 @@ -216,23 +219,25 @@ async def test_update_invalid_attributes(admin_client: TestClient, login: _Login async def test_update_many(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy2_update_many"].url_for() + url = admin_client.app[admin].router["dummy2_update_many"].url_for() p = {"ids": '["1", "2"]', "data": json.dumps({"msg": "ABC"})} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": ["1", "2"]} - async with admin_client.app["db"]() as sess: - r = await sess.get(admin_client.app["model2"], 1) + async with admin_client.app[db]() as sess: + r = await sess.get(admin_client.app[model2], 1) + assert r is not None assert r.msg == "ABC" - r = await sess.get(admin_client.app["model2"], 2) + r = await sess.get(admin_client.app[model2], 2) + assert r is not None assert r.msg == "ABC" async def test_update_many_deleted_entity(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_update_many"].url_for() + url = admin_client.app[admin].router["dummy_update_many"].url_for() p = {"ids": '["2"]', "data": '{"id": 4}'} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 404 @@ -241,7 +246,7 @@ async def test_update_many_deleted_entity(admin_client: TestClient, login: _Logi async def test_update_many_invalid_attributes(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_update_many"].url_for() + url = admin_client.app[admin].router["dummy_update_many"].url_for() p = {"ids": '["1"]', "data": '{"foo": "invalid"}'} async with admin_client.put(url, params=p, headers=h) as resp: assert resp.status == 400 @@ -251,22 +256,22 @@ async def test_update_many_invalid_attributes(admin_client: TestClient, login: _ async def test_delete(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_delete"].url_for() + url = admin_client.app[admin].router["dummy_delete"].url_for() p = {"id": 1, "previousData": '{"id": 1}'} async with admin_client.delete(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": {"id": "1"}} - async with admin_client.app["db"]() as sess: - assert await sess.get(admin_client.app["model"], 1) is None - r = await sess.scalars(sa.select(admin_client.app["model"])) + async with admin_client.app[db]() as sess: + assert await sess.get(admin_client.app[model], 1) is None + r = await sess.scalars(sa.select(admin_client.app[model])) assert len(r.all()) == 0 async def test_delete_entity_not_exists(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - url = admin_client.app["admin"].router["dummy_delete"].url_for() + url = admin_client.app[admin].router["dummy_delete"].url_for() p = {"id": 5, "previousData": '{"id": 5}'} async with admin_client.delete(url, params=p, headers=h) as resp: assert resp.status == 404 @@ -275,18 +280,18 @@ async def test_delete_entity_not_exists(admin_client: TestClient, login: _Login) async def test_delete_many(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(5): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_delete_many"].url_for() + url = admin_client.app[admin].router["dummy_delete_many"].url_for() p = {"ids": '["2", "3", "5"]'} async with admin_client.delete(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": ["2", "3", "5"]} - async with admin_client.app["db"]() as sess: - r = await sess.scalars(sa.select(admin_client.app["model"])) + async with admin_client.app[db]() as sess: + r = await sess.scalars(sa.select(admin_client.app[model])) models = r.all() assert len(models) == 3 assert {m.id for m in models} == {1, 4, 6} @@ -295,27 +300,27 @@ async def test_delete_many(admin_client: TestClient, login: _Login) -> None: async def test_delete_many_not_exists(admin_client: TestClient, login: _Login) -> None: h = await login(admin_client) assert admin_client.app - async with admin_client.app["db"].begin() as sess: + async with admin_client.app[db].begin() as sess: for _ in range(5): - sess.add(admin_client.app["model"]()) + sess.add(admin_client.app[model]()) - url = admin_client.app["admin"].router["dummy_delete_many"].url_for() + url = admin_client.app[admin].router["dummy_delete_many"].url_for() p = {"ids": '["2", "3", "9"]'} async with admin_client.delete(url, params=p, headers=h) as resp: assert resp.status == 200 assert await resp.json() == {"data": ["2", "3"]} - async with admin_client.app["db"]() as sess: - r = await sess.scalars(sa.select(admin_client.app["model"])) + async with admin_client.app[db]() as sess: + r = await sess.scalars(sa.select(admin_client.app[model])) models = r.all() assert len(models) == 4 assert {m.id for m in models} == {1, 4, 5, 6} - url = admin_client.app["admin"].router["dummy_delete_many"].url_for() + url = admin_client.app[admin].router["dummy_delete_many"].url_for() p = {"ids": '["12", "13"]'} async with admin_client.delete(url, params=p, headers=h) as resp: assert resp.status == 404 - async with admin_client.app["db"]() as sess: - r = await sess.scalars(sa.select(admin_client.app["model"])) + async with admin_client.app[db]() as sess: + r = await sess.scalars(sa.select(admin_client.app[model])) assert len(r.all()) == 4 From a3c36f7de157eb86cef299c295bfe65d373aafc0 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 7 Nov 2023 00:14:03 +0000 Subject: [PATCH 02/17] Fix type error --- aiohttp_admin/backends/sqlalchemy.py | 9 +++++---- tests/conftest.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/aiohttp_admin/backends/sqlalchemy.py b/aiohttp_admin/backends/sqlalchemy.py index 3754e642..6cfbd6a8 100644 --- a/aiohttp_admin/backends/sqlalchemy.py +++ b/aiohttp_admin/backends/sqlalchemy.py @@ -5,12 +5,12 @@ import sys from collections.abc import Callable, Coroutine, Iterator, Sequence from types import MappingProxyType as MPT -from typing import Any, Literal, Optional, TypeVar, Union +from typing import Any, Literal, Optional, TypeVar, Union, cast import sqlalchemy as sa from aiohttp import web from sqlalchemy.ext.asyncio import AsyncEngine -from sqlalchemy.orm import DeclarativeBase, QueryableAttribute +from sqlalchemy.orm import DeclarativeBase, DeclarativeBaseNoMeta, Mapper, QueryableAttribute from sqlalchemy.sql.roles import ExpressionElementRole from .abc import AbstractAdminResource, GetListParams, Meta, Record @@ -155,7 +155,7 @@ def create_filters(columns: sa.ColumnCollection[str, sa.Column[object]], # ID is based on PK, which we can't infer from types, so must use Any here. class SAResource(AbstractAdminResource[Any]): - def __init__(self, db: AsyncEngine, model_or_table: Union[sa.Table, type[DeclarativeBase]]): + def __init__(self, db: AsyncEngine, model_or_table: Union[sa.Table, type[DeclarativeBase], type[DeclarativeBaseNoMeta]]): if isinstance(model_or_table, sa.Table): table = model_or_table else: @@ -221,7 +221,8 @@ def __init__(self, db: AsyncEngine, model_or_table: Union[sa.Table, type[Declara if not isinstance(model_or_table, sa.Table): # Append fields to represent ORM relationships. - mapper = sa.inspect(model_or_table) + # Mypy doesn't handle union well here. + mapper = cast(Union[Mapper[DeclarativeBase], Mapper[DeclarativeBaseNoMeta]], sa.inspect(model_or_table)) assert mapper is not None # noqa: S101 for name, relationship in mapper.relationships.items(): # https://github.com/sqlalchemy/sqlalchemy/discussions/10161#discussioncomment-6583442 diff --git a/tests/conftest.py b/tests/conftest.py index 5288759f..7392faa2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from aiohttp import web from aiohttp.test_utils import TestClient from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.orm import DeclarativeBaseNoMeta, Mapped, mapped_column import aiohttp_admin from _auth import check_credentials @@ -14,7 +14,7 @@ _IdentityCallback = Callable[[str], Awaitable[aiohttp_admin.UserDetails]] -class Base(DeclarativeBase): +class Base(DeclarativeBaseNoMeta): """Base model.""" class DummyModel(Base): From eba7ba3e9ce973696849dcd81a53d1cb5c4c8ca8 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 19 Nov 2023 15:10:05 +0000 Subject: [PATCH 03/17] Update requirements.txt --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3c83cb61..7466f42c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -e . -aiohttp==3.9.0b1 -aiohttp-security==0.4.0 +aiohttp==3.9.0 +aiohttp-security==0.5.0 aiohttp-session[secure]==2.12.0 aiosqlite==0.19.0 cryptography==41.0.5 From f68e594ac97e6084ae7616a65638a03161218f3c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 19 Nov 2023 23:41:13 +0000 Subject: [PATCH 04/17] Update security.py --- aiohttp_admin/security.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiohttp_admin/security.py b/aiohttp_admin/security.py index f73f60a2..88add466 100644 --- a/aiohttp_admin/security.py +++ b/aiohttp_admin/security.py @@ -71,7 +71,7 @@ def permissions_as_dict(permissions: Collection[str]) -> dict[str, dict[str, lis return p_dict -class AdminAuthorizationPolicy(AbstractAuthorizationPolicy): # type: ignore[misc,no-any-unimported] +class AdminAuthorizationPolicy(AbstractAuthorizationPolicy): def __init__(self, schema: Schema): super().__init__() self._identity_callback = schema["security"].get("identity_callback") @@ -101,7 +101,7 @@ async def permits(self, identity: Optional[str], permission: Union[str, Enum], return has_permission(permission, permissions_as_dict(permissions), record) -class TokenIdentityPolicy(SessionIdentityPolicy): # type: ignore[misc,no-any-unimported] +class TokenIdentityPolicy(SessionIdentityPolicy): def __init__(self, fernet: Fernet, schema: Schema): super().__init__() self._fernet = fernet @@ -130,7 +130,7 @@ async def identify(self, request: web.Request) -> Optional[str]: # Both identites must match. return token_identity if token_identity == cookie_identity else None - async def remember(self, request: web.Request, response: web.Response, + async def remember(self, request: web.Request, response: web.StreamResponse, identity: str, **kwargs: object) -> None: """Send auth tokens to client for authentication.""" # For proper security we send a token for JS to store and an HTTP only cookie: @@ -140,7 +140,7 @@ async def remember(self, request: web.Request, response: web.Response, # Send httponly cookie, which will be invisible to JS. await super().remember(request, response, identity, **kwargs) - async def forget(self, request: web.Request, response: web.Response) -> None: + async def forget(self, request: web.Request, response: web.StreamResponse) -> None: """Delete session cookie (JS client should choose to delete its token).""" await super().forget(request, response) From b1b2ccc64b9f11293c57ef8d973ff25fb46027c3 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 19 Nov 2023 23:47:16 +0000 Subject: [PATCH 05/17] Update test_security.py --- tests/test_security.py | 78 +++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index ff30e67e..f4520ee2 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -7,9 +7,9 @@ from aiohttp_security import AbstractAuthorizationPolicy from aiohttp_admin import Permissions, UserDetails -from conftest import admin, db, model, model2 +from conftest import IdentityCallback, admin, db, model, model2 -_CreateClient = Callable[[AbstractAuthorizationPolicy], Awaitable[TestClient]] +_CreateClient = Callable[[IdentityCallback], Awaitable[TestClient]] _Login = Callable[[TestClient], Awaitable[dict[str, str]]] @@ -102,7 +102,7 @@ async def test_login_invalid_payload(admin_client: TestClient) -> None: assert result[1]["input"] is None -async def test_list_without_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_list_without_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -122,7 +122,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: # assert await resp.text() == expected -async def test_get_resource_with_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_get_resource_with_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -138,7 +138,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "1"}} -async def test_get_resource_with_wildcard_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_get_resource_with_wildcard_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -154,7 +154,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "1"}} -async def test_get_resource_with_negative_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_get_resource_with_negative_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -184,7 +184,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: # expected = "403: User does not have 'admin.dummy2.create' permission" -async def test_list_resource_finegrained_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_list_resource_finegrained_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -202,7 +202,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": [{"id": "3"}, {"id": "2"}, {"id": "1"}], "total": 3} -async def test_get_resource_finegrained_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_get_resource_finegrained_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -218,7 +218,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "1"}} -async def test_get_many_resource_finegrained_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_get_many_resource_finegrained_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -234,7 +234,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": [{"id": "1"}]} -async def test_create_resource_finegrained_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_create_resource_finegrained_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -256,7 +256,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "4", "msg": None}} -async def test_create_resource_filtered_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_create_resource_filtered_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -278,7 +278,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "4"}} -async def test_update_resource_finegrained_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_update_resource_finegrained_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -295,7 +295,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "222", "msg": "Test"}} -async def test_update_resource_filtered_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_update_resource_filtered_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -337,7 +337,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: # expected = "403: User does not have 'admin.dummy2.msg.edit' permission" -async def test_delete_resource_filtered_permission(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_delete_resource_filtered_permission(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -354,7 +354,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "1"}} -async def test_permissions_cached(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permissions_cached(create_admin_client: _CreateClient, login: _Login) -> None: identity_callback = mock.AsyncMock(spec_set=(), return_value={"permissions": {"admin.*"}}) admin_client = await create_admin_client(identity_callback) @@ -373,7 +373,7 @@ async def test_permissions_cached(create_admin_client: _CreateClient, # type: i identity_callback.assert_called_once() -async def test_permission_filter_list(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_list(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"|msg="Foo"')} @@ -396,7 +396,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: "total": 3} -async def test_permission_filter_list2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_list2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.view|msg="Test"')} @@ -414,7 +414,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: "data": [{"id": "2", "msg": "Test"}, {"id": "1", "msg": "Test"}], "total": 2} -async def test_permission_filter_get_one(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_get_one(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"')} @@ -431,7 +431,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 403 -async def test_permission_filter_get_one2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_get_one2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.view|msg="Test"')} @@ -448,7 +448,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 403 -async def test_permission_filter_get_many(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_get_many(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"')} @@ -466,7 +466,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": []} -async def test_permission_filter_get_many2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_get_many2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.view|msg="Test"')} @@ -484,7 +484,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": []} -async def test_permission_filter_create(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_create(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"')} @@ -503,7 +503,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 403 -async def test_permission_filter_create2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_create2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.add|msg="Test"')} @@ -522,7 +522,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 403 -async def test_permission_filter_update(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_update(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"')} @@ -543,7 +543,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 200 -async def test_permission_filter_update2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_update2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.edit|msg="Test"')} @@ -608,7 +608,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 200 -async def test_permission_filter_delete(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_delete(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"')} @@ -626,7 +626,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 200 -async def test_permission_filter_delete2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_delete2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.delete|msg="Test"')} @@ -644,7 +644,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 200 -async def test_permission_filter_delete_many(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_delete_many(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.*|msg="Test"')} @@ -666,7 +666,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": ["1", "2"]} -async def test_permission_filter_delete_many2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_delete_many2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", 'admin.dummy2.delete|msg="Test"')} @@ -688,7 +688,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": ["1", "2"]} -async def test_permission_filter_field_list(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_list(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.*|id=1|id=2")} @@ -707,7 +707,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: "total": 3} -async def test_permission_filter_field_list2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_list2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.view|id=1|id=3")} @@ -726,7 +726,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: "total": 3} -async def test_permission_filter_field_get_one(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_get_one(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.*|id=1|id=2")} @@ -744,7 +744,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "3"}} -async def test_permission_filter_field_get_one2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_get_one2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.view|id=1|id=2")} @@ -762,7 +762,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": {"id": "3"}} -async def test_permission_filter_field_get_many(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_get_many(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.*|id=1|id=2")} @@ -777,7 +777,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": [{"id": "2", "msg": "Test"}, {"id": "3"}]} -async def test_permission_filter_field_get_many2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_get_many2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.view|id=1|id=2")} @@ -792,7 +792,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": [{"id": "1", "msg": "Test"}, {"id": "3"}]} -async def test_permission_filter_field_create(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_create(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.*|id=1|id=2")} @@ -814,7 +814,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert r.msg is None -async def test_permission_filter_field_create2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_create2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.add|id=1|id=2")} @@ -836,7 +836,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert r.msg is None -async def test_permission_filter_field_update(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_update(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.*|id=1|id=2")} @@ -866,7 +866,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert r.msg == "Test" -async def test_permission_filter_field_update2(create_admin_client: _CreateClient, # type: ignore[no-any-unimported] # noqa: B950 +async def test_permission_filter_field_update2(create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: return {"permissions": ("admin.*", "admin.dummy2.msg.edit|id=1|id=2")} From d1581f1e46a78e55319ff497374cd7c10fdf9f58 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 19 Nov 2023 23:47:45 +0000 Subject: [PATCH 06/17] Update conftest.py --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7392faa2..9fe4ff44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from _auth import check_credentials from aiohttp_admin.backends.sqlalchemy import SAResource -_IdentityCallback = Callable[[str], Awaitable[aiohttp_admin.UserDetails]] +IdentityCallback = Callable[[Optional[str]], Awaitable[aiohttp_admin.UserDetails]] class Base(DeclarativeBaseNoMeta): """Base model.""" From eb119aea3b90b5869a5e7c8971c6f9c69ca2459c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 19 Nov 2023 23:49:45 +0000 Subject: [PATCH 07/17] Update conftest.py --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9fe4ff44..a82d512c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,8 +42,8 @@ def mock_engine() -> AsyncMock: @pytest.fixture def create_admin_client( aiohttp_client: Callable[[web.Application], Awaitable[TestClient]] -) -> Callable[[Optional[_IdentityCallback]], Awaitable[TestClient]]: - async def admin_client(identity_callback: Optional[_IdentityCallback] = None) -> TestClient: +) -> Callable[[Optional[IdentityCallback]], Awaitable[TestClient]]: + async def admin_client(identity_callback: Optional[IdentityCallback] = None) -> TestClient: app = web.Application() app[model] = DummyModel app[model2] = Dummy2Model From e2a0f3cef10eec775ef92ff70f8bc4e1e5fb0470 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 19 Nov 2023 23:51:21 +0000 Subject: [PATCH 08/17] Update test_security.py --- tests/test_security.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index f4520ee2..830ec883 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -319,7 +319,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert r.msg == "Test" -async def test_update_many_resource_finegrained_permission( # type: ignore[no-any-unimported] +async def test_update_many_resource_finegrained_permission( create_admin_client: _CreateClient, login: _Login) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: assert identity == "admin" @@ -564,7 +564,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 200 -async def test_permission_filter_update_many( # type: ignore[no-any-unimported] +async def test_permission_filter_update_many( create_admin_client: _CreateClient, login: _Login ) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: @@ -586,7 +586,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert resp.status == 200 -async def test_permission_filter_update_many2( # type: ignore[no-any-unimported] +async def test_permission_filter_update_many2( create_admin_client: _CreateClient, login: _Login ) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: @@ -896,7 +896,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert r.msg == "Test" -async def test_permission_filter_field_update_many( # type: ignore[no-any-unimported] +async def test_permission_filter_field_update_many( create_admin_client: _CreateClient, login: _Login ) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: @@ -916,7 +916,7 @@ async def identity_callback(identity: Optional[str]) -> UserDetails: assert await resp.json() == {"data": ["1", "2"]} -async def test_permission_filter_field_update_many2( # type: ignore[no-any-unimported] +async def test_permission_filter_field_update_many2( create_admin_client: _CreateClient, login: _Login ) -> None: async def identity_callback(identity: Optional[str]) -> UserDetails: From 292a4dd1725353e2a4e855c650af52576dde1384 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:17:03 +0000 Subject: [PATCH 09/17] Update security.py --- aiohttp_admin/security.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aiohttp_admin/security.py b/aiohttp_admin/security.py index 4e3c5a94..2a3025f5 100644 --- a/aiohttp_admin/security.py +++ b/aiohttp_admin/security.py @@ -79,8 +79,11 @@ def __init__(self, schema: Schema): async def authorized_userid(self, identity: str) -> str: return identity - async def permits(self, identity: Optional[str], permission: Union[str, Enum], - context: tuple[web.Request, Optional[Mapping[str, object]]]) -> bool: + async def permits( + self, identity: Optional[str], permission: Union[str, Enum], + context: Optional[tuple[web.Request, Optional[Mapping[str, object]]]] = None + ) -> bool: + assert context is not None if identity is None: return False From 52c12ed049947e2ddbffe9710f677102d5cea705 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:18:55 +0000 Subject: [PATCH 10/17] Update aiohttp_admin/security.py --- aiohttp_admin/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_admin/security.py b/aiohttp_admin/security.py index 2a3025f5..d1304f81 100644 --- a/aiohttp_admin/security.py +++ b/aiohttp_admin/security.py @@ -141,7 +141,7 @@ async def remember(self, request: web.Request, response: web.StreamResponse, # Send token that will be saved in local storage by the JS client. response.headers["X-Token"] = json.dumps(await self.user_identity_dict(request, identity)) # Send httponly cookie, which will be invisible to JS. - await super().remember(request, response, identity, **kwargs) + await super().remember(request, response, identity, **kwargs) # type: ignore[arg-type] async def forget(self, request: web.Request, response: web.StreamResponse) -> None: """Delete session cookie (JS client should choose to delete its token).""" From e812e4930c5c599efe2394a58f195e729422634f Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:21:25 +0000 Subject: [PATCH 11/17] Update conftest.py --- tests/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index a82d512c..6fa855f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from typing import Optional, Type +from typing import Optional from unittest.mock import AsyncMock, create_autospec import pytest @@ -14,20 +14,24 @@ IdentityCallback = Callable[[Optional[str]], Awaitable[aiohttp_admin.UserDetails]] + class Base(DeclarativeBaseNoMeta): """Base model.""" + class DummyModel(Base): __tablename__ = "dummy" id: Mapped[int] = mapped_column(primary_key=True) + class Dummy2Model(Base): __tablename__ = "dummy2" id: Mapped[int] = mapped_column(primary_key=True) msg: Mapped[Optional[str]] + model = web.AppKey[type[DummyModel]]("model") model2 = web.AppKey[type[Dummy2Model]]("model2") db = web.AppKey("db", async_sessionmaker[AsyncSession]) From 8778ae0acc2b2761e3eab0073780366eec4c9f93 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:22:04 +0000 Subject: [PATCH 12/17] Update test_security.py --- tests/test_security.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index 830ec883..44e64e32 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -4,10 +4,9 @@ from unittest import mock from aiohttp.test_utils import TestClient -from aiohttp_security import AbstractAuthorizationPolicy from aiohttp_admin import Permissions, UserDetails -from conftest import IdentityCallback, admin, db, model, model2 +from conftest import IdentityCallback, admin, db, model2 _CreateClient = Callable[[IdentityCallback], Awaitable[TestClient]] _Login = Callable[[TestClient], Awaitable[dict[str, str]]] From 082c507a45c9fd281a42b5432f767521bbb5362b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:48:36 +0000 Subject: [PATCH 13/17] Update __init__.py --- aiohttp_admin/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiohttp_admin/__init__.py b/aiohttp_admin/__init__.py index ab94d2ce..145faba7 100644 --- a/aiohttp_admin/__init__.py +++ b/aiohttp_admin/__init__.py @@ -65,7 +65,8 @@ def value(r: web.RouteDef) -> tuple[str, str]: for res in schema["resources"]: m = res["model"] - admin[state_key]["resources"][m.name]["urls"].update((key(r), value(r)) for r in m.routes) + urls = admin[state_key]["resources"][m.name]["urls"] + urls.update((key(r), value(r)) for r in m.routes) schema = check(Schema, schema) if secret is None: @@ -75,7 +76,8 @@ def value(r: web.RouteDef) -> tuple[str, str]: admin.middlewares.append(pydantic_middleware) admin.on_startup.append(on_startup) admin[check_credentials_key] = schema["security"]["check_credentials"] - admin[state_key] = State({"view": schema.get("view", {}), "js_module": schema.get("js_module"), "urls": {}, "resources": {}}) + admin[state_key] = State({"view": schema.get("view", {}), "js_module": schema.get("js_module"), + "urls": {}, "resources": {}}) max_age = schema["security"].get("max_age") secure = schema["security"].get("secure", True) From c3e6e41d20e49cfb76cec3cb6e1131b7353524a5 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:50:07 +0000 Subject: [PATCH 14/17] Update sqlalchemy.py --- aiohttp_admin/backends/sqlalchemy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiohttp_admin/backends/sqlalchemy.py b/aiohttp_admin/backends/sqlalchemy.py index 6cfbd6a8..bff59e5b 100644 --- a/aiohttp_admin/backends/sqlalchemy.py +++ b/aiohttp_admin/backends/sqlalchemy.py @@ -26,6 +26,7 @@ _FValues = Union[bool, int, str] _Filters = dict[Union[sa.Column[object], QueryableAttribute[Any]], Union[_FValues, Sequence[_FValues]]] +_ModelOrTable = Union[sa.Table, type[DeclarativeBase], type[DeclarativeBaseNoMeta]] logger = logging.getLogger(__name__) @@ -155,7 +156,7 @@ def create_filters(columns: sa.ColumnCollection[str, sa.Column[object]], # ID is based on PK, which we can't infer from types, so must use Any here. class SAResource(AbstractAdminResource[Any]): - def __init__(self, db: AsyncEngine, model_or_table: Union[sa.Table, type[DeclarativeBase], type[DeclarativeBaseNoMeta]]): + def __init__(self, db: AsyncEngine, model_or_table: _ModelOrTable): if isinstance(model_or_table, sa.Table): table = model_or_table else: @@ -222,7 +223,8 @@ def __init__(self, db: AsyncEngine, model_or_table: Union[sa.Table, type[Declara if not isinstance(model_or_table, sa.Table): # Append fields to represent ORM relationships. # Mypy doesn't handle union well here. - mapper = cast(Union[Mapper[DeclarativeBase], Mapper[DeclarativeBaseNoMeta]], sa.inspect(model_or_table)) + mapper = cast(Union[Mapper[DeclarativeBase], Mapper[DeclarativeBaseNoMeta]], + sa.inspect(model_or_table)) assert mapper is not None # noqa: S101 for name, relationship in mapper.relationships.items(): # https://github.com/sqlalchemy/sqlalchemy/discussions/10161#discussioncomment-6583442 From 18145e84697f5980bb7ccc524d3fde84f8184539 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:57:10 +0000 Subject: [PATCH 15/17] Update .flake8 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 6f42d352..e0b12106 100644 --- a/.flake8 +++ b/.flake8 @@ -16,7 +16,7 @@ per-file-ignores = examples/*:I900,S105 # flake8-import-order -application-import-names = aiohttp_admin, _auth, _auth_helpers, _models, _resources +application-import-names = aiohttp_admin, conftest, _auth, _auth_helpers, _models, _resources import-order-style = pycharm # flake8-quotes From 66e8a23da88d1d94d8c6c3ab33043758c9fee0eb Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 17:57:59 +0000 Subject: [PATCH 16/17] Apply suggestions from code review --- aiohttp_admin/routes.py | 2 +- aiohttp_admin/security.py | 3 ++- tests/conftest.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/aiohttp_admin/routes.py b/aiohttp_admin/routes.py index a8a40185..32306f1e 100644 --- a/aiohttp_admin/routes.py +++ b/aiohttp_admin/routes.py @@ -6,7 +6,7 @@ from aiohttp import web from . import views -from .types import _ResourceState, Schema, resources_key, state_key +from .types import Schema, _ResourceState, resources_key, state_key def setup_resources(admin: web.Application, schema: Schema) -> None: diff --git a/aiohttp_admin/security.py b/aiohttp_admin/security.py index d1304f81..9648cc03 100644 --- a/aiohttp_admin/security.py +++ b/aiohttp_admin/security.py @@ -83,7 +83,8 @@ async def permits( self, identity: Optional[str], permission: Union[str, Enum], context: Optional[tuple[web.Request, Optional[Mapping[str, object]]]] = None ) -> bool: - assert context is not None + # TODO: https://github.com/aio-libs/aiohttp-security/issues/677 + assert context is not None # noqa: S101 if identity is None: return False diff --git a/tests/conftest.py b/tests/conftest.py index 6fa855f4..b63d22be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,8 @@ import pytest from aiohttp import web from aiohttp.test_utils import TestClient -from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.ext.asyncio import (AsyncEngine, AsyncSession, + async_sessionmaker, create_async_engine) from sqlalchemy.orm import DeclarativeBaseNoMeta, Mapped, mapped_column import aiohttp_admin From e8f6e5f0e1b4487ea1a7e97cc36bb3005b9b05db Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Nov 2023 18:00:12 +0000 Subject: [PATCH 17/17] Update tests/test_backends_abc.py --- tests/test_backends_abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_backends_abc.py b/tests/test_backends_abc.py index e14121c9..9f877f4c 100644 --- a/tests/test_backends_abc.py +++ b/tests/test_backends_abc.py @@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable from aiohttp.test_utils import TestClient + from conftest import admin _Login = Callable[[TestClient], Awaitable[dict[str, str]]]