add login annd logout to index also improve auth

This commit is contained in:
Evgenii Alekseev 2021-09-01 02:49:02 +03:00
parent 40ef103e2f
commit b755088024
30 changed files with 713 additions and 230 deletions

View File

@ -17,6 +17,8 @@
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}"> <img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
</h1> </h1>
{% include "login-form.jinja2" %}
{% include "login-form-hide.jinja2" %}
{% include "search-line.jinja2" %} {% include "search-line.jinja2" %}
<section class="element"> <section class="element">
@ -46,6 +48,17 @@
<li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li> <li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li> <li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
<li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li> <li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
{% if auth_enabled %}
<li class="right">
{% if auth_username is not none %}
<form action="/logout" method="post">
<button class="login" type="submit">logout ({{ auth_username }})</button>
</form>
{% else %}
<button class="login" onclick="document.getElementById('login-form').style.display='block'">login</button>
{% endif %}
</li>
{% endif %}
</ul> </ul>
</footer> </footer>
</div> </div>

View File

@ -0,0 +1,9 @@
<script>
const modal = document.getElementById('login-form');
window.onclick = function(event) {
if (event.target === modal) {
modal.style.display = "none";
}
}
</script>

View File

@ -0,0 +1,18 @@
{#idea is from here https://www.w3schools.com/howto/howto_css_login_form.asp#}
<div id="login-form" class="modal-login-form">
<form class="modal-login-form-content animate" action="/login" method="post">
<div class="login-container">
<label for="username"><b>username</b></label>
<input type="text" placeholder="enter username" name="username" required>
<label for="password"><b>password</b></label>
<input type="password" placeholder="enter password" name="password" required>
<button class="login" type="submit">login</button>
</div>
<div class="login-container">
<button class="cancel" onclick="document.getElementById('login-form').style.display='none'">cancel</button>
</div>
</form>
</div>

View File

@ -40,6 +40,7 @@
width: inherit; width: inherit;
} }
/* table description */
th, td { th, td {
padding: 5px; padding: 5px;
} }
@ -103,6 +104,7 @@
background-color: rgba(var(--color-success), 1.0); background-color: rgba(var(--color-success), 1.0);
} }
/* navigation footer description */
ul.navigation { ul.navigation {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
@ -115,11 +117,8 @@
float: left; float: left;
} }
ul.navigation li.status { ul.navigation li.right {
display: block; float: right;
text-align: center;
text-decoration: none;
padding: 14px 16px;
} }
ul.navigation li a { ul.navigation li a {
@ -131,6 +130,86 @@
} }
ul.navigation li a:hover { 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)}
} }
</style> </style>

View File

View File

