mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 23:37:18 +00:00
add login annd logout to index also improve auth
This commit is contained in:
parent
40ef103e2f
commit
b755088024
@ -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>
|
||||||
|
9
package/share/ahriman/login-form-hide.jinja2
Normal file
9
package/share/ahriman/login-form-hide.jinja2
Normal 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>
|
18
package/share/ahriman/login-form.jinja2
Normal file
18
package/share/ahriman/login-form.jinja2
Normal 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>
|
@ -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>
|
||||||
|
0
src/ahriman/core/auth/__init__.py
Normal file
0
src/ahriman/core/auth/__init__.py
Normal 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
|
70
src/ahriman/core/auth/helpers.py
Normal file
70
src/ahriman/core/auth/helpers.py
Normal 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
|
86
src/ahriman/core/auth/mapping_auth.py
Normal file
86
src/ahriman/core/auth/mapping_auth.py
Normal 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)
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
14
tests/ahriman/core/auth/conftest.py
Normal file
14
tests/ahriman/core/auth/conftest.py
Normal 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)
|
81
tests/ahriman/core/auth/test_auth.py
Normal file
81
tests/ahriman/core/auth/test_auth.py
Normal 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)
|
102
tests/ahriman/core/auth/test_helpers.py
Normal file
102
tests/ahriman/core/auth/test_helpers.py
Normal 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()
|
67
tests/ahriman/core/auth/test_mapping_auth.py
Normal file
67
tests/ahriman/core/auth/test_mapping_auth.py
Normal 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)
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
|
@ -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"),
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
|
||||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user