mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
Compare commits
2 Commits
4b5a645f8d
...
3fa3cc46ad
Author | SHA1 | Date | |
---|---|---|---|
3fa3cc46ad | |||
20e7ba3b1d |
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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}"
|
||||
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user