@ -17,10 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import Dict, Optional, Set from __future__ import annotations
from typing import Optional, Set, Type
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -29,13 +30,12 @@ class Auth:
helper to deal with user authorization helper to deal with user authorization
:ivar allowed_paths: URI paths which can be accessed without 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 allowed_paths_groups: URI paths prefixes which can be accessed without authorization
:ivar salt: random generated string to salt passwords :ivar enabled: indicates if authorization is enabled
:ivar users: map of username to its descriptor
:cvar ALLOWED_PATHS: URI paths which can be accessed without authorization, predefined :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 :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() ALLOWED_PATHS_GROUPS: Set[str] = set()
def __init__(self, configuration: Configuration) -> None: def __init__(self, configuration: Configuration) -> None:
@ -43,40 +43,33 @@ class Auth:
default constructor default constructor
:param configuration: configuration instance :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 = set(configuration.getlist("auth", "allowed_paths"))
self.allowed_paths.update(self.ALLOWED_PATHS) self.allowed_paths.update(self.ALLOWED_PATHS)
self.allowed_paths_groups = set(configuration.getlist("auth", "allowed_paths_groups")) self.allowed_paths_groups = set(configuration.getlist("auth", "allowed_paths_groups"))
self.allowed_paths_groups.update(self.ALLOWED_PATHS_GROUPS) self.allowed_paths_groups.update(self.ALLOWED_PATHS_GROUPS)
self.enabled = configuration.getboolean("web", "auth", fallback=False)
@staticmethod @classmethod
def get_users(configuration: Configuration) -> Dict[str, User]: def load(cls: Type[Auth], configuration: Configuration) -> Auth:
""" """
load users from settings load authorization module from settings
:param configuration: configuration instance :param configuration: configuration instance
:return: map of username to its descriptor :return: authorization module according to current settings
""" """
users: Dict[str, User] = {} if configuration.getboolean("web", "auth", fallback=False):
for role in UserAccess: from ahriman.core.auth.mapping_auth import MappingAuth
section = configuration.section_name("auth", role.value) return MappingAuth(configuration)
if not configuration.has_section(section): return cls(configuration)
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: def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
""" """
validate user password validate user password
:param username: username :param username: username
:param password: entered password :param password: entered password
:return: True in case if password matches, False otherwise :return: True in case if password matches, False otherwise
""" """
if username is None or password is None: del username, password
return False # invalid data supplied return True
return username in self.users and self.users[username].check_credentials(password, self.salt)
def is_safe_request(self, uri: Optional[str]) -> bool: def is_safe_request(self, uri: Optional[str]) -> bool:
""" """
@ -88,11 +81,21 @@ class Auth:
return False # request without context is not allowed 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) 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 validate if user has access to requested resource
:param username: username :param username: username
:param required: required access level :param required: required access level
:return: True in case if user is allowed to do this request and False otherwise :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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View File

@ -28,8 +28,7 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage # type: ignor
from cryptography import fernet from cryptography import fernet
from typing import Optional from typing import Optional
from ahriman.core.auth import Auth from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.middlewares import HandlerType, MiddlewareType from ahriman.web.middlewares import HandlerType, MiddlewareType
@ -40,12 +39,12 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
:ivar validator: validator instance :ivar validator: validator instance
""" """
def __init__(self, configuration: Configuration) -> None: def __init__(self, validator: Auth) -> None:
""" """
default constructor 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]: async def authorized_userid(self, identity: str) -> Optional[str]:
""" """
@ -53,7 +52,7 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
:param identity: username :param identity: username
:return: user identity (username) in case if user exists and None otherwise :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: async def permits(self, identity: str, permission: UserAccess, context: Optional[str] = None) -> bool:
""" """
@ -63,25 +62,25 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
:param context: URI request path :param context: URI request path
:return: True in case if user is allowed to perform this request and False otherwise :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) return self.validator.verify_access(identity, permission)
def auth_handler() -> MiddlewareType: def auth_handler(validator: Auth) -> MiddlewareType:
""" """
authorization and authentication middleware authorization and authentication middleware
:param validator: authorization module instance
:return: built middleware :return: built middleware
""" """
@middleware @middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse: async def handle(request: Request, handler: HandlerType) -> StreamResponse:
print(request)
if request.path.startswith("/api"): if request.path.startswith("/api"):
permission = UserAccess.Status permission = UserAccess.Status
elif request.method in ("GET", "HEAD", "OPTIONS"): elif request.method in ("GET", "HEAD", "OPTIONS"):
permission = UserAccess.Read permission = UserAccess.Read
else: else:
permission = UserAccess.Write permission = UserAccess.Write
if not validator.is_safe_request(request.path):
await aiohttp_security.check_permission(request, permission, request.path) await aiohttp_security.check_permission(request, permission, request.path)
return await handler(request) return await handler(request)
@ -89,11 +88,11 @@ def auth_handler() -> MiddlewareType:
return handle 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 setup authorization policies for the application
:param application: web application instance :param application: web application instance
:param configuration: configuration instance :param validator: authorization module instance
:return: configured web application :return: configured web application
""" """
fernet_key = fernet.Fernet.generate_key() 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') storage = EncryptedCookieStorage(secret_key, cookie_name='API_SESSION')
setup_session(application, storage) setup_session(application, storage)
authorization_policy = AuthorizationPolicy(configuration) authorization_policy = AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy() identity_policy = aiohttp_security.SessionIdentityPolicy()
application["validator"] = authorization_policy.validator
aiohttp_security.setup(application, identity_policy, authorization_policy) aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(auth_handler()) application.middlewares.append(auth_handler(validator))
return application return application

