Compare commits

...

2 Commits

25 changed files with 60 additions and 41 deletions

View File

@ -44,7 +44,7 @@ The main functionality of the class is already described, but command is still n
arguments = set_parser
In addition, ``ahriman.application.handlers.handler.Handler.ALLOW_MULTI_ARCHITECTURE_RUN`` can be set to ``False`` in order to disable multiprocess run (e.g. in case if there are conflicting operations, like writting to stdout).
In addition, ``ahriman.application.handlers.handler.Handler.ALLOW_MULTI_ARCHITECTURE_RUN`` can be set to ``False`` in order to disable multiprocess run (e.g. in case if there are conflicting operations, like writing to stdout).
Save the file above as ``/usr/lib/python3.12/site-packages/ahriman/application/handlers/help_web.py`` (replace ``python3.12`` with actual python version) and you are set.

View File

@ -394,7 +394,7 @@ All extracted fields are packed as ``ahriman.models.pkgbuild_patch.PkgbuildPatch
The PKGBUILD class also provides some additional functions on top of that:
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multipackages.
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
Additional features

View File

@ -97,7 +97,7 @@ Otherwise, you would need to pass ``AHRIMAN_PORT`` and mount container network t
Simple server with authentication can be found in `examples <https://github.com/arcan1s/ahriman/tree/master/recipes/web>`__ too.
Mutli-repository web service
Multi-repository web service
""""""""""""""""""""""""""""
Idea is pretty same as to just run web service. However, it is required to run setup commands for each repository, except for one which is specified by ``AHRIMAN_REPOSITORY`` and ``AHRIMAN_ARCHITECTURE`` variables.

View File

@ -3,7 +3,7 @@ To 2.16.0
This release replaces ``passlib`` dependency with ``bcrypt``.
The reason behind this change is that python developers have deprecated and scheduled for removal ``crypt`` module, which is used by ``passlib``. (By the way, they recommend to use ``passlib`` as a replacement.) Unfortunately, it appears that ``passlib`` is unmaintained (see `the issue <https://foss.heptapod.net/python-libs/passlib/-/issues/187>`__), so the only solution is to migrate to anoher library.
The reason behind this change is that python developers have deprecated and scheduled for removal ``crypt`` module, which is used by ``passlib``. (By the way, they recommend to use ``passlib`` as a replacement.) Unfortunately, it appears that ``passlib`` is unmaintained (see `the issue <https://foss.heptapod.net/python-libs/passlib/-/issues/187>`__), so the only solution is to migrate to another library.
Because passwords are stored as hashes, it is near to impossible to shadow change passwords in database, the manual intervention is required if:

View File

@ -21,7 +21,7 @@ import argparse
from pathlib import Path
from pwd import getpwuid
from urllib.parse import quote_plus as urlencode
from urllib.parse import quote_plus as url_encode
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
@ -174,7 +174,7 @@ class Setup(Handler):
if args.web_unix_socket is not None:
unix_socket = str(args.web_unix_socket)
configuration.set_option("web", "unix_socket", unix_socket)
configuration.set_option("status", "address", f"http+unix://{urlencode(unix_socket)}")
configuration.set_option("status", "address", f"http+unix://{url_encode(unix_socket)}")
if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20))

View File

@ -146,7 +146,7 @@ class PkgbuildParser(shlex.shlex):
# reset state
buffer, prefix = [], None
# we have already prefix string, so we are in progress of expansion
# we have already got prefix string, so we are in progress of expansion
# we always operate the last element, so this matches ",", "next"
case (PkgbuildToken.Comma, _) if prefix is not None:
buffer.append(f"{prefix}{second}")
@ -168,7 +168,7 @@ class PkgbuildParser(shlex.shlex):
def _is_escaped(self) -> bool:
"""
check if the last element was quoted. ``shlex.shlex`` parser doesn't provide information about was the token
quoted or not, thus there is no difference between "'#'" (diez in quotes) and "#" (diez without quotes). This
quoted or not, thus there is no difference between "'#'" (sharp in quotes) and "#" (sharp without quotes). This
method simply rolls back to the last non-space character and check if it is a quotation mark
Returns:

View File

@ -59,10 +59,10 @@ class Mapping(Auth):
"""
if password is None:
return False # invalid data supplied
user = self.get_user(username)
user = await self.get_user(username)
return user is not None and user.check_credentials(password, self.salt)
def get_user(self, username: str) -> User | None:
async def get_user(self, username: str) -> User | None:
"""
retrieve user from in-memory mapping
@ -84,7 +84,7 @@ class Mapping(Auth):
Returns:
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
"""
return username is not None and self.get_user(username) is not None
return username is not None and await self.get_user(username) is not None
async def verify_access(self, username: str, required: UserAccess, context: str | None) -> bool:
"""
@ -98,5 +98,5 @@ class Mapping(Auth):
Returns:
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
"""
user = self.get_user(username)
user = await self.get_user(username)
return user is not None and user.verify_access(required)

View File

@ -120,7 +120,7 @@ class PAM(Mapping):
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
"""
# this method is basically inverted, first we check overrides in database and then fallback to the PAM logic
if (user := self.get_user(username)) is not None:
if (user := await self.get_user(username)) is not None:
return user.verify_access(required)
# if username is in admin group, then we treat it as full access
if username in self.group_members(self.full_access_group):

