mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-05-05 12:43:49 +00:00
Compare commits
3 Commits
40fa94afbb
...
54a331cc96
Author | SHA1 | Date | |
---|---|---|---|
54a331cc96 | |||
5f79cbc34b | |||
ea4193eef4 |
@ -59,7 +59,7 @@ class Handler:
|
|||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True on success, False otherwise
|
bool: ``True`` on success, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
configuration = Configuration.from_path(args.configuration, repository_id)
|
configuration = Configuration.from_path(args.configuration, repository_id)
|
||||||
@ -129,7 +129,7 @@ class Handler:
|
|||||||
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
|
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
enabled(bool): if False no check will be performed
|
enabled(bool): if ``False`` no check will be performed
|
||||||
predicate(bool): indicates condition on which exception should be thrown
|
predicate(bool): indicates condition on which exception should be thrown
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
@ -97,7 +97,7 @@ class Lock(LazyLogging):
|
|||||||
fd(int): file descriptor:
|
fd(int): file descriptor:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if file is locked and False otherwise
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
@ -119,7 +119,7 @@ class Lock(LazyLogging):
|
|||||||
watch until lock disappear
|
watch until lock disappear
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if file is locked and False otherwise
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
||||||
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
||||||
@ -223,7 +223,7 @@ class Lock(LazyLogging):
|
|||||||
exc_tb(TracebackType): exception traceback if any
|
exc_tb(TracebackType): exception traceback if any
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Literal[False]: always False (do not suppress any exception)
|
Literal[False]: always ``False`` (do not suppress any exception)
|
||||||
"""
|
"""
|
||||||
self.clear()
|
self.clear()
|
||||||
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
||||||
|
@ -101,7 +101,7 @@ class PacmanDatabase(SyncHttpClient):
|
|||||||
local_path(Path): path to locally stored file
|
local_path(Path): path to locally stored file
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if remote file is newer than local file
|
bool: ``True`` in case if remote file is newer than local file
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
PacmanError: in case if no last-modified header was found
|
PacmanError: in case if no last-modified header was found
|
||||||
|
@ -96,7 +96,7 @@ class Auth(LazyLogging):
|
|||||||
password(str | None): entered password
|
password(str | None): entered password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
del username, password
|
del username, password
|
||||||
return True
|
return True
|
||||||
@ -109,7 +109,7 @@ class Auth(LazyLogging):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is known and can be authorized and False otherwise
|
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
del username
|
del username
|
||||||
return True
|
return True
|
||||||
@ -124,7 +124,7 @@ class Auth(LazyLogging):
|
|||||||
context(str | None): URI request path
|
context(str | None): URI request path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
del username, required, context
|
del username, required, context
|
||||||
return True
|
return True
|
||||||
|
@ -38,7 +38,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
@ -54,7 +54,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
@ -70,7 +70,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
@ -86,7 +86,7 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
|
@ -57,7 +57,7 @@ class Mapping(Auth):
|
|||||||
password(str | None): entered password
|
password(str | None): entered password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if password is None:
|
if password is None:
|
||||||
return False # invalid data supplied
|
return False # invalid data supplied
|
||||||
@ -72,7 +72,7 @@ class Mapping(Auth):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User | None: user descriptor if username is known and None otherwise
|
User | None: user descriptor if username is known and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.database.user_get(username)
|
return self.database.user_get(username)
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class Mapping(Auth):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is known and can be authorized and False otherwise
|
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 self.get_user(username) is not None
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class Mapping(Auth):
|
|||||||
context(str | None): URI request path
|
context(str | None): URI request path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
return user is not None and user.verify_access(required)
|
return user is not None and user.verify_access(required)
|
||||||
|
@ -79,7 +79,7 @@ class PAM(Mapping):
|
|||||||
password(str | None): entered password
|
password(str | None): entered password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if password is None:
|
if password is None:
|
||||||
return False # invalid data supplied
|
return False # invalid data supplied
|
||||||
@ -101,7 +101,7 @@ class PAM(Mapping):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is known and can be authorized and False otherwise
|
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_ = getpwnam(username)
|
_ = getpwnam(username)
|
||||||
@ -119,7 +119,7 @@ class PAM(Mapping):
|
|||||||
context(str | None): URI request path
|
context(str | None): URI request path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
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
|
# 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 := self.get_user(username)) is not None:
|
||||||
|
@ -138,7 +138,7 @@ class Sources(LazyLogging):
|
|||||||
sources_dir(Path): local path to git repository
|
sources_dir(Path): local path to git repository
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if there is any remote and false otherwise
|
bool: ``True`` in case if there is any remote and false otherwise
|
||||||
"""
|
"""
|
||||||
instance = Sources()
|
instance = Sources()
|
||||||
remotes = check_output(*instance.git(), "remote", cwd=sources_dir, logger=instance.logger)
|
remotes = check_output(*instance.git(), "remote", cwd=sources_dir, logger=instance.logger)
|
||||||
@ -261,7 +261,7 @@ class Sources(LazyLogging):
|
|||||||
commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None)
|
commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if changes have been committed and False otherwise
|
bool: ``True`` in case if changes have been committed and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if not self.has_changes(sources_dir):
|
if not self.has_changes(sources_dir):
|
||||||
return False # nothing to commit
|
return False # nothing to commit
|
||||||
@ -351,7 +351,7 @@ class Sources(LazyLogging):
|
|||||||
sources_dir(Path): local path to git repository
|
sources_dir(Path): local path to git repository
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if there are uncommitted changes and False otherwise
|
bool: ``True`` if there are uncommitted changes and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
# there is --exit-code argument to diff, however, there might be other process errors
|
# there is --exit-code argument to diff, however, there might be other process errors
|
||||||
changes = check_output(*self.git(), "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
|
changes = check_output(*self.git(), "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
|
||||||
|
@ -149,7 +149,7 @@ class Validator(RootValidator):
|
|||||||
check if paths exists
|
check if paths exists
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
constraint(bool): True in case if path must exist and False otherwise
|
constraint(bool): ``True`` in case if path must exist and ``False`` otherwise
|
||||||
field(str): field name to be checked
|
field(str): field name to be checked
|
||||||
value(Path): value to be checked
|
value(Path): value to be checked
|
||||||
|
|
||||||
|
38
src/ahriman/core/database/migrations/m014_auditlog.py
Normal file
38
src/ahriman/core/database/migrations/m014_auditlog.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""
|
||||||
|
create table auditlog (
|
||||||
|
created integer not null,
|
||||||
|
repository text not null,
|
||||||
|
event text not null,
|
||||||
|
object_id text not null,
|
||||||
|
message text,
|
||||||
|
data json
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
create index auditlog_created_repository_event_object_id
|
||||||
|
on auditlog (created, repository, event, object_id)
|
||||||
|
""",
|
||||||
|
]
|
@ -21,6 +21,7 @@ from ahriman.core.database.operations.auth_operations import AuthOperations
|
|||||||
from ahriman.core.database.operations.build_operations import BuildOperations
|
from ahriman.core.database.operations.build_operations import BuildOperations
|
||||||
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
||||||
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
|
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
|
||||||
|
from ahriman.core.database.operations.event_operations import EventOperations
|
||||||
from ahriman.core.database.operations.logs_operations import LogsOperations
|
from ahriman.core.database.operations.logs_operations import LogsOperations
|
||||||
from ahriman.core.database.operations.package_operations import PackageOperations
|
from ahriman.core.database.operations.package_operations import PackageOperations
|
||||||
from ahriman.core.database.operations.patch_operations import PatchOperations
|
from ahriman.core.database.operations.patch_operations import PatchOperations
|
||||||
|
@ -39,7 +39,7 @@ class DependenciesOperations(Operations):
|
|||||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dependencies: changes for the package base if available
|
Dependencies: dependencies for the package base if available
|
||||||
"""
|
"""
|
||||||
repository_id = repository_id or self._repository_id
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
104
src/ahriman/core/database/operations/event_operations.py
Normal file
104
src/ahriman/core/database/operations/event_operations.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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 sqlite3 import Connection
|
||||||
|
|
||||||
|
from ahriman.core.database.operations.operations import Operations
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class EventOperations(Operations):
|
||||||
|
"""
|
||||||
|
operations for audit log table
|
||||||
|
"""
|
||||||
|
|
||||||
|
def event_get(self, event: str | EventType | None = None, object_id: str | None = None,
|
||||||
|
limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[Event]:
|
||||||
|
"""
|
||||||
|
get list of events with filters applied
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(str | EventType | None, optional): filter by event type (Default value = None)
|
||||||
|
object_id(str | None, optional): filter by event object (Default value = None)
|
||||||
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Event]: list of audit log events
|
||||||
|
"""
|
||||||
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
def run(connection: Connection) -> list[Event]:
|
||||||
|
return [
|
||||||
|
Event(
|
||||||
|
event=row["event"],
|
||||||
|
object_id=row["object_id"],
|
||||||
|
message=row["message"],
|
||||||
|
data=row["data"],
|
||||||
|
created=row["created"],
|
||||||
|
) for row in connection.execute(
|
||||||
|
"""
|
||||||
|
select created, event, object_id, message, data from auditlog
|
||||||
|
where (:event is null or event = :event)
|
||||||
|
and (:object_id is null or object_id = :object_id)
|
||||||
|
and repository = :repository
|
||||||
|
order by created limit :limit offset :offset
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"event": event,
|
||||||
|
"object_id": object_id,
|
||||||
|
"repository": repository_id.id,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.with_connection(run)
|
||||||
|
|
||||||
|
def event_insert(self, event: Event, repository_id: RepositoryId | None = None) -> None:
|
||||||
|
"""
|
||||||
|
insert audit log event
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(Event): event to insert
|
||||||
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
"""
|
||||||
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
def run(connection: Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
insert into auditlog
|
||||||
|
(created, repository, event, object_id, message, data)
|
||||||
|
values
|
||||||
|
(:created, :repository, :event, :object_id, :message, :data)
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"created": event.created,
|
||||||
|
"repository": repository_id.id,
|
||||||
|
"event": event.event,
|
||||||
|
"object_id": event.object_id,
|
||||||
|
"message": event.message,
|
||||||
|
"data": event.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.with_connection(run, commit=True)
|
@ -74,7 +74,7 @@ class Operations(LazyLogging):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
query(Callable[[Connection], T]): function to be called with connection
|
query(Callable[[Connection], T]): function to be called with connection
|
||||||
commit(bool, optional): if True commit() will be called on success (Default value = False)
|
commit(bool, optional): if ``True`` commit() will be called on success (Default value = False)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
T: result of the ``query`` call
|
T: result of the ``query`` call
|
||||||
|
@ -26,7 +26,7 @@ from typing import Self
|
|||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database.migrations import Migrations
|
from ahriman.core.database.migrations import Migrations
|
||||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
||||||
DependenciesOperations, LogsOperations, PackageOperations, PatchOperations
|
DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-ancestors
|
# pylint: disable=too-many-ancestors
|
||||||
@ -35,6 +35,7 @@ class SQLite(
|
|||||||
BuildOperations,
|
BuildOperations,
|
||||||
ChangesOperations,
|
ChangesOperations,
|
||||||
DependenciesOperations,
|
DependenciesOperations,
|
||||||
|
EventOperations,
|
||||||
LogsOperations,
|
LogsOperations,
|
||||||
PackageOperations,
|
PackageOperations,
|
||||||
PatchOperations):
|
PatchOperations):
|
||||||
|
@ -32,7 +32,7 @@ class BuildPrinter(StringPrinter):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): built package
|
package(Package): built package
|
||||||
is_success(bool): True in case if build has success status and False otherwise
|
is_success(bool): ``True`` in case if build has success status and ``False`` otherwise
|
||||||
use_utf(bool): use utf instead of normal symbols
|
use_utf(bool): use utf instead of normal symbols
|
||||||
"""
|
"""
|
||||||
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
|
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
|
||||||
@ -43,7 +43,7 @@ class BuildPrinter(StringPrinter):
|
|||||||
generate sign according to settings
|
generate sign according to settings
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
is_success(bool): True in case if build has success status and False otherwise
|
is_success(bool): ``True`` in case if build has success status and ``False`` otherwise
|
||||||
use_utf(bool): use utf instead of normal symbols
|
use_utf(bool): use utf instead of normal symbols
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -57,7 +57,7 @@ class ChangesPrinter(Printer):
|
|||||||
generate entry title from content
|
generate entry title from content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and None otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
if self.changes.is_empty:
|
if self.changes.is_empty:
|
||||||
return None
|
return None
|
||||||
|
@ -63,7 +63,7 @@ class Printer:
|
|||||||
generate entry title from content
|
generate entry title from content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and None otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -42,6 +42,6 @@ class StringPrinter(Printer):
|
|||||||
generate entry title from content
|
generate entry title from content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and None otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.content
|
return self.content
|
||||||
|
@ -85,7 +85,7 @@ class SyncHttpClient(LazyLogging):
|
|||||||
exception(requests.RequestException): exception raised
|
exception(requests.RequestException): exception raised
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: text of the response if it is not None and empty string otherwise
|
str: text of the response if it is not ``None`` and empty string otherwise
|
||||||
"""
|
"""
|
||||||
result: str = exception.response.text if exception.response is not None else ""
|
result: str = exception.response.text if exception.response is not None else ""
|
||||||
return result
|
return result
|
||||||
|
@ -38,8 +38,8 @@ class JinjaTemplate:
|
|||||||
|
|
||||||
* homepage - link to homepage, string, optional
|
* homepage - link to homepage, string, optional
|
||||||
* link_path - prefix fo packages to download, string, required
|
* link_path - prefix fo packages to download, string, required
|
||||||
* has_package_signed - True in case if package sign enabled, False otherwise, required
|
* has_package_signed - ``True`` in case if package sign enabled, ``False`` otherwise, required
|
||||||
* has_repo_signed - True in case if repository database sign enabled, False otherwise, required
|
* has_repo_signed - ``True`` in case if repository database sign enabled, ``False`` otherwise, required
|
||||||
* packages - sorted list of packages properties, required
|
* packages - sorted list of packages properties, required
|
||||||
* architecture, string
|
* architecture, string
|
||||||
* archive_size, pretty printed size, string
|
* archive_size, pretty printed size, string
|
||||||
|
@ -78,7 +78,7 @@ class RemoteCall(Report):
|
|||||||
process_id(str): remote process id
|
process_id(str): remote process id
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if remote process is alive and False otherwise
|
bool: ``True`` in case if remote process is alive and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = self.client.make_request("GET", f"{self.client.address}/api/v1/service/process/{process_id}")
|
response = self.client.make_request("GET", f"{self.client.address}/api/v1/service/process/{process_id}")
|
||||||
|
@ -72,7 +72,7 @@ class Spawn(Thread, LazyLogging):
|
|||||||
value(bool): command line argument value
|
value(bool): command line argument value
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: if ``value`` is True, then returns positive flag and negative otherwise
|
str: if ``value`` is ``True``, then returns positive flag and negative otherwise
|
||||||
"""
|
"""
|
||||||
return name if value else f"no-{name}"
|
return name if value else f"no-{name}"
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ class Spawn(Thread, LazyLogging):
|
|||||||
process_id(str): process id to be checked as returned by :func:`_spawn_process()`
|
process_id(str): process id to be checked as returned by :func:`_spawn_process()`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if process still counts as active and False otherwise
|
bool: ``True`` in case if process still counts as active and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return process_id in self.active
|
return process_id in self.active
|
||||||
|
@ -25,6 +25,7 @@ from ahriman.core.database import SQLite
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@ -79,6 +80,37 @@ class Client:
|
|||||||
|
|
||||||
return make_local_client()
|
return make_local_client()
|
||||||
|
|
||||||
|
def event_add(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
create new event
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(Event): audit log event
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||||
|
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||||
|
"""
|
||||||
|
retrieve list of events
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(str | EventType | None): filter by event type
|
||||||
|
object_id(str | None): filter by event object
|
||||||
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Event]: list of audit log events
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def package_changes_get(self, package_base: str) -> Changes:
|
def package_changes_get(self, package_base: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
get package changes
|
get package changes
|
||||||
@ -184,7 +216,7 @@ class Client:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: not implemented method
|
NotImplementedError: not implemented method
|
||||||
@ -213,7 +245,7 @@ class Client:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base to update
|
package_base(str): package base to update
|
||||||
variable(str | None): patch name. If None set, all patches will be removed
|
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: not implemented method
|
NotImplementedError: not implemented method
|
||||||
|
@ -22,6 +22,7 @@ from ahriman.core.status import Client
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
@ -48,6 +49,31 @@ class LocalClient(Client):
|
|||||||
self.database = database
|
self.database = database
|
||||||
self.repository_id = repository_id
|
self.repository_id = repository_id
|
||||||
|
|
||||||
|
def event_add(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
create new event
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(Event): audit log event
|
||||||
|
"""
|
||||||
|
self.database.event_insert(event, self.repository_id)
|
||||||
|
|
||||||
|
def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||||
|
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||||
|
"""
|
||||||
|
retrieve list of events
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(str | EventType | None): filter by event type
|
||||||
|
object_id(str | None): filter by event object
|
||||||
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Event]: list of audit log events
|
||||||
|
"""
|
||||||
|
return self.database.event_get(event, object_id, limit, offset, self.repository_id)
|
||||||
|
|
||||||
def package_changes_get(self, package_base: str) -> Changes:
|
def package_changes_get(self, package_base: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
get package changes
|
get package changes
|
||||||
@ -138,7 +164,7 @@ class LocalClient(Client):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||||
"""
|
"""
|
||||||
self.database.logs_remove(package_base, version, self.repository_id)
|
self.database.logs_remove(package_base, version, self.repository_id)
|
||||||
|
|
||||||
@ -162,7 +188,7 @@ class LocalClient(Client):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base to update
|
package_base(str): package base to update
|
||||||
variable(str | None): patch name. If None set, all patches will be removed
|
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||||
"""
|
"""
|
||||||
variables = [variable] if variable is not None else None
|
variables = [variable] if variable is not None else None
|
||||||
self.database.patches_remove(package_base, variables)
|
self.database.patches_remove(package_base, variables)
|
||||||
|
@ -27,6 +27,7 @@ from ahriman.core.status import Client
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
@ -68,6 +69,10 @@ class Watcher(LazyLogging):
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return list(self._known.values())
|
return list(self._known.values())
|
||||||
|
|
||||||
|
event_add: Callable[[Event], None]
|
||||||
|
|
||||||
|
event_get: Callable[[str | EventType | None, str | None, int, int], list[Event]]
|
||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
"""
|
"""
|
||||||
load packages from local database
|
load packages from local database
|
||||||
|
@ -27,6 +27,7 @@ from ahriman.core.status import Client
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@ -109,6 +110,15 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
"""
|
"""
|
||||||
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/dependencies"
|
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/dependencies"
|
||||||
|
|
||||||
|
def _events_url(self) -> str:
|
||||||
|
"""
|
||||||
|
get url for the events api
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: full url for web service for events
|
||||||
|
"""
|
||||||
|
return f"{self.address}/api/v1/events"
|
||||||
|
|
||||||
def _logs_url(self, package_base: str) -> str:
|
def _logs_url(self, package_base: str) -> str:
|
||||||
"""
|
"""
|
||||||
get url for the logs api
|
get url for the logs api
|
||||||
@ -157,6 +167,44 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
"""
|
"""
|
||||||
return f"{self.address}/api/v1/status"
|
return f"{self.address}/api/v1/status"
|
||||||
|
|
||||||
|
def event_add(self, event: Event) -> None:
|
||||||
|
"""
|
||||||
|
create new event
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(Event): audit log event
|
||||||
|
"""
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.make_request("POST", self._events_url(), params=self.repository_id.query(), json=event.view())
|
||||||
|
|
||||||
|
def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||||
|
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||||
|
"""
|
||||||
|
retrieve list of events
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(str | EventType | None): filter by event type
|
||||||
|
object_id(str | None): filter by event object
|
||||||
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Event]: list of audit log events
|
||||||
|
"""
|
||||||
|
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||||
|
if event is not None:
|
||||||
|
query.append(("event", str(event)))
|
||||||
|
if object_id is not None:
|
||||||
|
query.append(("object_id", object_id))
|
||||||
|
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
response = self.make_request("GET", self._events_url(), params=query)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
return [Event.from_json(event) for event in response_json]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
def package_changes_get(self, package_base: str) -> Changes:
|
def package_changes_get(self, package_base: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
get package changes
|
get package changes
|
||||||
@ -274,8 +322,9 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
Returns:
|
Returns:
|
||||||
list[tuple[float, str]]: package logs
|
list[tuple[float, str]]: package logs
|
||||||
"""
|
"""
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||||
|
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
|
|
||||||
@ -289,12 +338,13 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||||
"""
|
"""
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
query = self.repository_id.query()
|
query = self.repository_id.query()
|
||||||
if version is not None:
|
if version is not None:
|
||||||
query += [("version", version)]
|
query += [("version", version)]
|
||||||
|
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
self.make_request("DELETE", self._logs_url(package_base), params=query)
|
self.make_request("DELETE", self._logs_url(package_base), params=query)
|
||||||
|
|
||||||
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||||
@ -323,7 +373,7 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base to update
|
package_base(str): package base to update
|
||||||
variable(str | None): patch name. If None set, all patches will be removed
|
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||||
"""
|
"""
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
self.make_request("DELETE", self._patches_url(package_base, variable or ""))
|
self.make_request("DELETE", self._patches_url(package_base, variable or ""))
|
||||||
@ -361,6 +411,7 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
NotImplementedError: not implemented method
|
NotImplementedError: not implemented method
|
||||||
"""
|
"""
|
||||||
payload = {"status": status.value}
|
payload = {"status": status.value}
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
self.make_request("POST", self._package_url(package_base),
|
self.make_request("POST", self._package_url(package_base),
|
||||||
params=self.repository_id.query(), json=payload)
|
params=self.repository_id.query(), json=payload)
|
||||||
@ -380,6 +431,7 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
"status": status.value,
|
"status": status.value,
|
||||||
"package": package.view(),
|
"package": package.view(),
|
||||||
}
|
}
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
self.make_request("POST", self._package_url(package.base),
|
self.make_request("POST", self._package_url(package.base),
|
||||||
params=self.repository_id.query(), json=payload)
|
params=self.repository_id.query(), json=payload)
|
||||||
@ -407,5 +459,6 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
status(BuildStatusEnum): current ahriman status
|
status(BuildStatusEnum): current ahriman status
|
||||||
"""
|
"""
|
||||||
payload = {"status": status.value}
|
payload = {"status": status.value}
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
self.make_request("POST", self._status_url(), params=self.repository_id.query(), json=payload)
|
self.make_request("POST", self._status_url(), params=self.repository_id.query(), json=payload)
|
||||||
|
@ -64,7 +64,7 @@ class Leaf:
|
|||||||
packages(Iterable[Leaf]): list of known leaves
|
packages(Iterable[Leaf]): list of known leaves
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if package is dependency of others and False otherwise
|
bool: ``True`` in case if package is dependency of others and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
for leaf in packages:
|
for leaf in packages:
|
||||||
if leaf.dependencies.intersection(self.items):
|
if leaf.dependencies.intersection(self.items):
|
||||||
@ -79,7 +79,7 @@ class Leaf:
|
|||||||
packages(Iterable[Leaf]): list of known leaves
|
packages(Iterable[Leaf]): list of known leaves
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if any of packages is dependency of the leaf, False otherwise
|
bool: ``True`` if any of packages is dependency of the leaf, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
for leaf in packages:
|
for leaf in packages:
|
||||||
if self.dependencies.intersection(leaf.items):
|
if self.dependencies.intersection(leaf.items):
|
||||||
|
@ -160,7 +160,7 @@ class GitHub(Upload, HttpUpload):
|
|||||||
get release object if any
|
get release object if any
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict[str, Any] | None: GitHub API release object if release found and None otherwise
|
dict[str, Any] | None: GitHub API release object if release found and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
url = f"https://api.github.com/repos/{self.github_owner}/{
|
url = f"https://api.github.com/repos/{self.github_owner}/{
|
||||||
self.github_repository}/releases/tags/{self.github_release_tag}"
|
self.github_repository}/releases/tags/{self.github_release_tag}"
|
||||||
|
@ -225,7 +225,7 @@ def extract_user() -> str | None:
|
|||||||
extract user from system environment
|
extract user from system environment
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: SUDO_USER in case if set and USER otherwise. It can return None in case if environment has been
|
str | None: SUDO_USER in case if set and USER otherwise. It can return ``None`` in case if environment has been
|
||||||
cleared before application start
|
cleared before application start
|
||||||
"""
|
"""
|
||||||
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
|
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
|
||||||
@ -295,7 +295,7 @@ def package_like(filename: Path) -> bool:
|
|||||||
filename(Path): name of file to check
|
filename(Path): name of file to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if name contains ``.pkg.`` and not signature, False otherwise
|
bool: ``True`` in case if name contains ``.pkg.`` and not signature, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
name = filename.name
|
name = filename.name
|
||||||
return not name.startswith(".") and ".pkg." in name and not name.endswith(".sig")
|
return not name.startswith(".") and ".pkg." in name and not name.endswith(".sig")
|
||||||
|
@ -44,7 +44,7 @@ class AuthSettings(StrEnum):
|
|||||||
get enabled flag
|
get enabled flag
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: False in case if authorization is disabled and True otherwise
|
bool: ``False`` in case if authorization is disabled and ``True`` otherwise
|
||||||
"""
|
"""
|
||||||
return self != AuthSettings.Disabled
|
return self != AuthSettings.Disabled
|
||||||
|
|
||||||
|
92
src/ahriman/models/event.py
Normal file
92
src/ahriman/models/event.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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 dataclasses import dataclass, field, fields
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Any, Self
|
||||||
|
|
||||||
|
from ahriman.core.utils import dataclass_view, filter_json, utcnow
|
||||||
|
|
||||||
|
|
||||||
|
class EventType(StrEnum):
|
||||||
|
"""
|
||||||
|
predefined event types
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
PackageOutdated(EventType): (class attribute) package has been marked as out-of-date
|
||||||
|
PackageRemoved(EventType): (class attribute) package has been removed
|
||||||
|
PackageUpdateFailed(EventType): (class attribute) package update has been failed
|
||||||
|
PackageUpdated(EventType): (class attribute) package has been updated
|
||||||
|
"""
|
||||||
|
|
||||||
|
PackageOutdated = "package-outdated"
|
||||||
|
PackageRemoved = "package-removed"
|
||||||
|
PackageUpdateFailed = "package-update-failed"
|
||||||
|
PackageUpdated = "package-updated"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Event:
|
||||||
|
"""
|
||||||
|
audit log event
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
created(int): event timestamp
|
||||||
|
data(dict[str, Any]): event metadata
|
||||||
|
event(str | EventType): event type
|
||||||
|
message(str | None): event message if available
|
||||||
|
object_id(str): object identifier
|
||||||
|
"""
|
||||||
|
|
||||||
|
event: str | EventType
|
||||||
|
object_id: str
|
||||||
|
message: str | None = None
|
||||||
|
data: dict[str, Any] = field(default_factory=dict)
|
||||||
|
created: int = field(default_factory=lambda: int(utcnow().timestamp()))
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""
|
||||||
|
convert event type to enum if it is a well-known event type
|
||||||
|
"""
|
||||||
|
if self.event in EventType:
|
||||||
|
object.__setattr__(self, "event", EventType(self.event))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||||
|
"""
|
||||||
|
construct event from the json dump
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dump(dict[str, Any]): json dump body
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: dependencies object
|
||||||
|
"""
|
||||||
|
# filter to only known fields
|
||||||
|
known_fields = [pair.name for pair in fields(cls)]
|
||||||
|
return cls(**filter_json(dump, known_fields))
|
||||||
|
|
||||||
|
def view(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
generate json event view
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: json-friendly dictionary
|
||||||
|
"""
|
||||||
|
return dataclass_view(self)
|
@ -41,7 +41,7 @@ class MigrationResult:
|
|||||||
check migration and check if there are pending migrations
|
check migration and check if there are pending migrations
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if it requires migrations and False otherwise
|
bool: ``True`` in case if it requires migrations and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
self.validate()
|
self.validate()
|
||||||
return self.new_version > self.old_version
|
return self.new_version > self.old_version
|
||||||
|
@ -158,7 +158,7 @@ class Package(LazyLogging):
|
|||||||
get VCS flag based on the package base
|
get VCS flag based on the package base
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if package base looks like VCS package and False otherwise
|
bool: ``True`` in case if package base looks like VCS package and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.base.endswith("-bzr") \
|
return self.base.endswith("-bzr") \
|
||||||
or self.base.endswith("-csv")\
|
or self.base.endswith("-csv")\
|
||||||
@ -504,7 +504,7 @@ class Package(LazyLogging):
|
|||||||
timestamp(float | int): timestamp to check build date against
|
timestamp(float | int): timestamp to check build date against
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if package was built after the specified date and False otherwise. In case if build date
|
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise. In case if build date
|
||||||
is not set by any of packages, it returns False
|
is not set by any of packages, it returns False
|
||||||
"""
|
"""
|
||||||
return any(
|
return any(
|
||||||
@ -528,7 +528,7 @@ class Package(LazyLogging):
|
|||||||
(Default value = True)
|
(Default value = True)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the package is out-of-dated and False otherwise
|
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
|
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
|
||||||
if calculate_version and not self.is_newer_than(min_vcs_build_date):
|
if calculate_version and not self.is_newer_than(min_vcs_build_date):
|
||||||
|
@ -55,7 +55,7 @@ class PkgbuildPatch:
|
|||||||
parse key and define whether it function or not
|
parse key and define whether it function or not
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if key ends with parentheses and False otherwise
|
bool: ``True`` in case if key ends with parentheses and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.key is not None and self.key.endswith("()")
|
return self.key is not None and self.key.endswith("()")
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class PkgbuildPatch:
|
|||||||
check if patch is full diff one or just single-variable patch
|
check if patch is full diff one or just single-variable patch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case key set and False otherwise
|
bool: ``True`` in case key set and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.key is None
|
return self.key is None
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class Property:
|
|||||||
Attributes:
|
Attributes:
|
||||||
name(str): name of the property
|
name(str): name of the property
|
||||||
value(Any): property value
|
value(Any): property value
|
||||||
is_required(bool): if set to True then this property is required
|
is_required(bool): if set to ``True`` then this property is required
|
||||||
indent(int): property indentation level
|
indent(int): property indentation level
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class RemoteSource:
|
|||||||
check if source is remote
|
check if source is remote
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if package is well-known remote source (e.g. AUR) and False otherwise
|
bool: ``True`` in case if package is well-known remote source (e.g. AUR) and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.source in (PackageSource.AUR, PackageSource.Repository)
|
return self.source in (PackageSource.AUR, PackageSource.Repository)
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class RepositoryId:
|
|||||||
check if all data is supplied for the loading
|
check if all data is supplied for the loading
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if architecture or name are not set and False otherwise
|
bool: ``True`` in case if architecture or name are not set and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return not self.architecture or not self.name
|
return not self.architecture or not self.name
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ class RepositoryId:
|
|||||||
other(Any): other object to compare
|
other(Any): other object to compare
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if this is less than other and False otherwise
|
bool: ``True`` in case if this is less than other and ``False`` otherwise
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TypeError: if other is different from RepositoryId type
|
TypeError: if other is different from RepositoryId type
|
||||||
|
@ -82,7 +82,7 @@ class Result:
|
|||||||
get if build result is empty or not
|
get if build result is empty or not
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if success list is empty and False otherwise
|
bool: ``True`` in case if success list is empty and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return not self._added and not self._updated
|
return not self._added and not self._updated
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ class Result:
|
|||||||
other(Any): other object instance
|
other(Any): other object instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the other object is the same and False otherwise
|
bool: ``True`` if the other object is the same and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if not isinstance(other, Result):
|
if not isinstance(other, Result):
|
||||||
return False
|
return False
|
||||||
|
@ -98,7 +98,7 @@ class User:
|
|||||||
salt(str): salt for hashed password
|
salt(str): salt for hashed password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
verified: bool = self._HASHER.verify(password + salt, self.password)
|
verified: bool = self._HASHER.verify(password + salt, self.password)
|
||||||
@ -131,7 +131,7 @@ class User:
|
|||||||
required(UserAccess): required access level
|
required(UserAccess): required access level
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.access.permits(required)
|
return self.access.permits(required)
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class UserAccess(StrEnum):
|
|||||||
other(UserAccess): other permission to compare
|
other(UserAccess): other permission to compare
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if current permission allows the operation and False otherwise
|
bool: ``True`` in case if current permission allows the operation and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
for member in UserAccess:
|
for member in UserAccess:
|
||||||
if member == other:
|
if member == other:
|
||||||
|
@ -63,7 +63,7 @@ class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
|
|||||||
identity(str): username
|
identity(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: user identity (username) in case if user exists and None otherwise
|
str | None: user identity (username) in case if user exists and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return identity if await self.validator.known_username(identity) else None
|
return identity if await self.validator.known_username(identity) else None
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
|
|||||||
context(str | None, optional): URI request path (Default value = None)
|
context(str | None, optional): URI request path (Default value = None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to perform this request and False otherwise
|
bool: ``True`` in case if user is allowed to perform this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
# some methods for type checking and parent class compatibility
|
# some methods for type checking and parent class compatibility
|
||||||
if identity is None or not isinstance(permission, UserAccess):
|
if identity is None or not isinstance(permission, UserAccess):
|
||||||
|
@ -38,7 +38,7 @@ def _is_templated_unauthorized(request: Request) -> bool:
|
|||||||
request(Request): source request to check
|
request(Request): source request to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if response should be rendered as html and False otherwise
|
bool: ``True`` in case if response should be rendered as html and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return request.path in ("/api/v1/login", "/api/v1/logout") \
|
return request.path in ("/api/v1/login", "/api/v1/logout") \
|
||||||
and "application/json" not in request.headers.getall("accept", [])
|
and "application/json" not in request.headers.getall("accept", [])
|
||||||
|
@ -24,6 +24,8 @@ from ahriman.web.schemas.changes_schema import ChangesSchema
|
|||||||
from ahriman.web.schemas.counters_schema import CountersSchema
|
from ahriman.web.schemas.counters_schema import CountersSchema
|
||||||
from ahriman.web.schemas.dependencies_schema import DependenciesSchema
|
from ahriman.web.schemas.dependencies_schema import DependenciesSchema
|
||||||
from ahriman.web.schemas.error_schema import ErrorSchema
|
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||||
|
from ahriman.web.schemas.event_schema import EventSchema
|
||||||
|
from ahriman.web.schemas.event_search_schema import EventSearchSchema
|
||||||
from ahriman.web.schemas.file_schema import FileSchema
|
from ahriman.web.schemas.file_schema import FileSchema
|
||||||
from ahriman.web.schemas.info_schema import InfoSchema
|
from ahriman.web.schemas.info_schema import InfoSchema
|
||||||
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||||
|
47
src/ahriman/web/schemas/event_schema.py
Normal file
47
src/ahriman/web/schemas/event_schema.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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 marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.models.event import EventType
|
||||||
|
|
||||||
|
|
||||||
|
class EventSchema(Schema):
|
||||||
|
"""
|
||||||
|
request/response event schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
created = fields.Integer(required=True, metadata={
|
||||||
|
"description": "Event creation timestamp",
|
||||||
|
"example": 1680537091,
|
||||||
|
})
|
||||||
|
event = fields.String(required=True, metadata={
|
||||||
|
"description": "Event type",
|
||||||
|
"example": EventType.PackageUpdated,
|
||||||
|
})
|
||||||
|
object_id = fields.String(required=True, metadata={
|
||||||
|
"description": "Event object identifier",
|
||||||
|
"example": "ahriman",
|
||||||
|
})
|
||||||
|
message = fields.String(metadata={
|
||||||
|
"description": "Event message if available",
|
||||||
|
})
|
||||||
|
data = fields.Dict(keys=fields.String(), metadata={
|
||||||
|
"description": "Event metadata if available",
|
||||||
|
})
|
38
src/ahriman/web/schemas/event_search_schema.py
Normal file
38
src/ahriman/web/schemas/event_search_schema.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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 marshmallow import fields
|
||||||
|
|
||||||
|
from ahriman.models.event import EventType
|
||||||
|
from ahriman.web.schemas.pagination_schema import PaginationSchema
|
||||||
|
|
||||||
|
|
||||||
|
class EventSearchSchema(PaginationSchema):
|
||||||
|
"""
|
||||||
|
request event search schema
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = fields.String(metadata={
|
||||||
|
"description": "Event type",
|
||||||
|
"example": EventType.PackageUpdated,
|
||||||
|
})
|
||||||
|
object_id = fields.String(metadata={
|
||||||
|
"description": "Event object identifier",
|
||||||
|
"example": "ahriman",
|
||||||
|
})
|
@ -247,7 +247,7 @@ class BaseView(View, CorsViewMixin):
|
|||||||
extract username from request if any
|
extract username from request if any
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: authorized username if any and None otherwise (e.g. if authorization is disabled)
|
str | None: authorized username if any and ``None`` otherwise (e.g. if authorization is disabled)
|
||||||
"""
|
"""
|
||||||
try: # try to read from payload
|
try: # try to read from payload
|
||||||
data: dict[str, str] = await self.request.json() # technically it is not, but we only need str here
|
data: dict[str, str] = await self.request.json() # technically it is not, but we only need str here
|
||||||
|
19
src/ahriman/web/views/v1/auditlog/__init__.py
Normal file
19
src/ahriman/web/views/v1/auditlog/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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/>.
|
||||||
|
#
|
104
src/ahriman/web/views/v1/auditlog/events.py
Normal file
104
src/ahriman/web/views/v1/auditlog/events.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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/>.
|
||||||
|
#
|
||||||
|
import aiohttp_apispec # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||||
|
|
||||||
|
from ahriman.models.event import Event
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.schemas import AuthSchema, ErrorSchema, EventSchema, EventSearchSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class EventsView(BaseView):
|
||||||
|
"""
|
||||||
|
audit log view
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
|
"""
|
||||||
|
|
||||||
|
GET_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||||
|
ROUTES = ["/api/v1/events"]
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Audit log"],
|
||||||
|
summary="Get events",
|
||||||
|
description="Retrieve events from audit log",
|
||||||
|
responses={
|
||||||
|
200: {"description": "Success response", "schema": EventSchema(many=True)},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [GET_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.querystring_schema(EventSearchSchema)
|
||||||
|
async def get(self) -> Response:
|
||||||
|
"""
|
||||||
|
get events list
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: 200 with workers list on success
|
||||||
|
"""
|
||||||
|
limit, offset = self.page()
|
||||||
|
event = self.request.query.get("event") or None
|
||||||
|
object_id = self.request.query.get("object_id") or None
|
||||||
|
|
||||||
|
events = self.service().event_get(event, object_id, limit, offset)
|
||||||
|
response = [event.view() for event in events]
|
||||||
|
|
||||||
|
return json_response(response)
|
||||||
|
|
||||||
|
@aiohttp_apispec.docs(
|
||||||
|
tags=["Audit log"],
|
||||||
|
summary="Create event",
|
||||||
|
description="Add new event to the audit log",
|
||||||
|
responses={
|
||||||
|
204: {"description": "Success response"},
|
||||||
|
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||||
|
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||||
|
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||||
|
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||||
|
},
|
||||||
|
security=[{"token": [POST_PERMISSION]}],
|
||||||
|
)
|
||||||
|
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||||
|
@aiohttp_apispec.json_schema(EventSchema)
|
||||||
|
async def post(self) -> None:
|
||||||
|
"""
|
||||||
|
add new audit log event
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPBadRequest: if bad data is supplied
|
||||||
|
HTTPNoContent: in case of success response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = await self.request.json()
|
||||||
|
event = Event.from_json(data)
|
||||||
|
except Exception as ex:
|
||||||
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
|
self.service().event_add(event)
|
||||||
|
|
||||||
|
raise HTTPNoContent
|
@ -0,0 +1,8 @@
|
|||||||
|
from ahriman.core.database.migrations.m014_auditlog import steps
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_auditlog() -> None:
|
||||||
|
"""
|
||||||
|
migration must not be empty
|
||||||
|
"""
|
||||||
|
assert steps
|
@ -0,0 +1,40 @@
|
|||||||
|
from ahriman.core.database import SQLite
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must insert and get event
|
||||||
|
"""
|
||||||
|
event = Event(EventType.PackageUpdated, package_ahriman.base, "Updated", {"key": "value"})
|
||||||
|
database.event_insert(event)
|
||||||
|
assert database.event_get() == [event]
|
||||||
|
|
||||||
|
event2 = Event("event", "object")
|
||||||
|
database.event_insert(event2, RepositoryId("i686", database._repository_id.name))
|
||||||
|
assert database.event_get() == [event]
|
||||||
|
assert database.event_get(repository_id=RepositoryId("i686", database._repository_id.name)) == [event2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_insert_get_filter(database: SQLite) -> None:
|
||||||
|
"""
|
||||||
|
must insert and get events with filter
|
||||||
|
"""
|
||||||
|
database.event_insert(Event("event 1", "object 1", created=1))
|
||||||
|
database.event_insert(Event("event 2", "object 2"))
|
||||||
|
database.event_insert(Event(EventType.PackageUpdated, "package"))
|
||||||
|
|
||||||
|
assert database.event_get(event="event 1") == [Event("event 1", "object 1", created=1)]
|
||||||
|
assert database.event_get(object_id="object 1") == [Event("event 1", "object 1", created=1)]
|
||||||
|
assert all(event.event == EventType.PackageUpdated for event in database.event_get(event=EventType.PackageUpdated))
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_insert_get_pagination(database: SQLite) -> None:
|
||||||
|
"""
|
||||||
|
must insert and get events with pagination
|
||||||
|
"""
|
||||||
|
database.event_insert(Event("1", "1"))
|
||||||
|
database.event_insert(Event("2", "2"))
|
||||||
|
assert all(event.event == "2" for event in database.event_get(limit=1, offset=1))
|
@ -11,6 +11,7 @@ from ahriman.core.status.web_client import WebClient
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@ -94,6 +95,22 @@ def test_load_web_client_from_legacy_unix_socket(configuration: Configuration, d
|
|||||||
assert isinstance(Client.load(repository_id, configuration, database, report=True), WebClient)
|
assert isinstance(Client.load(repository_id, configuration, database, report=True), WebClient)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add(client: Client) -> None:
|
||||||
|
"""
|
||||||
|
must raise not implemented on event insertion
|
||||||
|
"""
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
client.event_add(Event("", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get(client: Client) -> None:
|
||||||
|
"""
|
||||||
|
must raise not implemented on events request
|
||||||
|
"""
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
client.event_get(None, None)
|
||||||
|
|
||||||
|
|
||||||
def test_package_changes_get(client: Client, package_ahriman: Package) -> None:
|
def test_package_changes_get(client: Client, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must raise not implemented on package changes request
|
must raise not implemented on package changes request
|
||||||
|
@ -7,11 +7,32 @@ from ahriman.core.status.local_client import LocalClient
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must add new event
|
||||||
|
"""
|
||||||
|
event_mock = mocker.patch("ahriman.core.database.SQLite.event_insert")
|
||||||
|
event = Event(EventType.PackageUpdated, package_ahriman.base)
|
||||||
|
|
||||||
|
local_client.event_add(event)
|
||||||
|
event_mock.assert_called_once_with(event, local_client.repository_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must retrieve events
|
||||||
|
"""
|
||||||
|
event_mock = mocker.patch("ahriman.core.database.SQLite.event_get")
|
||||||
|
local_client.event_get(EventType.PackageUpdated, package_ahriman.base, 1, 2)
|
||||||
|
event_mock.assert_called_once_with(EventType.PackageUpdated, package_ahriman.base, 1, 2, local_client.repository_id)
|
||||||
|
|
||||||
|
|
||||||
def test_package_changes_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_changes_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must retrieve package changes
|
must retrieve package changes
|
||||||
|
@ -10,6 +10,7 @@ from ahriman.core.status.web_client import WebClient
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.dependencies import Dependencies
|
from ahriman.models.dependencies import Dependencies
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@ -54,18 +55,12 @@ def test_dependencies_url(web_client: WebClient, package_ahriman: Package) -> No
|
|||||||
"/api/v1/packages/some%2Fpackage%25name/dependencies")
|
"/api/v1/packages/some%2Fpackage%25name/dependencies")
|
||||||
|
|
||||||
|
|
||||||
def test__patches_url(web_client: WebClient, package_ahriman: Package) -> None:
|
def test_event_url(web_client: WebClient) -> None:
|
||||||
"""
|
"""
|
||||||
must generate changes url correctly
|
must generate audit log url correctly
|
||||||
"""
|
"""
|
||||||
assert web_client._patches_url(package_ahriman.base).startswith(web_client.address)
|
assert web_client._events_url().startswith(web_client.address)
|
||||||
assert web_client._patches_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/patches")
|
assert web_client._events_url().endswith("/api/v1/events")
|
||||||
assert web_client._patches_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name/patches")
|
|
||||||
|
|
||||||
assert web_client._patches_url(package_ahriman.base, "var").endswith(
|
|
||||||
f"/api/v1/packages/{package_ahriman.base}/patches/var")
|
|
||||||
assert web_client._patches_url(package_ahriman.base, "some/variable%name").endswith(
|
|
||||||
f"/api/v1/packages/{package_ahriman.base}/patches/some%2Fvariable%25name")
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None:
|
def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||||
@ -89,6 +84,20 @@ def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
|
|||||||
assert web_client._package_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name")
|
assert web_client._package_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name")
|
||||||
|
|
||||||
|
|
||||||
|
def test_patches_url(web_client: WebClient, package_ahriman: Package) -> None:
|
||||||
|
"""
|
||||||
|
must generate changes url correctly
|
||||||
|
"""
|
||||||
|
assert web_client._patches_url(package_ahriman.base).startswith(web_client.address)
|
||||||
|
assert web_client._patches_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/patches")
|
||||||
|
assert web_client._patches_url("some/package%name").endswith("/api/v1/packages/some%2Fpackage%25name/patches")
|
||||||
|
|
||||||
|
assert web_client._patches_url(package_ahriman.base, "var").endswith(
|
||||||
|
f"/api/v1/packages/{package_ahriman.base}/patches/var")
|
||||||
|
assert web_client._patches_url(package_ahriman.base, "some/variable%name").endswith(
|
||||||
|
f"/api/v1/packages/{package_ahriman.base}/patches/some%2Fvariable%25name")
|
||||||
|
|
||||||
|
|
||||||
def test_status_url(web_client: WebClient) -> None:
|
def test_status_url(web_client: WebClient) -> None:
|
||||||
"""
|
"""
|
||||||
must generate package status url correctly
|
must generate package status url correctly
|
||||||
@ -97,6 +106,135 @@ def test_status_url(web_client: WebClient) -> None:
|
|||||||
assert web_client._status_url().endswith("/api/v1/status")
|
assert web_client._status_url().endswith("/api/v1/status")
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create event
|
||||||
|
"""
|
||||||
|
event = Event(EventType.PackageUpdated, package_ahriman.base)
|
||||||
|
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
||||||
|
|
||||||
|
web_client.event_add(event)
|
||||||
|
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True),
|
||||||
|
params=web_client.repository_id.query(), json=event.view())
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress any exception happened during events creation
|
||||||
|
"""
|
||||||
|
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||||
|
web_client.event_add(Event("", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress HTTP exception happened during events creation
|
||||||
|
"""
|
||||||
|
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||||
|
web_client.event_add(Event("", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add_failed_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress any exception happened during events creaton and don't log
|
||||||
|
"""
|
||||||
|
web_client.suppress_errors = True
|
||||||
|
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||||
|
logging_mock = mocker.patch("logging.exception")
|
||||||
|
|
||||||
|
web_client.event_add(Event("", ""))
|
||||||
|
logging_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add_failed_http_error_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress HTTP exception happened during events creation and don't log
|
||||||
|
"""
|
||||||
|
web_client.suppress_errors = True
|
||||||
|
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||||
|
logging_mock = mocker.patch("logging.exception")
|
||||||
|
|
||||||
|
web_client.event_add(Event("", ""))
|
||||||
|
logging_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must get events
|
||||||
|
"""
|
||||||
|
event = Event(EventType.PackageUpdated, package_ahriman.base)
|
||||||
|
response_obj = requests.Response()
|
||||||
|
response_obj._content = json.dumps([event.view()]).encode("utf8")
|
||||||
|
response_obj.status_code = 200
|
||||||
|
|
||||||
|
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
|
||||||
|
|
||||||
|
result = web_client.event_get(None, None)
|
||||||
|
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||||
|
params=web_client.repository_id.query() + [("limit", "-1"), ("offset", "0")])
|
||||||
|
assert result == [event]
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get_filter(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must get events with filter
|
||||||
|
"""
|
||||||
|
response_obj = requests.Response()
|
||||||
|
response_obj._content = json.dumps(Event("", "").view()).encode("utf8")
|
||||||
|
response_obj.status_code = 200
|
||||||
|
|
||||||
|
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=response_obj)
|
||||||
|
|
||||||
|
web_client.event_get("event", "object", 1, 2)
|
||||||
|
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||||
|
params=web_client.repository_id.query() + [
|
||||||
|
("limit", "1"),
|
||||||
|
("offset", "2"),
|
||||||
|
("event", "event"),
|
||||||
|
("object_id", "object"),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress any exception happened during events fetch
|
||||||
|
"""
|
||||||
|
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||||
|
web_client.event_get(None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress HTTP exception happened during events fetch
|
||||||
|
"""
|
||||||
|
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||||
|
web_client.event_get(None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get_failed_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress any exception happened during events fetch and don't log
|
||||||
|
"""
|
||||||
|
web_client.suppress_errors = True
|
||||||
|
mocker.patch("requests.Session.request", side_effect=Exception())
|
||||||
|
logging_mock = mocker.patch("logging.exception")
|
||||||
|
|
||||||
|
web_client.event_get(None, None)
|
||||||
|
logging_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_get_failed_http_error_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must suppress HTTP exception happened during events fetch and don't log
|
||||||
|
"""
|
||||||
|
web_client.suppress_errors = True
|
||||||
|
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
||||||
|
logging_mock = mocker.patch("logging.exception")
|
||||||
|
|
||||||
|
web_client.event_get(None, None)
|
||||||
|
logging_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_package_changes_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_changes_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must get changes
|
must get changes
|
||||||
|
17
tests/ahriman/models/test_event.py
Normal file
17
tests/ahriman/models/test_event.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from ahriman.models.event import Event, EventType
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_init() -> None:
|
||||||
|
"""
|
||||||
|
must replace event type for known types
|
||||||
|
"""
|
||||||
|
assert Event("random", "")
|
||||||
|
assert isinstance(Event(str(EventType.PackageUpdated), "").event, EventType)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_json_view() -> None:
|
||||||
|
"""
|
||||||
|
must construct and serialize event to json
|
||||||
|
"""
|
||||||
|
event = Event("event", "object", "message", {"key": "value"})
|
||||||
|
assert Event.from_json(event.view()) == event
|
1
tests/ahriman/web/schemas/test_event_schema.py
Normal file
1
tests/ahriman/web/schemas/test_event_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
1
tests/ahriman/web/schemas/test_event_search_schema.py
Normal file
1
tests/ahriman/web/schemas/test_event_search_schema.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# schema testing goes in view class tests
|
@ -0,0 +1,104 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
from ahriman.models.event import Event
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
from ahriman.web.views.v1.auditlog.events import EventsView
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_permission() -> None:
|
||||||
|
"""
|
||||||
|
must return correct permission for the request
|
||||||
|
"""
|
||||||
|
for method in ("GET", "POST"):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await EventsView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
|
def test_routes() -> None:
|
||||||
|
"""
|
||||||
|
must return correct routes
|
||||||
|
"""
|
||||||
|
assert EventsView.ROUTES == ["/api/v1/events"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must return all events
|
||||||
|
"""
|
||||||
|
event1 = Event("event1", "object1", "message", {"key": "value"})
|
||||||
|
event2 = Event("event2", "object2")
|
||||||
|
await client.post("/api/v1/events", json=event1.view())
|
||||||
|
await client.post("/api/v1/events", json=event2.view())
|
||||||
|
response_schema = pytest.helpers.schema_response(EventsView.get)
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/events")
|
||||||
|
assert response.ok
|
||||||
|
json = await response.json()
|
||||||
|
assert not response_schema.validate(json, many=True)
|
||||||
|
|
||||||
|
events = [Event.from_json(event) for event in json]
|
||||||
|
assert events == [event1, event2]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_with_pagination(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must get events with pagination
|
||||||
|
"""
|
||||||
|
event1 = Event("event1", "object1", "message", {"key": "value"})
|
||||||
|
event2 = Event("event2", "object2")
|
||||||
|
await client.post("/api/v1/events", json=event1.view())
|
||||||
|
await client.post("/api/v1/events", json=event2.view())
|
||||||
|
request_schema = pytest.helpers.schema_request(EventsView.get, location="querystring")
|
||||||
|
response_schema = pytest.helpers.schema_response(EventsView.get)
|
||||||
|
|
||||||
|
payload = {"limit": 1, "offset": 1}
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
response = await client.get("/api/v1/events", params=payload)
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
json = await response.json()
|
||||||
|
assert not response_schema.validate(json, many=True)
|
||||||
|
|
||||||
|
assert [Event.from_json(event) for event in json] == [event2]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_bad_request(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must return bad request for invalid query parameters
|
||||||
|
"""
|
||||||
|
response_schema = pytest.helpers.schema_response(EventsView.get, code=400)
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/events", params={"limit": "limit"})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
response = await client.get("/api/v1/events", params={"offset": "offset"})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must create event
|
||||||
|
"""
|
||||||
|
event = Event("event1", "object1", "message", {"key": "value"})
|
||||||
|
request_schema = pytest.helpers.schema_request(EventsView.post)
|
||||||
|
|
||||||
|
payload = event.view()
|
||||||
|
assert not request_schema.validate(payload)
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/events", json=payload)
|
||||||
|
assert response.status == 204
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_exception(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must raise exception on invalid payload
|
||||||
|
"""
|
||||||
|
response_schema = pytest.helpers.schema_response(EventsView.post, code=400)
|
||||||
|
|
||||||
|
response = await client.post("/api/v1/events", json={})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not response_schema.validate(await response.json())
|
1
tox.ini
1
tox.ini
@ -11,6 +11,7 @@ flags = --implicit-reexport --strict --allow-untyped-decorators --allow-subclass
|
|||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
|
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
asyncio_mode = auto
|
asyncio_mode = auto
|
||||||
spec_test_format = {result} {docstring_summary}
|
spec_test_format = {result} {docstring_summary}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user