View File

@ -20,7 +20,7 @@
from aiohttp.web import View from aiohttp.web import View
from typing import Any, Dict 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 from ahriman.core.status.watcher import Watcher

View File

@ -22,6 +22,7 @@ import aiohttp_jinja2
from typing import Any, Dict from typing import Any, Dict
from ahriman import version from ahriman import version
from ahriman.core.auth.helpers import authorized_userid
from ahriman.core.util import pretty_datetime from ahriman.core.util import pretty_datetime
from ahriman.web.views.base import BaseView 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: It uses jinja2 templates for report generation, the following variables are allowed:
architecture - repository architecture, string, required 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 packages - sorted list of packages properties, required
* base, string * base, string
* depends, sorted list of strings * depends, sorted list of strings
@ -79,6 +82,8 @@ class IndexView(BaseView):
return { return {
"architecture": self.service.architecture, "architecture": self.service.architecture,
"auth_enabled": self.validator.enabled,
"auth_username": await authorized_userid(self.request),
"packages": packages, "packages": packages,
"repository": self.service.repository.name, "repository": self.service.repository.name,
"service": service, "service": service,

View File

@ -17,10 +17,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import aiohttp_security # type: ignore
from aiohttp.web import HTTPFound, HTTPUnauthorized, Response from aiohttp.web import HTTPFound, HTTPUnauthorized, Response
from ahriman.core.auth.helpers import remember
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -45,11 +44,8 @@ class LoginView(BaseView):
username = data.get("username") username = data.get("username")
response = HTTPFound("/") response = HTTPFound("/")
try:
if self.validator.check_credentials(username, data.get("password")): if self.validator.check_credentials(username, data.get("password")):
await aiohttp_security.remember(self.request, response, username) await remember(self.request, response, username)
return response
except KeyError:
return response return response
raise HTTPUnauthorized() raise HTTPUnauthorized()

View File

@ -17,10 +17,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import aiohttp_security # type: ignore
from aiohttp.web import HTTPFound, Response from aiohttp.web import HTTPFound, Response
from ahriman.core.auth.helpers import check_authorized, forget
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -34,9 +33,9 @@ class LogoutView(BaseView):
logout user from the service. No parameters supported here logout user from the service. No parameters supported here
:return: redirect to main page :return: redirect to main page
""" """
await aiohttp_security.check_authorized(self.request) await check_authorized(self.request)
response = HTTPFound("/") response = HTTPFound("/")
await aiohttp_security.forget(self.request, response) await forget(self.request, response)
return response return response

View File

@ -23,6 +23,7 @@ import logging
from aiohttp import web from aiohttp import web
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException from ahriman.core.exceptions import InitializeException
from ahriman.core.status.watcher import Watcher 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.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, configuration) 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 from ahriman.web.middlewares.auth_handler import setup_auth
setup_auth(application, configuration) setup_auth(application, validator)
return application return application

View File

@ -1,11 +1,10 @@
from unittest.mock import MagicMock
import pytest import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any, Type, TypeVar from typing import Any, Type, TypeVar
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.models.package import Package from ahriman.models.package import Package
@ -44,6 +43,15 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
# generic fixtures # generic fixtures
@pytest.fixture
def auth(configuration: Configuration) -> Auth:
"""
auth provider fixture
:return: auth service instance
"""
return Auth(configuration)
@pytest.fixture @pytest.fixture
def configuration(resource_path_root: Path) -> Configuration: def configuration(resource_path_root: Path) -> Configuration:
""" """

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -2,7 +2,7 @@ import pytest
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo 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.build_tools.task import Task
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.tree import Leaf 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()) 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 @pytest.fixture
def pacman(configuration: Configuration) -> Pacman: def pacman(configuration: Configuration) -> Pacman:
""" """