View File

@ -58,8 +58,8 @@ class EventStatsPrinter(StringPrinter):
mean = statistics.mean(self.events)
if len(self.events) > 1:
stdev = statistics.stdev(self.events)
average = f"{mean:.3f} ± {stdev:.3f}"
st_dev = statistics.stdev(self.events)
average = f"{mean:.3f} ± {st_dev:.3f}"
else:
average = f"{mean:.3f}"

View File

@ -19,7 +19,7 @@
#
import contextlib
from urllib.parse import quote_plus as urlencode
from urllib.parse import quote_plus as url_encode
from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncAhrimanClient
@ -75,7 +75,7 @@ class WebClient(Client, SyncAhrimanClient):
# legacy-style section
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
# special pseudo-protocol which is used for unix sockets
return "web", f"http+unix://{urlencode(unix_socket)}"
return "web", f"http+unix://{url_encode(unix_socket)}"
address = configuration.get("web", "address", fallback=None)
if not address:
# build address from host and port directly
@ -94,7 +94,7 @@ class WebClient(Client, SyncAhrimanClient):
Returns:
str: full url for web service for changes
"""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/changes"
return f"{self.address}/api/v1/packages/{url_encode(package_base)}/changes"
def _dependencies_url(self, package_base: str) -> str:
"""
@ -106,7 +106,7 @@ class WebClient(Client, SyncAhrimanClient):
Returns:
str: full url for web service for dependencies
"""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/dependencies"
return f"{self.address}/api/v1/packages/{url_encode(package_base)}/dependencies"
def _events_url(self) -> str:
"""
@ -127,7 +127,7 @@ class WebClient(Client, SyncAhrimanClient):
Returns:
str: full url for web service for logs
"""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/logs"
return f"{self.address}/api/v1/packages/{url_encode(package_base)}/logs"
def _package_url(self, package_base: str = "") -> str:
"""
@ -139,7 +139,7 @@ class WebClient(Client, SyncAhrimanClient):
Returns:
str: full url of web service for specific package base
"""
suffix = f"/{urlencode(package_base)}" if package_base else ""
suffix = f"/{url_encode(package_base)}" if package_base else ""
return f"{self.address}/api/v1/packages{suffix}"
def _patches_url(self, package_base: str, variable: str = "") -> str:
@ -153,8 +153,8 @@ class WebClient(Client, SyncAhrimanClient):
Returns:
str: full url of web service for the package patch
"""
suffix = f"/{urlencode(variable)}" if variable else ""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/patches{suffix}"
suffix = f"/{url_encode(variable)}" if variable else ""
return f"{self.address}/api/v1/packages/{url_encode(package_base)}/patches{suffix}"
def _status_url(self) -> str:
"""

View File

@ -107,8 +107,8 @@ class Pkgbuild(Mapping[str, Any]):
def __getitem__(self, item: str) -> Any:
"""
get the field of the PKGBUILD. This method tries to get exact key value if possible; if none found, it tries to
fetch function with the same name
get the field of the PKGBUILD. This method tries to get exact key value if possible; if none was found,
it tries to fetch function with the same name
Args:
item(str): key name

View File

@ -99,6 +99,8 @@ class User:
Returns:
bool: ``True`` in case if password matches, ``False`` otherwise
"""
if self.password.startswith("$6$"):
raise DeprecationWarning("SHA512 passwords are not longer supported")
try:
return bcrypt.checkpw((password + salt).encode("utf8"), self.password.encode("utf8"))
except ValueError:

View File

@ -53,7 +53,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command and remove packages afterwards
must run command and remove packages afterward
"""
args = _default_args(args)
args.remove = True

View File

