From 4fb9335df94b4715f47cbd1d153b93e7ea81beb2 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Wed, 22 Feb 2023 18:47:56 +0200 Subject: [PATCH] add ability to read cookie secret from config --- docs/configuration.rst | 1 + src/ahriman/core/configuration/schema.py | 3 +++ src/ahriman/web/middlewares/auth_handler.py | 27 +++++++++++++++---- src/ahriman/web/web.py | 2 +- .../web/middlewares/test_auth_handler.py | 25 ++++++++++++++--- 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index b64c586b..98aea927 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -50,6 +50,7 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr * ``allow_read_only`` - allow requesting status APIs without authorization, boolean, required. * ``client_id`` - OAuth2 application client ID, string, required in case if ``oauth`` is used. * ``client_secret`` - OAuth2 application client secret key, string, required in case if ``oauth`` is used. +* ``cookie_secret_key`` - secret key which will be used for cookies encryption, string, optional. It must be 32 url-safe base64-encoded bytes and can be generated as following ``base64.urlsafe_b64encode(os.urandom(32)).decode("utf8")``. If not set, it will be generated automatically; note, however, that in this case, all sessions will be automatically expired during restart. * ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days. * ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used. * ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used. diff --git a/src/ahriman/core/configuration/schema.py b/src/ahriman/core/configuration/schema.py index 76e182ba..202ec369 100644 --- a/src/ahriman/core/configuration/schema.py +++ b/src/ahriman/core/configuration/schema.py @@ -109,6 +109,9 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = { "client_secret": { "type": "string", }, + "cookie_secret_key": { + "type": "string", + }, "max_age": { "type": "integer", "coerce": "integer", diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py index a881e84b..b47ef8ba 100644 --- a/src/ahriman/web/middlewares/auth_handler.py +++ b/src/ahriman/web/middlewares/auth_handler.py @@ -18,7 +18,6 @@ # along with this program. If not, see . # import aiohttp_security # type: ignore -import base64 import socket import types @@ -32,12 +31,13 @@ from cryptography import fernet from typing import Optional from ahriman.core.auth import Auth +from ahriman.core.configuration import Configuration from ahriman.models.user_access import UserAccess from ahriman.models.user_identity import UserIdentity from ahriman.web.middlewares import HandlerType, MiddlewareType -__all__ = ["AuthorizationPolicy", "auth_handler", "setup_auth"] +__all__ = ["AuthorizationPolicy", "auth_handler", "cookie_secret_key", "setup_auth"] class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type: ignore @@ -125,19 +125,36 @@ def auth_handler(allow_read_only: bool) -> MiddlewareType: return handle -def setup_auth(application: web.Application, validator: Auth) -> web.Application: +def cookie_secret_key(configuration: Configuration) -> fernet.Fernet: + """ + extract cookie secret key from configuration if set or generate new one + + Args: + configuration(Configuration): configuration instance + + Returns: + fernet.Fernet: fernet key instance + """ + if (secret_key := configuration.get("auth", "cookie_secret_key", fallback=None)) is not None: + return fernet.Fernet(secret_key) + + secret_key = fernet.Fernet.generate_key() + return fernet.Fernet(secret_key) + + +def setup_auth(application: web.Application, configuration: Configuration, validator: Auth) -> web.Application: """ setup authorization policies for the application Args: application(web.Application): web application instance + configuration(Configuration): configuration instance validator(Auth): authorization module instance Returns: web.Application: configured web application """ - fernet_key = fernet.Fernet.generate_key() - secret_key = base64.urlsafe_b64decode(fernet_key) + secret_key = cookie_secret_key(configuration) storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age) setup_session(application, storage) diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 6fb631f9..666da10a 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -168,6 +168,6 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw validator = application["validator"] = Auth.load(configuration, database) if validator.enabled: from ahriman.web.middlewares.auth_handler import setup_auth - setup_auth(application, validator) + setup_auth(application, configuration, validator) return application diff --git a/tests/ahriman/web/middlewares/test_auth_handler.py b/tests/ahriman/web/middlewares/test_auth_handler.py index f1c1ff22..e23ff17b 100644 --- a/tests/ahriman/web/middlewares/test_auth_handler.py +++ b/tests/ahriman/web/middlewares/test_auth_handler.py @@ -3,14 +3,16 @@ import socket from aiohttp import web from aiohttp.test_utils import TestClient +from cryptography import fernet from pytest_mock import MockerFixture from unittest.mock import AsyncMock from ahriman.core.auth import Auth +from ahriman.core.configuration import Configuration from ahriman.models.user import User from ahriman.models.user_access import UserAccess from ahriman.models.user_identity import UserIdentity -from ahriman.web.middlewares.auth_handler import auth_handler, AuthorizationPolicy, setup_auth +from ahriman.web.middlewares.auth_handler import AuthorizationPolicy, auth_handler, cookie_secret_key, setup_auth def _identity(username: str) -> str: @@ -175,11 +177,28 @@ async def test_auth_handler_write(mocker: MockerFixture) -> None: check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path) -def test_setup_auth(application_with_auth: web.Application, auth: Auth, mocker: MockerFixture) -> None: +def test_cookie_secret_key(configuration: Configuration) -> None: + """ + must generate fernet key + """ + secret_key = cookie_secret_key(configuration) + assert isinstance(secret_key, fernet.Fernet) + + +def test_cookie_secret_key_cached(configuration: Configuration) -> None: + """ + must use cookie key as set by configuration + """ + configuration.set_option("auth", "cookie_secret_key", fernet.Fernet.generate_key().decode("utf8")) + assert cookie_secret_key(configuration) is not None + + +def test_setup_auth(application_with_auth: web.Application, configuration: Configuration, auth: Auth, + mocker: MockerFixture) -> None: """ must set up authorization """ setup_mock = mocker.patch("aiohttp_security.setup") - application = setup_auth(application_with_auth, auth) + application = setup_auth(application_with_auth, configuration, auth) assert application.get("validator") is not None setup_mock.assert_called_once_with(application_with_auth, pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))