View File

@ -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)

View File

@ -61,6 +61,8 @@ def test_get_local_files(s3: S3, resource_path_root: Path) -> None:
Path("models/package_yay_srcinfo"), Path("models/package_yay_srcinfo"),
Path("web/templates/search-line.jinja2"), Path("web/templates/search-line.jinja2"),
Path("web/templates/build-status.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/repo-index.jinja2"),
Path("web/templates/sorttable.jinja2"), Path("web/templates/sorttable.jinja2"),
Path("web/templates/style.jinja2"), Path("web/templates/style.jinja2"),

View File

@ -3,7 +3,10 @@ import pytest
from aiohttp import web from aiohttp import web
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
import ahriman.core.auth.helpers
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.user import User
from ahriman.web.web import setup_service from ahriman.web.web import setup_service
@ -15,18 +18,26 @@ def application(configuration: Configuration, mocker: MockerFixture) -> web.Appl
:param mocker: mocker object :param mocker: mocker object
:return: application test instance :return: application test instance
""" """
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
return setup_service("x86_64", configuration) return setup_service("x86_64", configuration)
@pytest.fixture @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 application fixture with auth enabled
:param configuration: configuration fixture :param configuration: configuration fixture
:param user: user descriptor fixture
:param mocker: mocker object :param mocker: mocker object
:return: application test instance :return: application test instance
""" """
configuration.set("web", "auth", "yes") configuration.set("web", "auth", "yes")
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", True)
mocker.patch("pathlib.Path.mkdir") 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

View File

@ -2,6 +2,7 @@ import pytest
from collections import namedtuple from collections import namedtuple
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.web.middlewares.auth_handler import AuthorizationPolicy from ahriman.web.middlewares.auth_handler import AuthorizationPolicy
@ -24,6 +25,8 @@ def authorization_policy(configuration: Configuration, user: User) -> Authorizat
fixture for authorization policy fixture for authorization policy
:return: authorization policy fixture :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} policy.validator.users = {user.username: user}
return policy return policy

View File

@ -1,8 +1,9 @@
from aiohttp import web from aiohttp import web
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any 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.core.configuration import Configuration
from ahriman.models.user import User from ahriman.models.user import User
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -17,89 +18,83 @@ async def test_authorized_userid(authorization_policy: AuthorizationPolicy, user
assert await authorization_policy.authorized_userid("some random name") is None 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 must call validator check
""" """
safe_request_mock = mocker.patch("ahriman.core.auth.Auth.is_safe_request", return_value=False) authorization_policy.validator = MagicMock()
verify_access_mock = mocker.patch("ahriman.core.auth.Auth.verify_access", return_value=True) authorization_policy.validator.verify_access.return_value = True
assert await authorization_policy.permits(user.username, user.access, "/endpoint") assert await authorization_policy.permits(user.username, user.access, "/endpoint")
safe_request_mock.assert_called_with("/endpoint") authorization_policy.validator.verify_access.assert_called_with(user.username, user.access)
verify_access_mock.assert_called_with(user.username, user.access)
async def test_permits_safe(authorization_policy: AuthorizationPolicy, user: User, mocker: MockerFixture) -> None: async def test_auth_handler_api(aiohttp_request: Any, auth: Auth, 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:
""" """
must ask for status permission for api calls must ask for status permission for api calls
""" """
aiohttp_request = aiohttp_request._replace(path="/api") aiohttp_request = aiohttp_request._replace(path="/api")
request_handler = AsyncMock() 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") check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler() handler = auth_handler(auth)
await handler(aiohttp_request, request_handler) await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path) 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 must ask for status permission for api calls with POST
""" """
aiohttp_request = aiohttp_request._replace(path="/api", method="POST") aiohttp_request = aiohttp_request._replace(path="/api", method="POST")
request_handler = AsyncMock() 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") check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler() handler = auth_handler(auth)
await handler(aiohttp_request, request_handler) await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Status, aiohttp_request.path) 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 must ask for read permission for api calls with GET
""" """
for method in ("GET", "HEAD", "OPTIONS"): for method in ("GET", "HEAD", "OPTIONS"):
aiohttp_request = aiohttp_request._replace(method=method) aiohttp_request = aiohttp_request._replace(method=method)
request_handler = AsyncMock() 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") check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler() handler = auth_handler(auth)
await handler(aiohttp_request, request_handler) await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path) 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 must ask for read permission for api calls with POST
""" """
for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"): for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"):
aiohttp_request = aiohttp_request._replace(method=method) aiohttp_request = aiohttp_request._replace(method=method)
request_handler = AsyncMock() 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") check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler() handler = auth_handler(auth)
await handler(aiohttp_request, request_handler) await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path) 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 must setup authorization
""" """
aiohttp_security_setup_mock = mocker.patch("aiohttp_security.setup") 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 assert application.get("validator") is not None
aiohttp_security_setup_mock.assert_called_once() aiohttp_security_setup_mock.assert_called_once()

