diff --git a/docs/ahriman.web.rst b/docs/ahriman.web.rst index a2641675..59a59712 100644 --- a/docs/ahriman.web.rst +++ b/docs/ahriman.web.rst @@ -30,6 +30,14 @@ ahriman.web.cors module :no-undoc-members: :show-inheritance: +ahriman.web.keys module +----------------------- + +.. automodule:: ahriman.web.keys + :members: + :no-undoc-members: + :show-inheritance: + ahriman.web.routes module ------------------------- diff --git a/src/ahriman/web/apispec.py b/src/ahriman/web/apispec.py index 02436084..9a344cdd 100644 --- a/src/ahriman/web/apispec.py +++ b/src/ahriman/web/apispec.py @@ -23,7 +23,7 @@ from aiohttp.web import Application from typing import Any from ahriman import __version__ -from ahriman.core.configuration import Configuration +from ahriman.web.keys import ConfigurationKey __all__ = ["setup_apispec"] @@ -89,7 +89,7 @@ def _servers(application: Application) -> list[dict[str, Any]]: Returns: list[dict[str, Any]]: list (actually only one) of defined web urls """ - configuration: Configuration = application["configuration"] + configuration = application[ConfigurationKey] address = configuration.get("web", "address", fallback=None) if not address: host = configuration.get("web", "host") diff --git a/src/ahriman/web/keys.py b/src/ahriman/web/keys.py new file mode 100644 index 00000000..04e5a323 --- /dev/null +++ b/src/ahriman/web/keys.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from aiohttp.web import AppKey + +from ahriman.core.auth import Auth +from ahriman.core.configuration import Configuration +from ahriman.core.spawn import Spawn +from ahriman.core.status.watcher import Watcher +from ahriman.models.repository_id import RepositoryId + + +__all__ = [ + "AuthKey", + "ConfigurationKey", + "SpawnKey", + "WatcherKey", +] + + +AuthKey = AppKey("validator", Auth) +ConfigurationKey = AppKey("configuration", Configuration) +SpawnKey = AppKey("spawn", Spawn) +WatcherKey = AppKey("watcher", dict[RepositoryId, Watcher]) diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py index 640b1a09..06831b35 100644 --- a/src/ahriman/web/middlewares/auth_handler.py +++ b/src/ahriman/web/middlewares/auth_handler.py @@ -154,7 +154,7 @@ def setup_auth(application: Application, configuration: Configuration, validator setup_session(application, storage) authorization_policy = _AuthorizationPolicy(validator) - identity_policy = application["identity"] = aiohttp_security.SessionIdentityPolicy() + identity_policy = aiohttp_security.SessionIdentityPolicy() aiohttp_security.setup(application, identity_policy, authorization_policy) application.middlewares.append(_auth_handler(validator.allow_read_only)) diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py index 300ba18a..87905731 100644 --- a/src/ahriman/web/views/base.py +++ b/src/ahriman/web/views/base.py @@ -29,6 +29,7 @@ from ahriman.core.spawn import Spawn from ahriman.core.status.watcher import Watcher from ahriman.models.repository_id import RepositoryId from ahriman.models.user_access import UserAccess +from ahriman.web.keys import AuthKey, ConfigurationKey, SpawnKey, WatcherKey T = TypeVar("T", str, list[str]) @@ -54,8 +55,7 @@ class BaseView(View, CorsViewMixin): Returns: Configuration: configuration instance """ - configuration: Configuration = self.request.app["configuration"] - return configuration + return self.request.app[ConfigurationKey] @property def services(self) -> dict[RepositoryId, Watcher]: @@ -65,8 +65,7 @@ class BaseView(View, CorsViewMixin): Returns: dict[RepositoryId, Watcher]: map of loaded watchers per known repository """ - watchers: dict[RepositoryId, Watcher] = self.request.app["watcher"] - return watchers + return self.request.app[WatcherKey] @property def sign(self) -> GPG: @@ -86,8 +85,7 @@ class BaseView(View, CorsViewMixin): Returns: Spawn: external process spawner instance """ - spawner: Spawn = self.request.app["spawn"] - return spawner + return self.request.app[SpawnKey] @property def validator(self) -> Auth: @@ -97,8 +95,7 @@ class BaseView(View, CorsViewMixin): Returns: Auth: authorization service instance """ - validator: Auth = self.request.app["validator"] - return validator + return self.request.app[AuthKey] @classmethod async def get_permission(cls, request: Request) -> UserAccess: diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 15f5a319..64a48f44 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -34,6 +34,7 @@ from ahriman.core.status.watcher import Watcher from ahriman.models.repository_id import RepositoryId from ahriman.web.apispec import setup_apispec from ahriman.web.cors import setup_cors +from ahriman.web.keys import AuthKey, ConfigurationKey, SpawnKey, WatcherKey from ahriman.web.middlewares.exception_handler import exception_handler from ahriman.web.routes import setup_routes @@ -97,7 +98,7 @@ async def _on_startup(application: Application) -> None: application.logger.info("server started") try: - for watcher in application["watcher"].values(): + for watcher in application[WatcherKey].values(): watcher.load() except Exception: message = "could not load packages" @@ -114,7 +115,7 @@ def run_server(application: Application) -> None: """ application.logger.info("start server") - configuration: Configuration = application["configuration"] + configuration = application[ConfigurationKey] host = configuration.get("web", "host") port = configuration.getint("web", "port") unix_socket = _create_socket(configuration, application) @@ -156,7 +157,7 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis aiohttp_jinja2.setup(application, trim_blocks=True, lstrip_blocks=True, autoescape=True, loader=loader) application.logger.info("setup configuration") - application["configuration"] = configuration + application[ConfigurationKey] = configuration application.logger.info("setup watchers") if not repositories: @@ -166,13 +167,13 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis for repository_id in repositories: application.logger.info("load repository %s", repository_id) watchers[repository_id] = Watcher(repository_id, database) - application["watcher"] = watchers + application[WatcherKey] = watchers application.logger.info("setup process spawner") - application["spawn"] = spawner + application[SpawnKey] = spawner application.logger.info("setup authorization") - validator = application["validator"] = Auth.load(configuration, database) + validator = application[AuthKey] = Auth.load(configuration, database) if validator.enabled: from ahriman.web.middlewares.auth_handler import setup_auth setup_auth(application, configuration, validator) diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index 5f53d05d..3e232d63 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -16,6 +16,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.database import SQLite from ahriman.core.spawn import Spawn from ahriman.models.user import User +from ahriman.web.keys import AuthKey from ahriman.web.web import setup_server @@ -159,7 +160,7 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa _, repository_id = configuration.check_loaded() application = setup_server(configuration, spawner, [repository_id]) - generated = user.hash_password(application["validator"].salt) + generated = user.hash_password(application[AuthKey].salt) mocker.patch("ahriman.core.database.SQLite.user_get", return_value=generated) return application @@ -245,5 +246,5 @@ def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseE TestClient: web client test instance """ mocker.patch("pathlib.Path.iterdir", return_value=[]) - application_with_auth["validator"] = MagicMock(spec=OAuth) + application_with_auth[AuthKey] = MagicMock(spec=OAuth) return event_loop.run_until_complete(aiohttp_client(application_with_auth)) diff --git a/tests/ahriman/web/middlewares/test_auth_handler.py b/tests/ahriman/web/middlewares/test_auth_handler.py index a05c9ec1..2f6bc0ca 100644 --- a/tests/ahriman/web/middlewares/test_auth_handler.py +++ b/tests/ahriman/web/middlewares/test_auth_handler.py @@ -12,6 +12,7 @@ from ahriman.core.configuration import Configuration from ahriman.models.build_status import BuildStatusEnum from ahriman.models.user import User from ahriman.models.user_access import UserAccess +from ahriman.web.keys import AuthKey from ahriman.web.middlewares.auth_handler import _AuthorizationPolicy, _auth_handler, _cookie_secret_key, setup_auth @@ -192,5 +193,5 @@ def test_setup_auth(application_with_auth: Application, configuration: Configura """ setup_mock = mocker.patch("aiohttp_security.setup") application = setup_auth(application_with_auth, configuration, auth) - assert application.get("validator") is not None + assert application.get(AuthKey) is not None setup_mock.assert_called_once_with(application_with_auth, pytest.helpers.anyvar(int), pytest.helpers.anyvar(int)) diff --git a/tests/ahriman/web/test_apispec.py b/tests/ahriman/web/test_apispec.py index f55ce175..47df2ca9 100644 --- a/tests/ahriman/web/test_apispec.py +++ b/tests/ahriman/web/test_apispec.py @@ -5,6 +5,7 @@ from pytest_mock import MockerFixture from ahriman import __version__ from ahriman.web.apispec import _info, _security, _servers, setup_apispec +from ahriman.web.keys import ConfigurationKey def test_info() -> None: @@ -36,7 +37,7 @@ def test_servers_address(application: Application) -> None: """ must generate servers definitions with address """ - application["configuration"].set_option("web", "address", "https://example.com") + application[ConfigurationKey].set_option("web", "address", "https://example.com") servers = _servers(application) assert servers == [{"url": "https://example.com"}] diff --git a/tests/ahriman/web/test_keys.py b/tests/ahriman/web/test_keys.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ahriman/web/test_web.py b/tests/ahriman/web/test_web.py index 4a9d4e32..317e9a9e 100644 --- a/tests/ahriman/web/test_web.py +++ b/tests/ahriman/web/test_web.py @@ -10,6 +10,7 @@ from ahriman.core.exceptions import InitializeError from ahriman.core.log.filtered_access_logger import FilteredAccessLogger from ahriman.core.spawn import Spawn from ahriman.core.status.watcher import Watcher +from ahriman.web.keys import ConfigurationKey from ahriman.web.web import _create_socket, _on_shutdown, _on_startup, run_server, setup_server @@ -18,14 +19,14 @@ async def test_create_socket(application: Application, mocker: MockerFixture) -> must create socket """ path = "/run/ahriman.sock" - application["configuration"].set_option("web", "unix_socket", str(path)) + application[ConfigurationKey].set_option("web", "unix_socket", str(path)) current_on_shutdown = len(application.on_shutdown) bind_mock = mocker.patch("socket.socket.bind") chmod_mock = mocker.patch("pathlib.Path.chmod") unlink_mock = mocker.patch("pathlib.Path.unlink") - sock = _create_socket(application["configuration"], application) + sock = _create_socket(application[ConfigurationKey], application) assert sock.family == socket.AF_UNIX assert sock.type == socket.SOCK_STREAM bind_mock.assert_called_once_with(str(path)) @@ -41,7 +42,7 @@ def test_create_socket_empty(application: Application) -> None: """ must skip socket creation if not set by configuration """ - assert _create_socket(application["configuration"], application) is None + assert _create_socket(application[ConfigurationKey], application) is None def test_create_socket_safe(application: Application, mocker: MockerFixture) -> None: @@ -49,14 +50,14 @@ def test_create_socket_safe(application: Application, mocker: MockerFixture) -> must create socket with default permission set """ path = "/run/ahriman.sock" - application["configuration"].set_option("web", "unix_socket", str(path)) - application["configuration"].set_option("web", "unix_socket_unsafe", "no") + application[ConfigurationKey].set_option("web", "unix_socket", str(path)) + application[ConfigurationKey].set_option("web", "unix_socket_unsafe", "no") mocker.patch("socket.socket.bind") mocker.patch("pathlib.Path.unlink") chmod_mock = mocker.patch("pathlib.Path.chmod") - sock = _create_socket(application["configuration"], application) + sock = _create_socket(application[ConfigurationKey], application) assert sock is not None chmod_mock.assert_not_called() @@ -97,7 +98,7 @@ def test_run(application: Application, mocker: MockerFixture) -> None: must run application """ port = 8080 - application["configuration"].set_option("web", "port", str(port)) + application[ConfigurationKey].set_option("web", "port", str(port)) run_application_mock = mocker.patch("ahriman.web.web.run_app") run_server(application) @@ -112,7 +113,7 @@ def test_run_with_auth(application_with_auth: Application, mocker: MockerFixture must run application with enabled authorization """ port = 8080 - application_with_auth["configuration"].set_option("web", "port", str(port)) + application_with_auth[ConfigurationKey].set_option("web", "port", str(port)) run_application_mock = mocker.patch("ahriman.web.web.run_app") run_server(application_with_auth) @@ -127,12 +128,12 @@ def test_run_with_socket(application: Application, mocker: MockerFixture) -> Non must run application """ port = 8080 - application["configuration"].set_option("web", "port", str(port)) + application[ConfigurationKey].set_option("web", "port", str(port)) socket_mock = mocker.patch("ahriman.web.web._create_socket", return_value=42) run_application_mock = mocker.patch("ahriman.web.web.run_app") run_server(application) - socket_mock.assert_called_once_with(application["configuration"], application) + socket_mock.assert_called_once_with(application[ConfigurationKey], application) run_application_mock.assert_called_once_with( application, host="127.0.0.1", port=port, sock=42, handle_signals=True, access_log=pytest.helpers.anyvar(int), access_log_class=FilteredAccessLogger diff --git a/tests/ahriman/web/views/test_view_base.py b/tests/ahriman/web/views/test_view_base.py index e01c5bdb..fce32fbc 100644 --- a/tests/ahriman/web/views/test_view_base.py +++ b/tests/ahriman/web/views/test_view_base.py @@ -9,6 +9,7 @@ from unittest.mock import AsyncMock from ahriman.core.configuration import Configuration from ahriman.models.repository_id import RepositoryId from ahriman.models.user_access import UserAccess +from ahriman.web.keys import WatcherKey from ahriman.web.views.base import BaseView @@ -172,9 +173,9 @@ def test_service(base: BaseView) -> None: must return service for repository """ repository_id = RepositoryId("i686", "repo") - base.request.app["watcher"] = { + base.request.app[WatcherKey] = { repository_id: watcher - for watcher in base.request.app["watcher"].values() + for watcher in base.request.app[WatcherKey].values() } assert base.service(repository_id) == base.services[repository_id] diff --git a/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py b/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py index ac531a28..2d33d753 100644 --- a/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py +++ b/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py @@ -5,6 +5,7 @@ from pytest_mock import MockerFixture from ahriman.models.user import User from ahriman.models.user_access import UserAccess +from ahriman.web.keys import AuthKey from ahriman.web.views.v1.user.login import LoginView @@ -45,7 +46,7 @@ async def test_get_redirect_to_oauth(client_with_oauth_auth: TestClient) -> None """ must redirect to OAuth service provider in case if no code is supplied """ - oauth = client_with_oauth_auth.app["validator"] + oauth = client_with_oauth_auth.app[AuthKey] oauth.get_oauth_url.return_value = "http://localhost" request_schema = pytest.helpers.schema_request(LoginView.get, location="querystring") @@ -60,7 +61,7 @@ async def test_get_redirect_to_oauth_empty_code(client_with_oauth_auth: TestClie """ must redirect to OAuth service provider in case if empty code is supplied """ - oauth = client_with_oauth_auth.app["validator"] + oauth = client_with_oauth_auth.app[AuthKey] oauth.get_oauth_url.return_value = "http://localhost" request_schema = pytest.helpers.schema_request(LoginView.get, location="querystring") @@ -75,7 +76,7 @@ async def test_get(client_with_oauth_auth: TestClient, mocker: MockerFixture) -> """ must log in user correctly from OAuth """ - oauth = client_with_oauth_auth.app["validator"] + oauth = client_with_oauth_auth.app[AuthKey] oauth.get_oauth_username.return_value = "user" oauth.known_username.return_value = True oauth.enabled = False # lol @@ -98,7 +99,7 @@ async def test_get_unauthorized(client_with_oauth_auth: TestClient, mocker: Mock """ must return unauthorized from OAuth """ - oauth = client_with_oauth_auth.app["validator"] + oauth = client_with_oauth_auth.app[AuthKey] oauth.known_username.return_value = False oauth.max_age = 60 remember_mock = mocker.patch("aiohttp_security.remember")