add ability to read cookie secret from config

This commit is contained in:
Evgenii Alekseev 2023-02-22 18:47:56 +02:00
parent 96f394bab0
commit cbcfff27b8
5 changed files with 49 additions and 9 deletions

View File

@ -50,6 +50,7 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
* ``allow_read_only`` - allow requesting status APIs without authorization, boolean, required. * ``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_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. * ``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. * ``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_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. * ``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.

View File

@ -109,6 +109,9 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"client_secret": { "client_secret": {
"type": "string", "type": "string",
}, },
"cookie_secret_key": {
"type": "string",
},
"max_age": { "max_age": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import aiohttp_security # type: ignore import aiohttp_security # type: ignore
import base64
import socket import socket
import types import types
@ -32,12 +31,13 @@ from cryptography import fernet
from typing import Optional from typing import Optional
from ahriman.core.auth import Auth from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.models.user_identity import UserIdentity from ahriman.models.user_identity import UserIdentity
from ahriman.web.middlewares import HandlerType, MiddlewareType 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 class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type: ignore
@ -125,19 +125,36 @@ def auth_handler(allow_read_only: bool) -> MiddlewareType:
return handle 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 setup authorization policies for the application
Args: Args:
application(web.Application): web application instance application(web.Application): web application instance
configuration(Configuration): configuration instance
validator(Auth): authorization module instance validator(Auth): authorization module instance
Returns: Returns:
web.Application: configured web application web.Application: configured web application
""" """
fernet_key = fernet.Fernet.generate_key() secret_key = cookie_secret_key(configuration)
secret_key = base64.urlsafe_b64decode(fernet_key)
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age) storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
setup_session(application, storage) setup_session(application, storage)

View File

@ -168,6 +168,6 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
validator = application["validator"] = Auth.load(configuration, database) validator = application["validator"] = Auth.load(configuration, database)
if validator.enabled: if validator.enabled:
from ahriman.web.middlewares.auth_handler import setup_auth from ahriman.web.middlewares.auth_handler import setup_auth
setup_auth(application, validator) setup_auth(application, configuration, validator)
return application return application

View File

@ -3,14 +3,16 @@ import socket
from aiohttp import web from aiohttp import web
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from cryptography import fernet
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from ahriman.core.auth import Auth from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.models.user_identity import UserIdentity 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: 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) 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 must set up authorization
""" """
setup_mock = mocker.patch("aiohttp_security.setup") 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 assert application.get("validator") is not None
setup_mock.assert_called_once_with(application_with_auth, pytest.helpers.anyvar(int), pytest.helpers.anyvar(int)) setup_mock.assert_called_once_with(application_with_auth, pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))