View File

@ -20,3 +20,18 @@ def client(application: web.Application, loop: BaseEventLoop,
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[]) mocker.patch("pathlib.Path.iterdir", return_value=[])
return loop.run_until_complete(aiohttp_client(application)) 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))

View File

@ -1,19 +1,28 @@
from pytest_aiohttp import TestClient 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 (/) 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("/") response = await client.get("/")
assert response.status == 200 assert response.status == 200
assert await response.text() 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()

View File

@ -1,48 +1,41 @@
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.models.user import User 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 must login user correctly
""" """
client.app["validator"] = Auth(configuration)
payload = {"username": user.username, "password": user.password} payload = {"username": user.username, "password": user.password}
remember_patch = mocker.patch("aiohttp_security.remember") remember_mock = mocker.patch("aiohttp_security.remember")
mocker.patch("ahriman.core.auth.Auth.check_credentials", return_value=True)
post_response = await client.post("/login", json=payload) post_response = await client_with_auth.post("/login", json=payload)
assert post_response.status == 200 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 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 must process if no auth configured
""" """
payload = {"username": user.username, "password": user.password} payload = {"username": user.username, "password": user.password}
post_response = await client.post("/login", json=payload) post_response = await client.post("/login", json=payload)
remember_patch = mocker.patch("aiohttp_security.remember")
assert post_response.status == 200 assert post_response.status == 200
remember_patch.assert_not_called()
async def test_post_unauthorized(client: TestClient, configuration: Configuration, user: User, async def test_post_unauthorized(client_with_auth: TestClient, user: User, mocker: MockerFixture) -> None:
mocker: MockerFixture) -> None:
""" """
must return unauthorized on invalid auth must return unauthorized on invalid auth
""" """
client.app["validator"] = Auth(configuration) payload = {"username": user.username, "password": ""}
payload = {"username": user.username, "password": user.password} remember_mock = mocker.patch("aiohttp_security.remember")
mocker.patch("ahriman.core.auth.Auth.check_credentials", return_value=False)
post_response = await client.post("/login", json=payload) post_response = await client_with_auth.post("/login", json=payload)
assert post_response.status == 401 assert post_response.status == 401
remember_mock.assert_not_called()

View File

@ -3,36 +3,33 @@ from aiohttp.web import HTTPUnauthorized
from pytest_mock import MockerFixture 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 must logout user correctly
""" """
mocker.patch("aiohttp_security.check_authorized") 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 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 must raise exception if unauthorized
""" """
mocker.patch("aiohttp_security.check_authorized", side_effect=HTTPUnauthorized()) 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 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 must raise exception if auth is disabled
""" """
forget_patch = mocker.patch("aiohttp_security.forget")
post_response = await client.post("/logout") post_response = await client.post("/logout")
assert post_response.status == 401 assert post_response.status == 200
forget_patch.assert_not_called()

View File

@ -57,6 +57,5 @@ region = eu-central-1
secret_key = secret_key =
[web] [web]
auth = no
host = 127.0.0.1 host = 127.0.0.1
templates = ../web/templates templates = ../web/templates