@ -191,7 +191,7 @@ def test_extract_packages_by_status(application: Application, mocker: MockerFixt
def test_extract_packages_from_database(application: Application, mocker: MockerFixture) -> None:
"""
must extract packages from database from database
must extract packages from database
"""
packages_mock = mocker.patch("ahriman.core.database.SQLite.packages_get")
Rebuild.extract_packages(application, None, from_database=True)

View File

@ -5,7 +5,7 @@ from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import call as MockCall
from urllib.parse import quote_plus as urlencode
from urllib.parse import quote_plus as url_encode
from ahriman.application.handlers.setup import Setup
from ahriman.core.configuration import Configuration
@ -148,7 +148,7 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
MockCall("web", "port", str(args.web_port)),
MockCall("status", "address", f"http://127.0.0.1:{str(args.web_port)}"),
MockCall("web", "unix_socket", str(args.web_unix_socket)),
MockCall("status", "address", f"http+unix://{urlencode(str(args.web_unix_socket))}"),
MockCall("status", "address", f"http+unix://{url_encode(str(args.web_unix_socket))}"),
MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)),
])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -392,7 +392,7 @@ def test_subparsers_package_status_update(parser: argparse.ArgumentParser) -> No
def test_subparsers_package_status_update_option_status(parser: argparse.ArgumentParser) -> None:
"""
package-status-update command must convert status option to buildstatusenum instance
package-status-update command must convert status option to BuildStatusEnum instance
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update"])
assert isinstance(args.status, BuildStatusEnum)

View File

@ -115,7 +115,7 @@ def test_write_skip(lock: Lock) -> None:
def test_write_locked(lock: Lock, mocker: MockerFixture) -> None:
"""
must raise DuplicateRunError if cannot lock file
must raise DuplicateRunError if it cannot lock file
"""
mocker.patch("ahriman.application.lock.Lock.perform_lock", return_value=False)
with pytest.raises(DuplicateRunError):

View File

@ -287,6 +287,7 @@ def local_client(database: SQLite, configuration: Configuration) -> Client:
Args:
database(SQLite): database fixture
configuration(Configuration): configuration fixture
Returns:
Client: local status client test instance

View File

@ -31,27 +31,27 @@ async def test_check_credentials_unknown(mapping: Mapping, user: User) -> None:
assert not await mapping.check_credentials(user.username, user.password)
def test_get_user(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
async def test_get_user(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must return user from storage by username
"""
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=user)
assert mapping.get_user(user.username) == user
assert await mapping.get_user(user.username) == user
def test_get_user_normalized(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
async def test_get_user_normalized(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
"""
must return user from storage by username case-insensitive
"""
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=user)
assert mapping.get_user(user.username.upper()) == user
assert await mapping.get_user(user.username.upper()) == user
def test_get_user_unknown(mapping: Mapping, user: User) -> None:
async def test_get_user_unknown(mapping: Mapping, user: User) -> None:
"""
must return None in case if no user found
"""
assert mapping.get_user(user.username) is None
assert await mapping.get_user(user.username) is None
async def test_known_username(mapping: Mapping, user: User, mocker: MockerFixture) -> None:

View File

@ -77,7 +77,7 @@ async def test_known_username(pam: PAM, user: User, mocker: MockerFixture) -> No
async def test_known_username_mapping(pam: PAM, user: User, mocker: MockerFixture) -> None:
"""
must fallback to username checking to database if no user found in system
must fall back to username checking to database if no user found in system
"""
mocker.patch("ahriman.core.auth.pam.getpwnam", side_effect=KeyError)
mapping_mock = mocker.patch("ahriman.core.auth.mapping.Mapping.known_username")

View File

@ -32,6 +32,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=False)
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
changes_mock.assert_called_once_with(package_ahriman.base)
depends_on_mock.assert_called_once_with()
dependencies_mock.assert_called_once_with(package_ahriman.base, Dependencies())
# must move files (once)

View File

@ -136,7 +136,7 @@ def test_event_add_failed_http_error(web_client: WebClient, mocker: MockerFixtur
def test_event_add_failed_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during events creaton and don't log
must suppress any exception happened during events creation and don't log
"""
web_client.suppress_errors = True
mocker.patch("requests.Session.request", side_effect=Exception())

View File

@ -5,7 +5,7 @@ from ahriman.models.scan_paths import ScanPaths
def test_is_allowed() -> None:
"""
must check if path is subpath of one in allowed list
must check if path is sub-path of one in allowed list
"""
assert ScanPaths(["usr"]).is_allowed(Path("usr"))
assert ScanPaths(["usr"]).is_allowed(Path("usr") / "lib")

View File

@ -1,3 +1,5 @@
import pytest
from dataclasses import replace
from ahriman.models.user import User
@ -25,6 +27,19 @@ def test_check_credentials_empty_hash(user: User) -> None:
assert not user.check_credentials(current_password, "salt")
def test_check_credentials_sha512() -> None:
"""
must raise DeprecationWarning for sha512 hashed passwords
"""
user = User(
username="user",
password="$6$rounds=656000$mWBiecMPrHAL1VgX$oU4Y5HH8HzlvMaxwkNEJjK13ozElyU1wAHBoO/WW5dAaE4YEfnB0X3FxbynKMl4FBdC3Ovap0jINz4LPkNADg0",
access=UserAccess.Read,
)
with pytest.raises(DeprecationWarning):
assert user.check_credentials("password", "salt")
def test_hash_password_empty_hash(user: User) -> None:
"""
must return empty string after hash in case if password not set

View File

@ -252,6 +252,6 @@ async def test_username_request(base: BaseView) -> None:
async def test_username_request_exception(base: BaseView) -> None:
"""
must not fail in case if cannot read request
must not fail in case if it cannot read request
"""
assert await base.username() is None