From b755088024c8e571a5bb33365cfa62e5496ef65b Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Wed, 1 Sep 2021 02:49:02 +0300 Subject: [PATCH] add login annd logout to index also improve auth --- package/share/ahriman/build-status.jinja2 | 13 +++ package/share/ahriman/login-form-hide.jinja2 | 9 ++ package/share/ahriman/login-form.jinja2 | 18 ++++ package/share/ahriman/style.jinja2 | 91 ++++++++++++++-- src/ahriman/core/auth/__init__.py | 0 src/ahriman/core/{ => auth}/auth.py | 55 +++++----- src/ahriman/core/auth/helpers.py | 70 ++++++++++++ src/ahriman/core/auth/mapping_auth.py | 86 +++++++++++++++ src/ahriman/web/middlewares/auth_handler.py | 30 +++--- src/ahriman/web/views/base.py | 2 +- src/ahriman/web/views/index.py | 5 + src/ahriman/web/views/login.py | 10 +- src/ahriman/web/views/logout.py | 7 +- src/ahriman/web/web.py | 7 +- tests/ahriman/conftest.py | 12 ++- tests/ahriman/core/auth/conftest.py | 14 +++ tests/ahriman/core/auth/test_auth.py | 81 ++++++++++++++ tests/ahriman/core/auth/test_helpers.py | 102 ++++++++++++++++++ tests/ahriman/core/auth/test_mapping_auth.py | 67 ++++++++++++ tests/ahriman/core/conftest.py | 11 +- tests/ahriman/core/test_auth.py | 83 -------------- tests/ahriman/core/upload/test_s3.py | 2 + tests/ahriman/web/conftest.py | 15 ++- tests/ahriman/web/middlewares/conftest.py | 5 +- .../web/middlewares/test_auth_handler.py | 51 ++++----- tests/ahriman/web/views/conftest.py | 15 +++ tests/ahriman/web/views/test_view_index.py | 29 +++-- tests/ahriman/web/views/test_view_login.py | 29 ++--- tests/ahriman/web/views/test_view_logout.py | 23 ++-- tests/testresources/core/ahriman.ini | 1 - 30 files changed, 713 insertions(+), 230 deletions(-) create mode 100644 package/share/ahriman/login-form-hide.jinja2 create mode 100644 package/share/ahriman/login-form.jinja2 create mode 100644 src/ahriman/core/auth/__init__.py rename src/ahriman/core/{ => auth}/auth.py (70%) create mode 100644 src/ahriman/core/auth/helpers.py create mode 100644 src/ahriman/core/auth/mapping_auth.py create mode 100644 tests/ahriman/core/auth/conftest.py create mode 100644 tests/ahriman/core/auth/test_auth.py create mode 100644 tests/ahriman/core/auth/test_helpers.py create mode 100644 tests/ahriman/core/auth/test_mapping_auth.py delete mode 100644 tests/ahriman/core/test_auth.py diff --git a/package/share/ahriman/build-status.jinja2 b/package/share/ahriman/build-status.jinja2 index 2355473c..4254d4ff 100644 --- a/package/share/ahriman/build-status.jinja2 +++ b/package/share/ahriman/build-status.jinja2 @@ -17,6 +17,8 @@ {{ service.status }} + {% include "login-form.jinja2" %} + {% include "login-form-hide.jinja2" %} {% include "search-line.jinja2" %}
@@ -46,6 +48,17 @@
  • ahriman
  • releases
  • report a bug
  • + {% if auth_enabled %} +
  • + {% if auth_username is not none %} +
    + +
    + {% else %} + + {% endif %} +
  • + {% endif %} diff --git a/package/share/ahriman/login-form-hide.jinja2 b/package/share/ahriman/login-form-hide.jinja2 new file mode 100644 index 00000000..af7afb38 --- /dev/null +++ b/package/share/ahriman/login-form-hide.jinja2 @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/package/share/ahriman/login-form.jinja2 b/package/share/ahriman/login-form.jinja2 new file mode 100644 index 00000000..b7f05424 --- /dev/null +++ b/package/share/ahriman/login-form.jinja2 @@ -0,0 +1,18 @@ +{#idea is from here https://www.w3schools.com/howto/howto_css_login_form.asp#} + \ No newline at end of file diff --git a/package/share/ahriman/style.jinja2 b/package/share/ahriman/style.jinja2 index 80a821c3..4fe43842 100644 --- a/package/share/ahriman/style.jinja2 +++ b/package/share/ahriman/style.jinja2 @@ -40,6 +40,7 @@ width: inherit; } + /* table description */ th, td { padding: 5px; } @@ -103,6 +104,7 @@ background-color: rgba(var(--color-success), 1.0); } + /* navigation footer description */ ul.navigation { list-style-type: none; margin: 0; @@ -115,11 +117,8 @@ float: left; } - ul.navigation li.status { - display: block; - text-align: center; - text-decoration: none; - padding: 14px 16px; + ul.navigation li.right { + float: right; } ul.navigation li a { @@ -131,6 +130,86 @@ } ul.navigation li a:hover { - background-color: rgba(var(--color-hover), 1.0); + opacity: 0.6; + } + + /* login button in footer and modal page */ + button.login { + background-color: rgba(var(--color-header), 1.0); + padding: 14px 16px; + border: none; + cursor: pointer; + width: 100%; + } + + button.login:hover { + opacity: 0.6; + } + + button.cancel { + background-color: rgba(var(--color-failed), 1.0); + padding: 14px 16px; + border: none; + cursor: pointer; + width: 100%; + } + + button.cancel:hover { + opacity: 0.6; + } + + /* modal page inputs and containers */ + input[type=text], input[type=password] { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border: 1px solid #ccc; + box-sizing: border-box; + } + + .login-container { + padding: 14px 16px; + } + + span.password { + float: right; + padding-top: 16px; + } + + .modal-login-form { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgb(0, 0, 0); + background-color: rgba(0, 0, 0, 0.4); + padding-top: 60px; + } + + .modal-login-form-content { + background-color: #fefefe; + margin: 5% auto 15% auto; + border: 1px solid #888; + width: 25%; + } + + /* modal page animation */ + .animate { + -webkit-animation: animatezoom 0.6s; + animation: animatezoom 0.6s + } + + @-webkit-keyframes animatezoom { + from {-webkit-transform: scale(0)} + to {-webkit-transform: scale(1)} + } + + @keyframes animatezoom { + from {transform: scale(0)} + to {transform: scale(1)} } diff --git a/src/ahriman/core/auth/__init__.py b/src/ahriman/core/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ahriman/core/auth.py b/src/ahriman/core/auth/auth.py similarity index 70% rename from src/ahriman/core/auth.py rename to src/ahriman/core/auth/auth.py index fcc92b8b..f6c7476f 100644 --- a/src/ahriman/core/auth.py +++ b/src/ahriman/core/auth/auth.py @@ -17,10 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from typing import Dict, Optional, Set +from __future__ import annotations + +from typing import Optional, Set, Type from ahriman.core.configuration import Configuration -from ahriman.models.user import User from ahriman.models.user_access import UserAccess @@ -29,13 +30,12 @@ class Auth: helper to deal with user authorization :ivar allowed_paths: URI paths which can be accessed without authorization :ivar allowed_paths_groups: URI paths prefixes which can be accessed without authorization - :ivar salt: random generated string to salt passwords - :ivar users: map of username to its descriptor + :ivar enabled: indicates if authorization is enabled :cvar ALLOWED_PATHS: URI paths which can be accessed without authorization, predefined :cvar ALLOWED_PATHS_GROUPS: URI paths prefixes which can be accessed without authorization, predefined """ - ALLOWED_PATHS = {"/favicon.ico", "/login", "/logout"} + ALLOWED_PATHS = {"/", "/favicon.ico", "/index.html", "/login", "/logout"} ALLOWED_PATHS_GROUPS: Set[str] = set() def __init__(self, configuration: Configuration) -> None: @@ -43,40 +43,33 @@ class Auth: default constructor :param configuration: configuration instance """ - self.salt = configuration.get("auth", "salt") - self.users = self.get_users(configuration) - self.allowed_paths = set(configuration.getlist("auth", "allowed_paths")) self.allowed_paths.update(self.ALLOWED_PATHS) self.allowed_paths_groups = set(configuration.getlist("auth", "allowed_paths_groups")) self.allowed_paths_groups.update(self.ALLOWED_PATHS_GROUPS) + self.enabled = configuration.getboolean("web", "auth", fallback=False) - @staticmethod - def get_users(configuration: Configuration) -> Dict[str, User]: + @classmethod + def load(cls: Type[Auth], configuration: Configuration) -> Auth: """ - load users from settings + load authorization module from settings :param configuration: configuration instance - :return: map of username to its descriptor + :return: authorization module according to current settings """ - users: Dict[str, User] = {} - for role in UserAccess: - section = configuration.section_name("auth", role.value) - if not configuration.has_section(section): - continue - for user, password in configuration[section].items(): - users[user] = User(user, password, role) - return users + if configuration.getboolean("web", "auth", fallback=False): + from ahriman.core.auth.mapping_auth import MappingAuth + return MappingAuth(configuration) + return cls(configuration) - def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: + def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use """ validate user password :param username: username :param password: entered password :return: True in case if password matches, False otherwise """ - if username is None or password is None: - return False # invalid data supplied - return username in self.users and self.users[username].check_credentials(password, self.salt) + del username, password + return True def is_safe_request(self, uri: Optional[str]) -> bool: """ @@ -88,11 +81,21 @@ class Auth: return False # request without context is not allowed return uri in self.allowed_paths or any(uri.startswith(path) for path in self.allowed_paths_groups) - def verify_access(self, username: str, required: UserAccess) -> bool: + def known_username(self, username: str) -> bool: # pylint: disable=no-self-use + """ + check if user is known + :param username: username + :return: True in case if user is known and can be authorized and False otherwise + """ + del username + return True + + def verify_access(self, username: str, required: UserAccess) -> bool: # pylint: disable=no-self-use """ validate if user has access to requested resource :param username: username :param required: required access level :return: True in case if user is allowed to do this request and False otherwise """ - return username in self.users and self.users[username].verify_access(required) + del username, required + return True diff --git a/src/ahriman/core/auth/helpers.py b/src/ahriman/core/auth/helpers.py new file mode 100644 index 00000000..f94ddc98 --- /dev/null +++ b/src/ahriman/core/auth/helpers.py @@ -0,0 +1,70 @@ +# +# Copyright (c) 2021 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 typing import Any + +try: + import aiohttp_security # type: ignore + _has_aiohttp_security = True +except ImportError: + _has_aiohttp_security = False + + +async def authorized_userid(*args: Any) -> Any: + """ + handle aiohttp security methods + :param args: argument list as provided by authorized_userid function + :return: None in case if no aiohttp_security module found and function call otherwise + """ + if _has_aiohttp_security: + return await aiohttp_security.authorized_userid(*args) # pylint: disable=no-value-for-parameter + return None + + +async def check_authorized(*args: Any) -> Any: + """ + handle aiohttp security methods + :param args: argument list as provided by check_authorized function + :return: None in case if no aiohttp_security module found and function call otherwise + """ + if _has_aiohttp_security: + return await aiohttp_security.check_authorized(*args) # pylint: disable=no-value-for-parameter + return None + + +async def forget(*args: Any) -> Any: + """ + handle aiohttp security methods + :param args: argument list as provided by forget function + :return: None in case if no aiohttp_security module found and function call otherwise + """ + if _has_aiohttp_security: + return await aiohttp_security.forget(*args) # pylint: disable=no-value-for-parameter + return None + + +async def remember(*args: Any) -> Any: + """ + handle disabled auth + :param args: argument list as provided by remember function + :return: None in case if no aiohttp_security module found and function call otherwise + """ + if _has_aiohttp_security: + return await aiohttp_security.remember(*args) # pylint: disable=no-value-for-parameter + return None diff --git a/src/ahriman/core/auth/mapping_auth.py b/src/ahriman/core/auth/mapping_auth.py new file mode 100644 index 00000000..8e14ad14 --- /dev/null +++ b/src/ahriman/core/auth/mapping_auth.py @@ -0,0 +1,86 @@ +# +# Copyright (c) 2021 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 typing import Dict, Optional + +from ahriman.core.auth.auth import Auth +from ahriman.core.configuration import Configuration +from ahriman.models.user import User +from ahriman.models.user_access import UserAccess + + +class MappingAuth(Auth): + """ + user authorization based on mapping from configuration file + :ivar salt: random generated string to salt passwords + :ivar users: map of username to its descriptor + """ + + def __init__(self, configuration: Configuration) -> None: + """ + default constructor + :param configuration: configuration instance + """ + Auth.__init__(self, configuration) + self.salt = configuration.get("auth", "salt") + self.users = self.get_users(configuration) + + @staticmethod + def get_users(configuration: Configuration) -> Dict[str, User]: + """ + load users from settings + :param configuration: configuration instance + :return: map of username to its descriptor + """ + users: Dict[str, User] = {} + for role in UserAccess: + section = configuration.section_name("auth", role.value) + if not configuration.has_section(section): + continue + for user, password in configuration[section].items(): + users[user] = User(user, password, role) + return users + + def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: + """ + validate user password + :param username: username + :param password: entered password + :return: True in case if password matches, False otherwise + """ + if username is None or password is None: + return False # invalid data supplied + return self.known_username(username) and self.users[username].check_credentials(password, self.salt) + + def known_username(self, username: str) -> bool: + """ + check if user is known + :param username: username + :return: True in case if user is known and can be authorized and False otherwise + """ + return username in self.users + + def verify_access(self, username: str, required: UserAccess) -> bool: + """ + validate if user has access to requested resource + :param username: username + :param required: required access level + :return: True in case if user is allowed to do this request and False otherwise + """ + return self.known_username(username) and self.users[username].verify_access(required) diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py index 6f84a011..8635850c 100644 --- a/src/ahriman/web/middlewares/auth_handler.py +++ b/src/ahriman/web/middlewares/auth_handler.py @@ -28,8 +28,7 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage # type: ignor from cryptography import fernet from typing import Optional -from ahriman.core.auth import Auth -from ahriman.core.configuration import Configuration +from ahriman.core.auth.auth import Auth from ahriman.models.user_access import UserAccess from ahriman.web.middlewares import HandlerType, MiddlewareType @@ -40,12 +39,12 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type :ivar validator: validator instance """ - def __init__(self, configuration: Configuration) -> None: + def __init__(self, validator: Auth) -> None: """ default constructor - :param configuration: configuration instance + :param validator: authorization module instance """ - self.validator = Auth(configuration) + self.validator = validator async def authorized_userid(self, identity: str) -> Optional[str]: """ @@ -53,7 +52,7 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type :param identity: username :return: user identity (username) in case if user exists and None otherwise """ - return identity if identity in self.validator.users else None + return identity if self.validator.known_username(identity) else None async def permits(self, identity: str, permission: UserAccess, context: Optional[str] = None) -> bool: """ @@ -63,37 +62,37 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type :param context: URI request path :return: True in case if user is allowed to perform this request and False otherwise """ - if self.validator.is_safe_request(context): - return True return self.validator.verify_access(identity, permission) -def auth_handler() -> MiddlewareType: +def auth_handler(validator: Auth) -> MiddlewareType: """ authorization and authentication middleware + :param validator: authorization module instance :return: built middleware """ @middleware async def handle(request: Request, handler: HandlerType) -> StreamResponse: - print(request) if request.path.startswith("/api"): permission = UserAccess.Status elif request.method in ("GET", "HEAD", "OPTIONS"): permission = UserAccess.Read else: permission = UserAccess.Write - await aiohttp_security.check_permission(request, permission, request.path) + + if not validator.is_safe_request(request.path): + await aiohttp_security.check_permission(request, permission, request.path) return await handler(request) return handle -def setup_auth(application: web.Application, configuration: Configuration) -> web.Application: +def setup_auth(application: web.Application, validator: Auth) -> web.Application: """ setup authorization policies for the application :param application: web application instance - :param configuration: configuration instance + :param validator: authorization module instance :return: configured web application """ fernet_key = fernet.Fernet.generate_key() @@ -101,11 +100,10 @@ def setup_auth(application: web.Application, configuration: Configuration) -> we storage = EncryptedCookieStorage(secret_key, cookie_name='API_SESSION') setup_session(application, storage) - authorization_policy = AuthorizationPolicy(configuration) + authorization_policy = AuthorizationPolicy(validator) identity_policy = aiohttp_security.SessionIdentityPolicy() - application["validator"] = authorization_policy.validator aiohttp_security.setup(application, identity_policy, authorization_policy) - application.middlewares.append(auth_handler()) + application.middlewares.append(auth_handler(validator)) return application diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py index bb078570..9b10ba68 100644 --- a/src/ahriman/web/views/base.py +++ b/src/ahriman/web/views/base.py @@ -20,7 +20,7 @@ from aiohttp.web import View from typing import Any, Dict -from ahriman.core.auth import Auth +from ahriman.core.auth.auth import Auth from ahriman.core.status.watcher import Watcher diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index 57b96811..51a30364 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -22,6 +22,7 @@ import aiohttp_jinja2 from typing import Any, Dict from ahriman import version +from ahriman.core.auth.helpers import authorized_userid from ahriman.core.util import pretty_datetime from ahriman.web.views.base import BaseView @@ -33,6 +34,8 @@ class IndexView(BaseView): It uses jinja2 templates for report generation, the following variables are allowed: architecture - repository architecture, string, required + auth_enabled - whether authorization is enabled by configuration or not, boolean, required + auth_username - authorized user id if any, string. None means not authorized packages - sorted list of packages properties, required * base, string * depends, sorted list of strings @@ -79,6 +82,8 @@ class IndexView(BaseView): return { "architecture": self.service.architecture, + "auth_enabled": self.validator.enabled, + "auth_username": await authorized_userid(self.request), "packages": packages, "repository": self.service.repository.name, "service": service, diff --git a/src/ahriman/web/views/login.py b/src/ahriman/web/views/login.py index f4f3867c..8155e9d5 100644 --- a/src/ahriman/web/views/login.py +++ b/src/ahriman/web/views/login.py @@ -17,10 +17,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_security # type: ignore - from aiohttp.web import HTTPFound, HTTPUnauthorized, Response +from ahriman.core.auth.helpers import remember from ahriman.web.views.base import BaseView @@ -45,11 +44,8 @@ class LoginView(BaseView): username = data.get("username") response = HTTPFound("/") - try: - if self.validator.check_credentials(username, data.get("password")): - await aiohttp_security.remember(self.request, response, username) - return response - except KeyError: + if self.validator.check_credentials(username, data.get("password")): + await remember(self.request, response, username) return response raise HTTPUnauthorized() diff --git a/src/ahriman/web/views/logout.py b/src/ahriman/web/views/logout.py index 4336f603..5e0ecde0 100644 --- a/src/ahriman/web/views/logout.py +++ b/src/ahriman/web/views/logout.py @@ -17,10 +17,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_security # type: ignore - from aiohttp.web import HTTPFound, Response +from ahriman.core.auth.helpers import check_authorized, forget from ahriman.web.views.base import BaseView @@ -34,9 +33,9 @@ class LogoutView(BaseView): logout user from the service. No parameters supported here :return: redirect to main page """ - await aiohttp_security.check_authorized(self.request) + await check_authorized(self.request) response = HTTPFound("/") - await aiohttp_security.forget(self.request, response) + await forget(self.request, response) return response diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 5e539dc1..1cf98d3e 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -23,6 +23,7 @@ import logging from aiohttp import web +from ahriman.core.auth.auth import Auth from ahriman.core.configuration import Configuration from ahriman.core.exceptions import InitializeException from ahriman.core.status.watcher import Watcher @@ -92,8 +93,10 @@ def setup_service(architecture: str, configuration: Configuration) -> web.Applic application.logger.info("setup watcher") application["watcher"] = Watcher(architecture, configuration) - if configuration.getboolean("web", "auth", fallback=False): + application.logger.info("setup authorization") + validator = application["validator"] = Auth.load(configuration) + if validator.enabled: from ahriman.web.middlewares.auth_handler import setup_auth - setup_auth(application, configuration) + setup_auth(application, validator) return application diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 9dbcd4d6..99a600fe 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -1,11 +1,10 @@ -from unittest.mock import MagicMock - import pytest from pathlib import Path from pytest_mock import MockerFixture from typing import Any, Type, TypeVar +from ahriman.core.auth.auth import Auth from ahriman.core.configuration import Configuration from ahriman.core.status.watcher import Watcher from ahriman.models.package import Package @@ -44,6 +43,15 @@ def anyvar(cls: Type[T], strict: bool = False) -> T: # generic fixtures +@pytest.fixture +def auth(configuration: Configuration) -> Auth: + """ + auth provider fixture + :return: auth service instance + """ + return Auth(configuration) + + @pytest.fixture def configuration(resource_path_root: Path) -> Configuration: """ diff --git a/tests/ahriman/core/auth/conftest.py b/tests/ahriman/core/auth/conftest.py new file mode 100644 index 00000000..9eabe07a --- /dev/null +++ b/tests/ahriman/core/auth/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from ahriman.core.auth.mapping_auth import MappingAuth +from ahriman.core.configuration import Configuration + + +@pytest.fixture +def mapping_auth(configuration: Configuration) -> MappingAuth: + """ + auth provider fixture + :return: auth service instance + """ + configuration.set("web", "auth", "yes") + return MappingAuth(configuration) diff --git a/tests/ahriman/core/auth/test_auth.py b/tests/ahriman/core/auth/test_auth.py new file mode 100644 index 00000000..3905106f --- /dev/null +++ b/tests/ahriman/core/auth/test_auth.py @@ -0,0 +1,81 @@ +from ahriman.core.auth.auth import Auth +from ahriman.core.auth.mapping_auth import MappingAuth +from ahriman.core.configuration import Configuration +from ahriman.models.user import User +from ahriman.models.user_access import UserAccess + + +def test_load_dummy(configuration: Configuration) -> None: + """ + must load dummy validator if authorization is not enabled + """ + configuration.set("web", "auth", "no") + auth = Auth.load(configuration) + assert isinstance(auth, Auth) + + +def test_load_dummy_empty(configuration: Configuration) -> None: + """ + must load dummy validator if no option set + """ + auth = Auth.load(configuration) + assert isinstance(auth, Auth) + + +def test_load_mapping(configuration: Configuration) -> None: + """ + must load mapping validator if option set + """ + configuration.set("web", "auth", "yes") + auth = Auth.load(configuration) + assert isinstance(auth, MappingAuth) + + +def test_check_credentials(auth: Auth, user: User) -> None: + """ + must pass any credentials + """ + assert auth.check_credentials(user.username, user.password) + assert auth.check_credentials(None, "") + assert auth.check_credentials("", None) + assert auth.check_credentials(None, None) + + +def test_is_safe_request(auth: Auth) -> None: + """ + must validate safe request + """ + # login and logout are always safe + assert auth.is_safe_request("/login") + assert auth.is_safe_request("/logout") + + auth.allowed_paths.add("/safe") + auth.allowed_paths_groups.add("/unsafe/safe") + + assert auth.is_safe_request("/safe") + assert not auth.is_safe_request("/unsafe") + assert auth.is_safe_request("/unsafe/safe") + assert auth.is_safe_request("/unsafe/safe/suffix") + + +def test_is_safe_request_empty(auth: Auth) -> None: + """ + must not allow requests without path + """ + assert not auth.is_safe_request(None) + assert not auth.is_safe_request("") + + +def test_known_username(auth: Auth, user: User) -> None: + """ + must allow any username + """ + assert auth.known_username(user.username) + + +def test_verify_access(auth: Auth, user: User) -> None: + """ + must allow any access + """ + assert auth.verify_access(user.username, user.access) + assert auth.verify_access(user.username, UserAccess.Write) diff --git a/tests/ahriman/core/auth/test_helpers.py b/tests/ahriman/core/auth/test_helpers.py new file mode 100644 index 00000000..042f1e57 --- /dev/null +++ b/tests/ahriman/core/auth/test_helpers.py @@ -0,0 +1,102 @@ +import importlib +import sys + +import ahriman.core.auth.helpers as helpers + +from pytest_mock import MockerFixture + + +def test_import_aiohttp_security() -> None: + """ + must import aiohttp_security correctly + """ + assert helpers._has_aiohttp_security + + +def test_import_aiohttp_security_missing(mocker: MockerFixture) -> None: + """ + must set missing flag if no aiohttp_security module found + """ + mocker.patch.dict(sys.modules, {"aiohttp_security": None}) + importlib.reload(helpers) + assert not helpers._has_aiohttp_security + + +async def test_authorized_userid_dummy(mocker: MockerFixture) -> None: + """ + must not call authorized_userid from library if not enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", False) + authorized_userid_mock = mocker.patch("aiohttp_security.authorized_userid") + await helpers.authorized_userid() + authorized_userid_mock.assert_not_called() + + +async def test_authorized_userid_library(mocker: MockerFixture) -> None: + """ + must call authorized_userid from library if enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", True) + authorized_userid_mock = mocker.patch("aiohttp_security.authorized_userid") + await helpers.authorized_userid() + authorized_userid_mock.assert_called_once() + + +async def test_check_authorized_dummy(mocker: MockerFixture) -> None: + """ + must not call check_authorized from library if not enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", False) + check_authorized_mock = mocker.patch("aiohttp_security.check_authorized") + await helpers.check_authorized() + check_authorized_mock.assert_not_called() + + +async def test_check_authorized_library(mocker: MockerFixture) -> None: + """ + must call check_authorized from library if enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", True) + check_authorized_mock = mocker.patch("aiohttp_security.check_authorized") + await helpers.check_authorized() + check_authorized_mock.assert_called_once() + + +async def test_forget_dummy(mocker: MockerFixture) -> None: + """ + must not call forget from library if not enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", False) + forget_mock = mocker.patch("aiohttp_security.forget") + await helpers.forget() + forget_mock.assert_not_called() + + +async def test_forget_library(mocker: MockerFixture) -> None: + """ + must call forget from library if enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", True) + forget_mock = mocker.patch("aiohttp_security.forget") + await helpers.forget() + forget_mock.assert_called_once() + + +async def test_remember_dummy(mocker: MockerFixture) -> None: + """ + must not call remember from library if not enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", False) + remember_mock = mocker.patch("aiohttp_security.remember") + await helpers.remember() + remember_mock.assert_not_called() + + +async def test_remember_library(mocker: MockerFixture) -> None: + """ + must call remember from library if enabled + """ + mocker.patch.object(helpers, "_has_aiohttp_security", True) + remember_mock = mocker.patch("aiohttp_security.remember") + await helpers.remember() + remember_mock.assert_called_once() diff --git a/tests/ahriman/core/auth/test_mapping_auth.py b/tests/ahriman/core/auth/test_mapping_auth.py new file mode 100644 index 00000000..0f306c17 --- /dev/null +++ b/tests/ahriman/core/auth/test_mapping_auth.py @@ -0,0 +1,67 @@ +from ahriman.core.auth.mapping_auth import MappingAuth +from ahriman.core.configuration import Configuration +from ahriman.models.user import User +from ahriman.models.user_access import UserAccess + + +def test_get_users(mapping_auth: MappingAuth, configuration: Configuration) -> None: + """ + must return valid user list + """ + user_write = User("user_write", "pwd_write", UserAccess.Write) + write_section = Configuration.section_name("auth", user_write.access.value) + configuration.add_section(write_section) + configuration.set(write_section, user_write.username, user_write.password) + user_read = User("user_read", "pwd_read", UserAccess.Read) + read_section = Configuration.section_name("auth", user_read.access.value) + configuration.add_section(read_section) + configuration.set(read_section, user_read.username, user_read.password) + + users = mapping_auth.get_users(configuration) + expected = {user_write.username: user_write, user_read.username: user_read} + assert users == expected + + +def test_check_credentials(mapping_auth: MappingAuth, user: User) -> None: + """ + must return true for valid credentials + """ + current_password = user.password + user.password = user.generate_password(user.password, mapping_auth.salt) + mapping_auth.users[user.username] = user + assert mapping_auth.check_credentials(user.username, current_password) + assert not mapping_auth.check_credentials(user.username, user.password) # here password is hashed so it is invalid + + +def test_check_credentials_empty(mapping_auth: MappingAuth) -> None: + """ + must reject on empty credentials + """ + assert not mapping_auth.check_credentials(None, "") + assert not mapping_auth.check_credentials("", None) + assert not mapping_auth.check_credentials(None, None) + + +def test_check_credentials_unknown(mapping_auth: MappingAuth, user: User) -> None: + """ + must reject on unknown user + """ + assert not mapping_auth.check_credentials(user.username, user.password) + + +def test_known_username(mapping_auth: MappingAuth, user: User) -> None: + """ + must allow only known users + """ + mapping_auth.users[user.username] = user + assert mapping_auth.known_username(user.username) + assert not mapping_auth.known_username(user.password) + + +def test_verify_access(mapping_auth: MappingAuth, user: User) -> None: + """ + must verify user access + """ + mapping_auth.users[user.username] = user + assert mapping_auth.verify_access(user.username, user.access) + assert not mapping_auth.verify_access(user.username, UserAccess.Write) diff --git a/tests/ahriman/core/conftest.py b/tests/ahriman/core/conftest.py index f800f5d0..69447e16 100644 --- a/tests/ahriman/core/conftest.py +++ b/tests/ahriman/core/conftest.py @@ -2,7 +2,7 @@ import pytest from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.repo import Repo -from ahriman.core.auth import Auth +from ahriman.core.auth.auth import Auth from ahriman.core.build_tools.task import Task from ahriman.core.configuration import Configuration from ahriman.core.tree import Leaf @@ -30,15 +30,6 @@ def leaf_python_schedule(package_python_schedule: Package) -> Leaf: return Leaf(package_python_schedule, set()) -@pytest.fixture -def auth(configuration: Configuration) -> Auth: - """ - auth provider fixture - :return: auth service instance - """ - return Auth(configuration) - - @pytest.fixture def pacman(configuration: Configuration) -> Pacman: """ diff --git a/tests/ahriman/core/test_auth.py b/tests/ahriman/core/test_auth.py deleted file mode 100644 index 7dd05fb5..00000000 --- a/tests/ahriman/core/test_auth.py +++ /dev/null @@ -1,83 +0,0 @@ -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 - - -def test_get_users(auth: Auth, configuration: Configuration) -> None: - """ - must return valid user list - """ - user_write = User("user_write", "pwd_write", UserAccess.Write) - write_section = Configuration.section_name("auth", user_write.access.value) - configuration.add_section(write_section) - configuration.set(write_section, user_write.username, user_write.password) - user_read = User("user_read", "pwd_read", UserAccess.Read) - read_section = Configuration.section_name("auth", user_read.access.value) - configuration.add_section(read_section) - configuration.set(read_section, user_read.username, user_read.password) - - users = auth.get_users(configuration) - expected = {user_write.username: user_write, user_read.username: user_read} - assert users == expected - - -def test_check_credentials(auth: Auth, user: User) -> None: - """ - must return true for valid credentials - """ - current_password = user.password - user.password = user.generate_password(user.password, auth.salt) - auth.users[user.username] = user - assert auth.check_credentials(user.username, current_password) - assert not auth.check_credentials(user.username, user.password) # here password is hashed so it is invalid - - -def test_check_credentials_empty(auth: Auth) -> None: - """ - must reject on empty credentials - """ - assert not auth.check_credentials(None, "") - assert not auth.check_credentials("", None) - assert not auth.check_credentials(None, None) - - -def test_check_credentials_unknown(auth: Auth, user: User) -> None: - """ - must reject on unknown user - """ - assert not auth.check_credentials(user.username, user.password) - - -def test_is_safe_request(auth: Auth) -> None: - """ - must validate safe request - """ - # login and logout are always safe - assert auth.is_safe_request("/login") - assert auth.is_safe_request("/logout") - - auth.allowed_paths.add("/safe") - auth.allowed_paths_groups.add("/unsafe/safe") - - assert auth.is_safe_request("/safe") - assert not auth.is_safe_request("/unsafe") - assert auth.is_safe_request("/unsafe/safe") - assert auth.is_safe_request("/unsafe/safe/suffix") - - -def test_is_safe_request_empty(auth: Auth) -> None: - """ - must not allow requests without path - """ - assert not auth.is_safe_request(None) - assert not auth.is_safe_request("") - - -def test_verify_access(auth: Auth, user: User) -> None: - """ - must verify user access - """ - auth.users[user.username] = user - assert auth.verify_access(user.username, user.access) - assert not auth.verify_access(user.username, UserAccess.Write) diff --git a/tests/ahriman/core/upload/test_s3.py b/tests/ahriman/core/upload/test_s3.py index 4eae1d44..a8d8d771 100644 --- a/tests/ahriman/core/upload/test_s3.py +++ b/tests/ahriman/core/upload/test_s3.py @@ -61,6 +61,8 @@ def test_get_local_files(s3: S3, resource_path_root: Path) -> None: Path("models/package_yay_srcinfo"), Path("web/templates/search-line.jinja2"), Path("web/templates/build-status.jinja2"), + Path("web/templates/login-form.jinja2"), + Path("web/templates/login-form-hide.jinja2"), Path("web/templates/repo-index.jinja2"), Path("web/templates/sorttable.jinja2"), Path("web/templates/style.jinja2"), diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index be27cf4f..67423c01 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -3,7 +3,10 @@ import pytest from aiohttp import web from pytest_mock import MockerFixture +import ahriman.core.auth.helpers + from ahriman.core.configuration import Configuration +from ahriman.models.user import User from ahriman.web.web import setup_service @@ -15,18 +18,26 @@ def application(configuration: Configuration, mocker: MockerFixture) -> web.Appl :param mocker: mocker object :return: application test instance """ + mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False) mocker.patch("pathlib.Path.mkdir") return setup_service("x86_64", configuration) @pytest.fixture -def application_with_auth(configuration: Configuration, mocker: MockerFixture) -> web.Application: +def application_with_auth(configuration: Configuration, user: User, mocker: MockerFixture) -> web.Application: """ application fixture with auth enabled :param configuration: configuration fixture + :param user: user descriptor fixture :param mocker: mocker object :return: application test instance """ configuration.set("web", "auth", "yes") + mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True) mocker.patch("pathlib.Path.mkdir") - return setup_service("x86_64", configuration) + application = setup_service("x86_64", configuration) + + generated = User(user.username, user.generate_password(user.password, application["validator"].salt), user.access) + application["validator"].users[generated.username] = generated + + return application diff --git a/tests/ahriman/web/middlewares/conftest.py b/tests/ahriman/web/middlewares/conftest.py index 6a9733c2..a168d3b9 100644 --- a/tests/ahriman/web/middlewares/conftest.py +++ b/tests/ahriman/web/middlewares/conftest.py @@ -2,6 +2,7 @@ import pytest from collections import namedtuple +from ahriman.core.auth.auth import Auth from ahriman.core.configuration import Configuration from ahriman.models.user import User from ahriman.web.middlewares.auth_handler import AuthorizationPolicy @@ -24,6 +25,8 @@ def authorization_policy(configuration: Configuration, user: User) -> Authorizat fixture for authorization policy :return: authorization policy fixture """ - policy = AuthorizationPolicy(configuration) + configuration.set("web", "auth", "yes") + validator = Auth.load(configuration) + policy = AuthorizationPolicy(validator) policy.validator.users = {user.username: user} return policy diff --git a/tests/ahriman/web/middlewares/test_auth_handler.py b/tests/ahriman/web/middlewares/test_auth_handler.py index dc69899a..ea381c29 100644 --- a/tests/ahriman/web/middlewares/test_auth_handler.py +++ b/tests/ahriman/web/middlewares/test_auth_handler.py @@ -1,8 +1,9 @@ from aiohttp import web from pytest_mock import MockerFixture from typing import Any -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock +from ahriman.core.auth.auth import Auth from ahriman.core.configuration import Configuration from ahriman.models.user import User from ahriman.models.user_access import UserAccess @@ -17,89 +18,83 @@ async def test_authorized_userid(authorization_policy: AuthorizationPolicy, user assert await authorization_policy.authorized_userid("some random name") is None -async def test_permits(authorization_policy: AuthorizationPolicy, user: User, mocker: MockerFixture) -> None: +async def test_permits(authorization_policy: AuthorizationPolicy, user: User) -> None: """ must call validator check """ - safe_request_mock = mocker.patch("ahriman.core.auth.Auth.is_safe_request", return_value=False) - verify_access_mock = mocker.patch("ahriman.core.auth.Auth.verify_access", return_value=True) + authorization_policy.validator = MagicMock() + authorization_policy.validator.verify_access.return_value = True assert await authorization_policy.permits(user.username, user.access, "/endpoint") - safe_request_mock.assert_called_with("/endpoint") - verify_access_mock.assert_called_with(user.username, user.access) + authorization_policy.validator.verify_access.assert_called_with(user.username, user.access) -async def test_permits_safe(authorization_policy: AuthorizationPolicy, user: User, mocker: MockerFixture) -> None: - """ - must call validator check - """ - safe_request_mock = mocker.patch("ahriman.core.auth.Auth.is_safe_request", return_value=True) - verify_access_mock = mocker.patch("ahriman.core.auth.Auth.verify_access") - - assert await authorization_policy.permits(user.username, user.access, "/endpoint") - safe_request_mock.assert_called_with("/endpoint") - verify_access_mock.assert_not_called() - - -async def test_auth_handler_api(aiohttp_request: Any, mocker: MockerFixture) -> None: +async def test_auth_handler_api(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None: """ must ask for status permission for api calls """ aiohttp_request = aiohttp_request._replace(path="/api") request_handler = AsyncMock() + mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False) check_permission_mock = mocker.patch("aiohttp_security.check_permission") - handler = auth_handler() + handler = auth_handler(auth) await handler(aiohttp_request, request_handler) check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path) -async def test_auth_handler_api_post(aiohttp_request: Any, mocker: MockerFixture) -> None: +async def test_auth_handler_api_post(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None: """ must ask for status permission for api calls with POST """ aiohttp_request = aiohttp_request._replace(path="/api", method="POST") request_handler = AsyncMock() + mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False) check_permission_mock = mocker.patch("aiohttp_security.check_permission") - handler = auth_handler() + handler = auth_handler(auth) await handler(aiohttp_request, request_handler) check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path) -async def test_auth_handler_read(aiohttp_request: Any, mocker: MockerFixture) -> None: +async def test_auth_handler_read(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None: """ must ask for read permission for api calls with GET """ for method in ("GET", "HEAD", "OPTIONS"): aiohttp_request = aiohttp_request._replace(method=method) request_handler = AsyncMock() + mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False) check_permission_mock = mocker.patch("aiohttp_security.check_permission") - handler = auth_handler() + handler = auth_handler(auth) await handler(aiohttp_request, request_handler) check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path) -async def test_auth_handler_write(aiohttp_request: Any, mocker: MockerFixture) -> None: +async def test_auth_handler_write(aiohttp_request: Any, auth: Auth, mocker: MockerFixture) -> None: """ must ask for read permission for api calls with POST """ for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"): aiohttp_request = aiohttp_request._replace(method=method) request_handler = AsyncMock() + mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False) check_permission_mock = mocker.patch("aiohttp_security.check_permission") - handler = auth_handler() + handler = auth_handler(auth) await handler(aiohttp_request, request_handler) check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path) -def test_setup_auth(application: web.Application, configuration: Configuration, mocker: MockerFixture) -> None: +def test_setup_auth( + application_with_auth: web.Application, + configuration: Configuration, + mocker: MockerFixture) -> None: """ must setup authorization """ aiohttp_security_setup_mock = mocker.patch("aiohttp_security.setup") - application = setup_auth(application, configuration) + application = setup_auth(application_with_auth, configuration) assert application.get("validator") is not None aiohttp_security_setup_mock.assert_called_once() diff --git a/tests/ahriman/web/views/conftest.py b/tests/ahriman/web/views/conftest.py index 29ec18d0..8ae6bcc9 100644 --- a/tests/ahriman/web/views/conftest.py +++ b/tests/ahriman/web/views/conftest.py @@ -20,3 +20,18 @@ def client(application: web.Application, loop: BaseEventLoop, """ mocker.patch("pathlib.Path.iterdir", return_value=[]) return loop.run_until_complete(aiohttp_client(application)) + + +@pytest.fixture +def client_with_auth(application_with_auth: web.Application, loop: BaseEventLoop, + aiohttp_client: Any, mocker: MockerFixture) -> TestClient: + """ + web client fixture with full authorization functions + :param application_with_auth: application fixture + :param loop: context event loop + :param aiohttp_client: aiohttp client fixture + :param mocker: mocker object + :return: web client test instance + """ + mocker.patch("pathlib.Path.iterdir", return_value=[]) + return loop.run_until_complete(aiohttp_client(application_with_auth)) diff --git a/tests/ahriman/web/views/test_view_index.py b/tests/ahriman/web/views/test_view_index.py index 1d2099fa..cda12b7b 100644 --- a/tests/ahriman/web/views/test_view_index.py +++ b/tests/ahriman/web/views/test_view_index.py @@ -1,19 +1,28 @@ from pytest_aiohttp import TestClient -async def test_get(client: TestClient) -> None: +async def test_get(client_with_auth: TestClient) -> None: """ must generate status page correctly (/) """ + response = await client_with_auth.get("/") + assert response.status == 200 + assert await response.text() + + +async def test_get_index(client_with_auth: TestClient) -> None: + """ + must generate status page correctly (/index.html) + """ + response = await client_with_auth.get("/index.html") + assert response.status == 200 + assert await response.text() + + +async def test_get_without_auth(client: TestClient) -> None: + """ + must use dummy authorized_userid function in case if no security library installed + """ response = await client.get("/") assert response.status == 200 assert await response.text() - - -async def test_get_index(client: TestClient) -> None: - """ - must generate status page correctly (/index.html) - """ - response = await client.get("/index.html") - assert response.status == 200 - assert await response.text() diff --git a/tests/ahriman/web/views/test_view_login.py b/tests/ahriman/web/views/test_view_login.py index 478c308e..289b1fc2 100644 --- a/tests/ahriman/web/views/test_view_login.py +++ b/tests/ahriman/web/views/test_view_login.py @@ -1,48 +1,41 @@ from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture -from ahriman.core.auth import Auth -from ahriman.core.configuration import Configuration from ahriman.models.user import User -async def test_post(client: TestClient, configuration: Configuration, user: User, mocker: MockerFixture) -> None: +async def test_post(client_with_auth: TestClient, user: User, mocker: MockerFixture) -> None: """ must login user correctly """ - client.app["validator"] = Auth(configuration) payload = {"username": user.username, "password": user.password} - remember_patch = mocker.patch("aiohttp_security.remember") - mocker.patch("ahriman.core.auth.Auth.check_credentials", return_value=True) + remember_mock = mocker.patch("aiohttp_security.remember") - post_response = await client.post("/login", json=payload) + post_response = await client_with_auth.post("/login", json=payload) assert post_response.status == 200 - post_response = await client.post("/login", data=payload) + post_response = await client_with_auth.post("/login", data=payload) assert post_response.status == 200 - remember_patch.assert_called() + remember_mock.assert_called() -async def test_post_skip(client: TestClient, user: User, mocker: MockerFixture) -> None: +async def test_post_skip(client: TestClient, user: User) -> None: """ must process if no auth configured """ payload = {"username": user.username, "password": user.password} post_response = await client.post("/login", json=payload) - remember_patch = mocker.patch("aiohttp_security.remember") assert post_response.status == 200 - remember_patch.assert_not_called() -async def test_post_unauthorized(client: TestClient, configuration: Configuration, user: User, - mocker: MockerFixture) -> None: +async def test_post_unauthorized(client_with_auth: TestClient, user: User, mocker: MockerFixture) -> None: """ must return unauthorized on invalid auth """ - client.app["validator"] = Auth(configuration) - payload = {"username": user.username, "password": user.password} - mocker.patch("ahriman.core.auth.Auth.check_credentials", return_value=False) + payload = {"username": user.username, "password": ""} + remember_mock = mocker.patch("aiohttp_security.remember") - post_response = await client.post("/login", json=payload) + post_response = await client_with_auth.post("/login", json=payload) assert post_response.status == 401 + remember_mock.assert_not_called() diff --git a/tests/ahriman/web/views/test_view_logout.py b/tests/ahriman/web/views/test_view_logout.py index 82e7c4be..d9135c4a 100644 --- a/tests/ahriman/web/views/test_view_logout.py +++ b/tests/ahriman/web/views/test_view_logout.py @@ -3,36 +3,33 @@ from aiohttp.web import HTTPUnauthorized from pytest_mock import MockerFixture -async def test_post(client: TestClient, mocker: MockerFixture) -> None: +async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None: """ must logout user correctly """ mocker.patch("aiohttp_security.check_authorized") - forget_patch = mocker.patch("aiohttp_security.forget") + forget_mock = mocker.patch("aiohttp_security.forget") - post_response = await client.post("/logout") + post_response = await client_with_auth.post("/logout") assert post_response.status == 200 - forget_patch.assert_called_once() + forget_mock.assert_called_once() -async def test_post_unauthorized(client: TestClient, mocker: MockerFixture) -> None: +async def test_post_unauthorized(client_with_auth: TestClient, mocker: MockerFixture) -> None: """ must raise exception if unauthorized """ mocker.patch("aiohttp_security.check_authorized", side_effect=HTTPUnauthorized()) - forget_patch = mocker.patch("aiohttp_security.forget") + forget_mock = mocker.patch("aiohttp_security.forget") - post_response = await client.post("/logout") + post_response = await client_with_auth.post("/logout") assert post_response.status == 401 - forget_patch.assert_not_called() + forget_mock.assert_not_called() -async def test_post_disabled(client: TestClient, mocker: MockerFixture) -> None: +async def test_post_disabled(client: TestClient) -> None: """ must raise exception if auth is disabled """ - forget_patch = mocker.patch("aiohttp_security.forget") - post_response = await client.post("/logout") - assert post_response.status == 401 - forget_patch.assert_not_called() + assert post_response.status == 200 diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index e0cab29f..d28582ea 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -57,6 +57,5 @@ region = eu-central-1 secret_key = [web] -auth = no host = 127.0.0.1 templates = ../web/templates \ No newline at end of file