+ {% 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