Compare commits

...

13 Commits
1.3.0 ... 1.4.1

Author SHA1 Message Date
db195391e4 Release 1.4.1 2021-10-03 01:02:41 +03:00
59f2992559 do not use set_defaults for architecture arguments
according to the source code defaults always updates the values
dictionary. This in this specific case it is impossible to override the
value it will be always empty list.

In order to handle it we are adding another property to the Handler
class which allows to run with None architecture list.

This particular set_defaults behaviour is still useful for other cases
when we have to run command without any specific architecture
2021-10-03 00:59:24 +03:00
4f06647193 Release 1.4.0 2021-10-01 09:25:35 +03:00
73a4cee257 add package request endpoint 2021-10-01 08:58:50 +03:00
13d00c6f66 docs update 2021-09-26 14:39:35 +03:00
3e032c3515 add index url 2021-09-26 14:29:42 +03:00
d73d5daad3 add debugtoolbar support 2021-09-26 12:31:12 +03:00
f55b44b391 set both value and innerText during search
current implementation just adds package name with the description which
is incorrect
2021-09-26 10:34:21 +03:00
51b28baf40 add ability to specify package source explicitly during the addition 2021-09-26 09:55:14 +03:00
24326f9753 define permissions in views directly 2021-09-25 17:03:46 +03:00
36c763069d only set file rights if requested 2021-09-23 20:36:22 +03:00
c9a155bbc4 raise httpexception instead of returning it from a function 2021-09-19 14:28:24 +03:00
182bde5e09 add manpage 2021-09-19 13:56:11 +03:00
80 changed files with 3211 additions and 1921 deletions

2
.gitignore vendored
View File

@ -94,5 +94,3 @@ ENV/
.venv/
*.tar.xz
man/

View File

@ -1,4 +1,4 @@
.PHONY: architecture archive archive_directory archlinux check clean directory push tests version
.PHONY: architecture archive archive_directory archlinux check clean directory man push tests version
.DEFAULT_GOAL := archlinux
PROJECT := ahriman
@ -39,12 +39,15 @@ clean:
directory: clean
mkdir "$(PROJECT)"
man:
cd src && PYTHONPATH=. argparse-manpage --module ahriman.application.ahriman --function _parser --author "ahriman team" --project-name ahriman --author-email "" --url https://github.com/arcan1s/ahriman --output ../docs/ahriman.1
mypy:
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" --install-types --non-interactive || true
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
push: archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py
push: architecture man archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py docs/ahriman-architecture.svg docs/ahriman.1
git commit -m "Release $(VERSION)"
git tag "$(VERSION)"
git push

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 326 KiB

401
docs/ahriman.1 Normal file
View File

@ -0,0 +1,401 @@
.TH ahriman "1" Manual
.SH NAME
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-log] [--no-report] [--unsafe] [-v] {add,check,clean,config,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,user,web} ...
.SH DESCRIPTION
ArcH Linux ReposItory MANager
.SH OPTIONS
.TP
\fB\-a\fR \fI\,ARCHITECTURE\/\fR, \fB\-\-architecture\fR \fI\,ARCHITECTURE\/\fR
target architectures (can be used multiple times)
.TP
\fB\-c\fR \fI\,CONFIGURATION\/\fR, \fB\-\-configuration\fR \fI\,CONFIGURATION\/\fR
configuration path
.TP
\fB\-\-force\fR
force run, remove file lock
.TP
\fB\-l\fR \fI\,LOCK\/\fR, \fB\-\-lock\fR \fI\,LOCK\/\fR
lock file
.TP
\fB\-\-no\-log\fR
redirect all log messages to stderr
.TP
\fB\-\-no\-report\fR
force disable reporting to web service
.TP
\fB\-\-unsafe\fR
allow to run ahriman as non\-ahriman user
.TP
\fB\-v\fR, \fB\-\-version\fR
show program's version number and exit
.SS
\fBSub-commands\fR
.TP
\fBahriman\fR \fI\,add\/\fR
add package
.TP
\fBahriman\fR \fI\,check\/\fR
check for updates
.TP
\fBahriman\fR \fI\,clean\/\fR
clean local caches
.TP
\fBahriman\fR \fI\,config\/\fR
dump configuration
.TP
\fBahriman\fR \fI\,init\/\fR
create repository tree
.TP
\fBahriman\fR \fI\,key-import\/\fR
import PGP key
.TP
\fBahriman\fR \fI\,rebuild\/\fR
rebuild repository
.TP
\fBahriman\fR \fI\,remove\/\fR
remove package
.TP
\fBahriman\fR \fI\,remove-unknown\/\fR
remove unknown packages
.TP
\fBahriman\fR \fI\,report\/\fR
generate report
.TP
\fBahriman\fR \fI\,search\/\fR
search for package
.TP
\fBahriman\fR \fI\,setup\/\fR
initial service configuration
.TP
\fBahriman\fR \fI\,sign\/\fR
sign packages
.TP
\fBahriman\fR \fI\,status\/\fR
get package status
.TP
\fBahriman\fR \fI\,status-update\/\fR
update package status
.TP
\fBahriman\fR \fI\,sync\/\fR
sync repository
.TP
\fBahriman\fR \fI\,update\/\fR
update packages
.TP
\fBahriman\fR \fI\,user\/\fR
manage users for web services
.TP
\fBahriman\fR \fI\,web\/\fR
start web server
.SH OPTIONS 'ahriman add'
usage: ahriman add [-h] [--now] [--source {PackageSource.Auto,PackageSource.Archive,PackageSource.Directory,PackageSource.AUR}] [--without-dependencies] package [package ...]
add package
.TP
\fBpackage\fR
package base/name or archive path
.TP
\fB\-\-now\fR
run update function after
.TP
\fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.Directory,PackageSource.AUR}
package source
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman check'
usage: ahriman check [-h] [--no-vcs] [package ...]
check for updates. Same as update \-\-dry\-run \-\-no\-manual
.TP
\fBpackage\fR
filter check by package base
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman clean'
usage: ahriman clean [-h] [--no-build] [--no-cache] [--no-chroot] [--no-manual] [--no-packages]
clear local caches
.TP
\fB\-\-no\-build\fR
do not clear directory with package sources
.TP
\fB\-\-no\-cache\fR
do not clear directory with package caches
.TP
\fB\-\-no\-chroot\fR
do not clear build chroot
.TP
\fB\-\-no\-manual\fR
do not clear directory with manually added packages
.TP
\fB\-\-no\-packages\fR
do not clear directory with built packages
.SH OPTIONS 'ahriman config'
usage: ahriman config [-h]
dump configuration for specified architecture
.SH OPTIONS 'ahriman init'
usage: ahriman init [-h]
create empty repository tree. Optional command for auto architecture support
.SH OPTIONS 'ahriman key-import'
usage: ahriman key-import [-h] [--key-server KEY_SERVER] key
import PGP key from public sources to repository user
.TP
\fBkey\fR
PGP key to import from public server
.TP
\fB\-\-key\-server\fR \fI\,KEY_SERVER\/\fR
key server for key import
.SH OPTIONS 'ahriman rebuild'
usage: ahriman rebuild [-h] [--depends-on DEPENDS_ON]
rebuild whole repository
.TP
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
only rebuild packages that depend on specified package
.SH OPTIONS 'ahriman remove'
usage: ahriman remove [-h] package [package ...]
remove package
.TP
\fBpackage\fR
package name or base
.SH OPTIONS 'ahriman remove-unknown'
usage: ahriman remove-unknown [-h] [--dry-run]
remove packages which are missing in AUR
.TP
\fB\-\-dry\-run\fR
just perform check for packages without removal
.SH OPTIONS 'ahriman report'
usage: ahriman report [-h] [target ...]
generate report
.TP
\fBtarget\fR
target to generate report
.SH OPTIONS 'ahriman search'
usage: ahriman search [-h] search [search ...]
search for package in AUR using API
.TP
\fBsearch\fR
search terms, can be specified multiple times
.SH OPTIONS 'ahriman setup'
usage: ahriman setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
create initial service configuration, requires root
.TP
\fB\-\-build\-command\fR \fI\,BUILD_COMMAND\/\fR
build command prefix
.TP
\fB\-\-from\-configuration\fR \fI\,FROM_CONFIGURATION\/\fR
path to default devtools pacman configuration
.TP
\fB\-\-no\-multilib\fR
do not add multilib repository
.TP
\fB\-\-packager\fR \fI\,PACKAGER\/\fR
packager name and email
.TP
\fB\-\-repository\fR \fI\,REPOSITORY\/\fR
repository name
.TP
\fB\-\-sign\-key\fR \fI\,SIGN_KEY\/\fR
sign key id
.TP
\fB\-\-sign\-target\fR {SignSettings.Packages,SignSettings.Repository}
sign options
.TP
\fB\-\-web\-port\fR \fI\,WEB_PORT\/\fR
port of the web service
.SH OPTIONS 'ahriman sign'
usage: ahriman sign [-h] [package ...]
(re\-)sign packages and repository database
.TP
\fBpackage\fR
sign only specified packages
.SH OPTIONS 'ahriman status'
usage: ahriman status [-h] [--ahriman] [--status {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}] [package ...]
request status of the package
.TP
\fBpackage\fR
filter status by package base
.TP
\fB\-\-ahriman\fR
get service status itself
.TP
\fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
filter packages by status
.SH OPTIONS 'ahriman status-update'
usage: ahriman status-update [-h] [--status {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}] [--remove] [package ...]
request status of the package
.TP
\fBpackage\fR
set status for specified packages. If no packages supplied, service status will be updated
.TP
\fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
new status
.TP
\fB\-\-remove\fR
remove package status page
.SH OPTIONS 'ahriman sync'
usage: ahriman sync [-h] [target ...]
sync packages to remote server
.TP
\fBtarget\fR
target to sync
.SH OPTIONS 'ahriman update'
usage: ahriman update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
run updates
.TP
\fBpackage\fR
filter check by package base
.TP
\fB\-\-dry\-run\fR
just perform check for updates, same as check command
.TP
\fB\-\-no\-aur\fR
do not check for AUR updates. Implies \-\-no\-vcs
.TP
\fB\-\-no\-manual\fR
do not include manual updates
.TP
\fB\-\-no\-vcs\fR
do not check VCS packages
.SH OPTIONS 'ahriman user'
usage: ahriman user [-h] [--as-service] [-a {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [--no-reload] [-p PASSWORD] [-r] [--secure] username
manage users for web services with password and role. In case if password was not entered it will be asked interactively
.TP
\fBusername\fR
username for web service
.TP
\fB\-\-as\-service\fR
add user as service user
.TP
\fB\-a\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}, \fB\-\-access\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}
user access level
.TP
\fB\-\-no\-reload\fR
do not reload authentication module
.TP
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
user password
.TP
\fB\-r\fR, \fB\-\-remove\fR
remove user from configuration
.TP
\fB\-\-secure\fR
set file permissions to user\-only
.SH OPTIONS 'ahriman web'
usage: ahriman web [-h]
start web server
.SH AUTHORS
.B ahriman
was written by ahriman team <>.
.SH DISTRIBUTION
The latest version of ahriman may be downloaded from
.UR https://github.com/arcan1s/ahriman
.UE

View File

@ -148,6 +148,7 @@ Some features require optional dependencies to be installed:
Web application requires the following python packages to be installed:
* Core part requires `aiohttp` (application itself), `aiohttp_jinja2` and `Jinja2` (HTML generation from templates).
* In addition, `aiohttp_debugtoolbar` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production.
* In addition, authorization feature requires `aiohttp_security`, `aiohttp_session` and `cryptography`.
* In addition to base authorization dependencies, OAuth2 also requires `aioauth-client` library.
@ -173,9 +174,9 @@ Package provides base jinja templates which can be overridden by settings. Vanil
## Requests and scopes
Service provides optional authorization which can be turned on in settings. In order to control user access there are two levels of authorization - read-only (only GET-like requests) and write (anything).
Service provides optional authorization which can be turned on in settings. In order to control user access there are two levels of authorization - read-only (only GET-like requests) and write (anything) which are provided by each web view directly.
If this feature is configured any request except for whitelisted will be prohibited without authentication. In addition, configuration flag `auth.allow_read_only` can be used in order to allow seeing main page without authorization (this page is in default white list).
If this feature is configured any request will be prohibited without authentication. In addition, configuration flag `auth.safe_build_status` can be used in order to allow seeing main page without authorization.
For authenticated users it uses encrypted session cookies to store tokens; encryption key is generated each time at the start of the application. It also stores expiration time of the session inside.

View File

@ -23,14 +23,12 @@ libalpm and AUR related configuration.
Base authorization settings. `OAuth` provider requires `aioauth-client` library to be installed.
* `target` - specifies authorization provider, string, optional, default `disabled`. Allowed values are `disabled`, `configuration`, `oauth`.
* `allow_read_only` - allow requesting read only pages without authorization, boolean, required.
* `allowed_paths` - URI paths (exact match) which can be accessed without authorization, space separated list of strings, optional.
* `allowed_paths_groups` - URI paths prefixes which can be accessed without authorization, space separated list of strings, optional.
* `client_id` - OAuth2 application client ID, string, required in case if `oauth` is used.
* `client_secret` - OAuth2 application client secret key, string, required in case if `oauth` is used.
* `max_age` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
* `oauth_provider` - OAuth2 provider class name as is in `aioauth-client` (e.g. `GoogleClient`, `GithubClient` etc), string, required in case if `oauth` is used.
* `oauth_scopes` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. `https://www.googleapis.com/auth/userinfo.email` for `GoogleClient` or `user:email` for `GithubClient`, space separated list of strings, required in case if `oauth` is used.
* `safe_build_status` - allow requesting status page without authorization, boolean, required.
* `salt` - password hash salt, string, required in case if authorization enabled (automatically generated by `create-user` subcommand).
## `auth:*` groups
@ -127,7 +125,11 @@ Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture.
* `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used.
* `debug` - enable debug toolbar, boolean, optional, default `no`.
* `debug_check_host` - check hosts to access debug toolbar, boolean, optional, default `no`.
* `debug_allowed_hosts` - allowed hosts to get access to debug toolbar, space separated list of string, optional.
* `host` - host to bind, string, optional.
* `index_url` - full url of the repository index page, string, optional.
* `password` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* `port` - port to bind, int, optional.
* `static_path` - path to directory with static files, string, required.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=1.3.0
pkgver=1.4.1
pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager"
arch=('any')
@ -15,6 +15,7 @@ optdepends=('breezy: -bzr packages support'
'mercurial: -hg packages support'
'python-aioauth-client: web server with OAuth2 authorization'
'python-aiohttp: web server'
'python-aiohttp-debugtoolbar: web server with enabled debug panel'
'python-aiohttp-jinja2: web server'
'python-aiohttp-security: web server with authorization'
'python-aiohttp-session: web server with authorization'

View File

@ -10,10 +10,10 @@ root = /
[auth]
target = disabled
allow_read_only = yes
max_age = 604800
oauth_provider = GoogleClient
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
safe_build_status = yes
[build]
archbuild_flags =
@ -51,6 +51,9 @@ command = rsync --archive --compress --partial --delete
chunk_size = 8388608
[web]
debug = no
debug_check_host = no
debug_allowed_hosts =
host = 127.0.0.1
static_path = /usr/share/ahriman/static
templates = /usr/share/ahriman

View File

@ -100,6 +100,12 @@
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
</ul>
{% if index_url is not none %}
<ul class="nav">
<li><a class="nav-link" href="{{ index_url }}" title="repo index">repo index</a></li>
</ul>
{% endif %}
{% if auth.enabled %}
{% if auth.username is none %}
{{ auth.control|safe }}

View File

@ -16,6 +16,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="requestPackages()">Request</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()">Add</button>
</div>
</div>

View File

@ -34,7 +34,8 @@
success: function (resp) {
const $options = resp.map(function (pkg) {
const $option = document.createElement("option");
$option.value = `${pkg.package} (${pkg.description})`;
$option.value = pkg.package;
$option.innerText = `${pkg.package} (${pkg.description})`;
return $option;
});
$knownPackages.empty().append($options);
@ -79,6 +80,11 @@
doPackageAction("/service-api/v1/add", $packages);
}
function requestPackages() {
const $packages = [$package.val()]
doPackageAction("/service-api/v1/request", $packages);
}
function removePackages() { doPackageAction("/service-api/v1/remove", getSelection()); }
function updatePackages() { doPackageAction("/service-api/v1/add", getSelection()); }

View File

@ -81,6 +81,9 @@ setup(
"package/share/ahriman/utils/bootstrap-scripts.jinja2",
"package/share/ahriman/utils/style.jinja2",
]),
("share/man/man1", [
"docs/ahriman.1",
])
],
extras_require={
@ -107,6 +110,7 @@ setup(
"aiohttp",
"aiohttp_jinja2",
"aioauth-client",
"aiohttp_debugtoolbar",
"aiohttp_session",
"aiohttp_security",
"cryptography",

View File

@ -26,6 +26,7 @@ from pathlib import Path
from ahriman import version
from ahriman.application import handlers
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package_source import PackageSource
from ahriman.models.sign_settings import SignSettings
from ahriman.models.user_access import UserAccess
@ -91,8 +92,10 @@ def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package base/name or archive path", nargs="+")
parser.add_argument("--now", help="run update function after", action="store_true")
parser.add_argument("--source", help="package source", choices=PackageSource, type=PackageSource,
default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add, architecture=[])
parser.set_defaults(handler=handlers.Add)
return parser
@ -107,7 +110,7 @@ def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, architecture=[], no_aur=False, no_manual=True, dry_run=True)
parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True)
return parser
@ -124,7 +127,7 @@ def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
parser.add_argument("--no-manual", help="do not clear directory with manually added packages", action="store_true")
parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
parser.set_defaults(handler=handlers.Clean, architecture=[], no_log=True, unsafe=True)
parser.set_defaults(handler=handlers.Clean, no_log=True, unsafe=True)
return parser
@ -178,7 +181,7 @@ def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
parser.set_defaults(handler=handlers.Rebuild, architecture=[])
parser.set_defaults(handler=handlers.Rebuild)
return parser
@ -191,7 +194,7 @@ def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("remove", help="remove package", description="remove package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package name or base", nargs="+")
parser.set_defaults(handler=handlers.Remove, architecture=[])
parser.set_defaults(handler=handlers.Remove)
return parser
@ -205,7 +208,7 @@ def _set_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser
description="remove packages which are missing in AUR",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
parser.set_defaults(handler=handlers.RemoveUnknown, architecture=[])
parser.set_defaults(handler=handlers.RemoveUnknown)
return parser
@ -218,7 +221,7 @@ def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("report", help="generate report", description="generate report",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report, architecture=[])
parser.set_defaults(handler=handlers.Report)
return parser
@ -266,7 +269,7 @@ def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("sign", help="sign packages", description="(re-)sign packages and repository database",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="sign only specified packages", nargs="*")
parser.set_defaults(handler=handlers.Sign, architecture=[])
parser.set_defaults(handler=handlers.Sign)
return parser
@ -313,7 +316,7 @@ def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("sync", help="sync repository", description="sync packages to remote server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync, architecture=[])
parser.set_defaults(handler=handlers.Sync)
return parser
@ -330,7 +333,7 @@ def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, architecture=[])
parser.set_defaults(handler=handlers.Update)
return parser
@ -357,6 +360,7 @@ def _set_user_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
parser.add_argument("-p", "--password", help="user password")
parser.add_argument("-r", "--remove", help="remove user from configuration", action="store_true")
parser.add_argument("--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.User, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
return parser

View File

@ -29,6 +29,7 @@ from ahriman.core.repository.repository import Repository
from ahriman.core.tree import Tree
from ahriman.core.util import package_like
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
class Application:
@ -96,10 +97,11 @@ class Application:
return updates
def add(self, names: Iterable[str], without_dependencies: bool) -> None:
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
"""
add packages for the next build
:param names: list of package bases to add
:param source: package source to add
:param without_dependencies: if set, dependency check will be disabled
"""
known_packages = self._known_packages()
@ -122,14 +124,14 @@ class Application:
if without_dependencies:
return
dependencies = Package.dependencies(path)
self.add(dependencies.difference(known_packages), without_dependencies)
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
def process_single(src: str) -> None:
maybe_path = Path(src)
if maybe_path.is_dir():
add_directory(maybe_path)
elif maybe_path.is_file():
add_archive(maybe_path)
resolved_source = source.resolve(src)
if resolved_source == PackageSource.Directory:
add_directory(Path(src))
elif resolved_source == PackageSource.Archive:
add_archive(Path(src))
else:
path = add_manual(src)
process_dependencies(path)

View File

@ -42,7 +42,7 @@ class Add(Handler):
:param no_report: force disable reporting
"""
application = Application(architecture, configuration, no_report)
application.add(args.package, args.without_dependencies)
application.add(args.package, args.source, args.without_dependencies)
if not args.now:
return

View File

@ -30,6 +30,8 @@ class Dump(Handler):
dump configuration handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
_print = print
@classmethod

View File

@ -34,9 +34,11 @@ from ahriman.models.repository_paths import RepositoryPaths
class Handler:
"""
base handler class for command callbacks
:cvar ALLOW_AUTO_ARCHITECTURE_RUN: allow to define architecture from existing repositories
:cvar ALLOW_MULTI_ARCHITECTURE_RUN: allow to run with multiple architectures
"""
ALLOW_AUTO_ARCHITECTURE_RUN = True
ALLOW_MULTI_ARCHITECTURE_RUN = True
@classmethod
@ -85,9 +87,11 @@ class Handler:
:param args: command line args
:return: list of architectures for which tree is created
"""
if args.architecture is None:
if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None:
# for some parsers (e.g. config) we need to run with specific architecture
# for those cases architecture must be set explicitly
raise MissingArchitecture(args.command)
if args.architecture:
if args.architecture: # architecture is specified explicitly
return set(args.architecture)
config = Configuration()
@ -96,7 +100,7 @@ class Handler:
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
architectures = RepositoryPaths.known_architectures(root)
if not architectures:
if not architectures: # well we did not find anything
raise MissingArchitecture(args.command)
return architectures

View File

@ -31,6 +31,8 @@ class Init(Handler):
repository init handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -31,6 +31,8 @@ class KeyImport(Handler):
key import packages handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -31,6 +31,8 @@ class Search(Handler):
packages search handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -37,6 +37,8 @@ class Setup(Handler):
:cvar SUDOERS_PATH: path to sudoers.d include configuration
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild")
BIN_DIR_PATH = Path("/usr/local/bin")
MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist")

View File

@ -33,6 +33,8 @@ class Status(Handler):
package status handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -32,6 +32,8 @@ class StatusUpdate(Handler):
status update handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:

View File

@ -35,6 +35,8 @@ class User(Handler):
user management handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
@ -52,7 +54,7 @@ class User(Handler):
User.clear_user(auth_configuration, user)
if not args.remove:
User.create_configuration(auth_configuration, user, salt, args.as_service)
User.write_configuration(auth_configuration)
User.write_configuration(auth_configuration, args.secure)
if not args.no_reload:
client = Application(architecture, configuration, no_report=False).repository.reporter
@ -127,13 +129,15 @@ class User(Handler):
return MUser.generate_password(salt_length)
@staticmethod
def write_configuration(configuration: Configuration) -> None:
def write_configuration(configuration: Configuration, secure: bool) -> None:
"""
write configuration file
:param configuration: configuration instance
:param secure: if true then set file permissions to 0o600
"""
if configuration.path is None:
return # should never happen actually
with configuration.path.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
configuration.path.chmod(0o600)
if secure:
configuration.path.chmod(0o600)

View File

@ -31,6 +31,7 @@ class Web(Handler):
web server handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
@classmethod

View File

@ -33,16 +33,11 @@ from ahriman.models.user_access import UserAccess
class Auth:
"""
helper to deal with user authorization
:ivar allowed_paths: URI paths which can be accessed without authorization
:ivar allowed_paths_groups: URI paths prefixes which can be accessed without authorization
:ivar enabled: indicates if authorization is enabled
:cvar ALLOWED_PATHS: URI paths which can be accessed without authorization, predefined
:cvar ALLOWED_PATHS_GROUPS: URI paths prefixes which can be accessed without authorization, predefined
:ivar max_age: session age in seconds. It will be used for both client side and server side checks
:ivar safe_build_status: allow read only access to the index page
"""
ALLOWED_PATHS = {"/", "/index.html"}
ALLOWED_PATHS_GROUPS = {"/static", "/user-api"}
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
"""
default constructor
@ -51,11 +46,8 @@ class Auth:
"""
self.logger = logging.getLogger("http")
self.allow_read_only = configuration.getboolean("auth", "allow_read_only")
self.allowed_paths = set(configuration.getlist("auth", "allowed_paths", fallback=[]))
self.allowed_paths.update(self.ALLOWED_PATHS)
self.allowed_paths_groups = set(configuration.getlist("auth", "allowed_paths_groups", fallback=[]))
self.allowed_paths_groups.update(self.ALLOWED_PATHS_GROUPS)
self.safe_build_status = configuration.getboolean("auth", "safe_build_status")
self.enabled = provider.is_enabled
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
@ -115,19 +107,6 @@ class Auth:
del username, password
return True
async def is_safe_request(self, uri: Optional[str], required: UserAccess) -> bool:
"""
check if requested path are allowed without authorization
:param uri: request uri
:param required: required access level
:return: True in case if this URI can be requested without authorization and False otherwise
"""
if required == UserAccess.Read and self.allow_read_only:
return True # in case if read right requested and allowed in options
if not uri:
return False # request without context is not allowed
return uri in self.allowed_paths or any(uri.startswith(path) for path in self.allowed_paths_groups)
async def known_username(self, username: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
check if user is known

View File

@ -28,6 +28,7 @@ from threading import Lock, Thread
from typing import Callable, Dict, Iterable, Tuple
from ahriman.core.configuration import Configuration
from ahriman.models.package_source import PackageSource
class Spawn(Thread):
@ -79,7 +80,9 @@ class Spawn(Thread):
:param packages: packages list to add
:param now: build packages now
"""
kwargs = {"now": ""} if now else {}
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
kwargs["now"] = ""
self.spawn_process("add", *packages, **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None:

View File

@ -0,0 +1,55 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
from enum import Enum
from pathlib import Path
from ahriman.core.util import package_like
class PackageSource(Enum):
"""
package source for addition enumeration
:cvar Auto: automatically determine type of the source
:cvar Archive: source is a package archive
:cvar Directory: source is a directory which contains packages
:cvar AUR: source is an AUR package for which it should search
"""
Auto = "auto"
Archive = "archive"
Directory = "directory"
AUR = "aur"
def resolve(self, source: str) -> PackageSource:
"""
resolve auto into the correct type
:param source: source of the package
:return: non-auto type of the package source
"""
if self != PackageSource.Auto:
return self
maybe_path = Path(source)
if maybe_path.is_dir():
return PackageSource.Directory
if maybe_path.is_file() and package_like(maybe_path):
return PackageSource.Archive
return PackageSource.AUR

View File

@ -23,9 +23,11 @@ from enum import Enum
class UserAccess(Enum):
"""
web user access enumeration
:cvar Read: user can read status page
:cvar Safe: user can access the page without authorization, should not be user for user configuration
:cvar Read: user can read the page
:cvar Write: user can modify task and package list
"""
Safe = "safe"
Read = "read"
Write = "write"

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "1.3.0"
__version__ = "1.4.1"

View File

@ -19,10 +19,12 @@
#
import aiohttp_security # type: ignore
import base64
import types
from aiohttp import web
from aiohttp.web import middleware, Request
from aiohttp.web_response import StreamResponse
from aiohttp.web_urldispatcher import StaticResource
from aiohttp_session import setup as setup_session # type: ignore
from aiohttp_session.cookie_storage import EncryptedCookieStorage # type: ignore
from cryptography import fernet
@ -72,20 +74,22 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
return await self.validator.verify_access(user.username, permission, context)
def auth_handler(validator: Auth) -> MiddlewareType:
def auth_handler() -> MiddlewareType:
"""
authorization and authentication middleware
:param validator: authorization module instance
:return: built middleware
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
if request.method in ("GET", "HEAD", "OPTIONS"):
permission = UserAccess.Read
permission_method = getattr(handler, "get_permission", None)
if permission_method is not None:
permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None)
permission = UserAccess.Safe if isinstance(handler_instance, StaticResource) else UserAccess.Write
else:
permission = UserAccess.Write
if not await validator.is_safe_request(request.path, permission):
if permission != UserAccess.Safe:
await aiohttp_security.check_permission(request, permission, request.path)
return await handler(request)
@ -109,6 +113,6 @@ def setup_auth(application: web.Application, validator: Auth) -> web.Application
identity_policy = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(auth_handler(validator))
application.middlewares.append(auth_handler())
return application

View File

@ -24,6 +24,7 @@ from ahriman.web.views.index import IndexView
from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.reload_auth import ReloadAuthView
from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView
from ahriman.web.views.status.ahriman import AhrimanView
from ahriman.web.views.status.package import PackageView
@ -48,6 +49,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
POST /service-api/v1/remove remove existing package from repository
POST /service-api/v1/request request to add new packages to repository
GET /service-api/v1/search search for substring in AUR
POST /service-api/v1/update update packages in repository, actually it is just alias for add
@ -82,6 +85,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_post("/service-api/v1/remove", RemoveView)
application.router.add_post("/service-api/v1/request", RequestView)
application.router.add_get("/service-api/v1/search", SearchView, allow_head=False)
application.router.add_post("/service-api/v1/update", AddView)

View File

@ -17,13 +17,16 @@
# 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 aiohttp.web import View
from typing import Any, Dict, List, Optional
from __future__ import annotations
from aiohttp.web import Request, View
from typing import Any, Dict, List, Optional, Type
from ahriman.core.auth.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.user_access import UserAccess
class BaseView(View):
@ -63,6 +66,16 @@ class BaseView(View):
validator: Auth = self.request.app["validator"]
return validator
@classmethod
async def get_permission(cls: Type[BaseView], request: Request) -> UserAccess:
"""
retrieve user permission from the request
:param request: request object
:return: extracted permission
"""
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Write)
return permission
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
"""
extract json data from either json or form data

View File

@ -24,12 +24,15 @@ from typing import Any, Dict
from ahriman import version
from ahriman.core.auth.helpers import authorized_userid
from ahriman.core.util import pretty_datetime
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class IndexView(BaseView):
"""
root view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
It uses jinja2 templates for report generation, the following variables are allowed:
@ -39,6 +42,7 @@ class IndexView(BaseView):
* control - HTML to insert for login control, HTML string, required
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
index_url - url to the repository index, string, optional
packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
@ -58,6 +62,8 @@ class IndexView(BaseView):
version - ahriman version, string, required
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Safe
@aiohttp_jinja2.template("build-status.jinja2")
async def get(self) -> Dict[str, Any]:
"""
@ -87,8 +93,9 @@ class IndexView(BaseView):
# auth block
auth_username = await authorized_userid(self.request)
authenticated = not self.validator.enabled or self.validator.safe_build_status or auth_username is not None
auth = {
"authenticated": not self.validator.enabled or self.validator.allow_read_only or auth_username is not None,
"authenticated": authenticated,
"control": self.validator.auth_control,
"enabled": self.validator.enabled,
"username": auth_username,
@ -97,6 +104,7 @@ class IndexView(BaseView):
return {
"architecture": self.service.architecture,
"auth": auth,
"index_url": self.configuration.get("web", "index_url", fallback=None),
"packages": packages,
"repository": self.service.repository.name,
"service": service,

View File

@ -19,22 +19,25 @@
#
from aiohttp.web import HTTPFound, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class AddView(BaseView):
"""
add package web view
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
"""
add new package
JSON body must be supplied, the following model is used:
{
"packages": "ahriman", # either list of packages or package name as in AUR
"build_now": true # optional flag which runs build
"packages": "ahriman" # either list of packages or package name as in AUR
}
:return: redirect to main page on success
@ -42,11 +45,10 @@ class AddView(BaseView):
data = await self.extract_data(["packages"])
try:
now = data.get("build_now", True)
packages = data["packages"]
except Exception as e:
return json_response(data=str(e), status=400)
self.spawner.packages_add(packages, now)
self.spawner.packages_add(packages, now=True)
return HTTPFound("/")
raise HTTPFound("/")

View File

@ -17,18 +17,21 @@
# 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 aiohttp.web import Response
from aiohttp.web_exceptions import HTTPNoContent
from aiohttp.web import HTTPNoContent, Response
from ahriman.core.auth.auth import Auth
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class ReloadAuthView(BaseView):
"""
reload authentication module web view
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
"""
reload authentication module. No parameters supported here
@ -45,4 +48,4 @@ class ReloadAuthView(BaseView):
self.request.app.logger.warning("could not update authentication module validator", exc_info=True)
raise
return HTTPNoContent()
raise HTTPNoContent()

View File

@ -19,14 +19,18 @@
#
from aiohttp.web import HTTPFound, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class RemoveView(BaseView):
"""
remove package web view
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Write
async def post(self) -> Response:
"""
remove existing packages
@ -47,4 +51,4 @@ class RemoveView(BaseView):
self.spawner.packages_remove(packages)
return HTTPFound("/")
raise HTTPFound("/")

View File

@ -0,0 +1,54 @@
#
# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class RequestView(BaseView):
"""
request package web view. It is actually the same as AddView, but without now
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Read
async def post(self) -> Response:
"""
request to add new package
JSON body must be supplied, the following model is used:
{
"packages": "ahriman" # either list of packages or package name as in AUR
}
:return: redirect to main page on success
"""
data = await self.extract_data(["packages"])
try:
packages = data["packages"]
except Exception as e:
return json_response(data=str(e), status=400)
self.spawner.packages_add(packages, now=False)
raise HTTPFound("/")

View File

@ -22,14 +22,19 @@ import aur # type: ignore
from aiohttp.web import Response, json_response
from typing import Callable, Iterator
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class SearchView(BaseView):
"""
AUR search web view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
async def get(self) -> Response:
"""
search packages in AUR

View File

@ -20,14 +20,21 @@
from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class AhrimanView(BaseView):
"""
service status web view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
:cvar POST_PERMISSION: post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Write
async def get(self) -> Response:
"""
get current service status
@ -55,4 +62,4 @@ class AhrimanView(BaseView):
self.service.update_self(status)
return HTTPNoContent()
raise HTTPNoContent()

View File

@ -22,14 +22,22 @@ from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class PackageView(BaseView):
"""
package base specific web view
:cvar DELETE_PERMISSION: delete permissions of self
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
:cvar POST_PERMISSION: post permissions of self
"""
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Write
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
async def get(self) -> Response:
"""
get current package base status
@ -58,7 +66,7 @@ class PackageView(BaseView):
base = self.request.match_info["package"]
self.service.remove(base)
return HTTPNoContent()
raise HTTPNoContent()
async def post(self) -> Response:
"""
@ -87,4 +95,4 @@ class PackageView(BaseView):
except UnknownPackage:
return json_response(data=f"Package {base} is unknown, but no package body set", status=400)
return HTTPNoContent()
raise HTTPNoContent()

View File

@ -19,14 +19,21 @@
#
from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class PackagesView(BaseView):
"""
global watcher view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
:cvar POST_PERMISSION: post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Write
async def get(self) -> Response:
"""
get current packages status
@ -47,4 +54,4 @@ class PackagesView(BaseView):
"""
self.service.load()
return HTTPNoContent()
raise HTTPNoContent()

View File

@ -22,14 +22,19 @@ from aiohttp.web import Response, json_response
from ahriman import version
from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class StatusView(BaseView):
"""
web service status web view
:cvar GET_PERMISSION: get permissions of self
:cvar HEAD_PERMISSION: head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
async def get(self) -> Response:
"""
get current service status

View File

@ -20,6 +20,7 @@
from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized, Response
from ahriman.core.auth.helpers import remember
from ahriman.models.user_access import UserAccess
from ahriman.models.user_identity import UserIdentity
from ahriman.web.views.base import BaseView
@ -27,8 +28,12 @@ from ahriman.web.views.base import BaseView
class LoginView(BaseView):
"""
login endpoint view
:cvar GET_PERMISSION: get permissions of self
:cvar POST_PERMISSION: post permissions of self
"""
GET_PERMISSION = POST_PERMISSION = UserAccess.Safe
async def get(self) -> Response:
"""
OAuth2 response handler
@ -46,7 +51,7 @@ class LoginView(BaseView):
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
if not code:
return HTTPFound(oauth_provider.get_oauth_url())
raise HTTPFound(oauth_provider.get_oauth_url())
response = HTTPFound("/")
username = await oauth_provider.get_oauth_username(code)

View File

@ -20,14 +20,18 @@
from aiohttp.web import HTTPFound, Response
from ahriman.core.auth.helpers import check_authorized, forget
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class LogoutView(BaseView):
"""
logout endpoint view
:cvar POST_PERMISSION: post permissions of self
"""
POST_PERMISSION = UserAccess.Safe
async def post(self) -> Response:
"""
logout user from the service. No parameters supported here

View File

@ -99,6 +99,14 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
application.logger.info("setup process spawner")
application["spawn"] = spawner
application.logger.info("setup debug panel")
debug_enabled = configuration.getboolean("web", "debug", fallback=False)
if debug_enabled:
import aiohttp_debugtoolbar # type: ignore
aiohttp_debugtoolbar.setup(application,
hosts=configuration.getlist("web", "debug_allowed_hosts", fallback=[]),
check_host=configuration.getboolean("web", "debug_check_host", fallback=False))
application.logger.info("setup authorization")
validator = application["validator"] = Auth.load(configuration)
if validator.enabled:

View File

@ -27,7 +27,7 @@ def args() -> argparse.Namespace:
fixture for command line arguments
:return: command line arguments test instance
"""
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
return argparse.Namespace(architecture=None, lock=None, force=False, unsafe=False, no_report=True)
@pytest.fixture

View File

@ -71,7 +71,6 @@ def test_extract_architectures(args: argparse.Namespace, configuration: Configur
"""
must generate list of available architectures
"""
args.architecture = []
args.configuration = configuration.path
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
@ -84,7 +83,6 @@ def test_extract_architectures_empty(args: argparse.Namespace, configuration: Co
"""
must raise exception if no available architectures found
"""
args.architecture = []
args.command = "config"
args.configuration = configuration.path
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set())
@ -93,12 +91,12 @@ def test_extract_architectures_empty(args: argparse.Namespace, configuration: Co
Handler.extract_architectures(args)
def test_extract_architectures_exception(args: argparse.Namespace) -> None:
def test_extract_architectures_exception(args: argparse.Namespace, mocker: MockerFixture) -> None:
"""
must raise exception on missing architectures
"""
args.command = "config"
args.architecture = None
mocker.patch.object(Handler, "ALLOW_AUTO_ARCHITECTURE_RUN", False)
with pytest.raises(MissingArchitecture):
Handler.extract_architectures(args)

View File

@ -4,6 +4,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Add
from ahriman.core.configuration import Configuration
from ahriman.models.package_source import PackageSource
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -14,6 +15,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
args.package = []
args.now = False
args.source = PackageSource.Auto
args.without_dependencies = False
return args

View File

@ -18,3 +18,10 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
Dump.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
print_mock.assert_called()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Dump.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -16,3 +16,10 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
Init.run(args, "x86_64", configuration, True)
create_tree_mock.assert_called_once()
init_mock.assert_called_once()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Init.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -27,3 +27,10 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
KeyImport.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not KeyImport.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -53,3 +53,10 @@ def test_log_fn(args: argparse.Namespace, configuration: Configuration, aur_pack
Search.run(args, "x86_64", configuration, True)
print_mock.assert_called() # we don't really care about call details tbh
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Search.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -149,3 +149,10 @@ def test_create_executable(args: argparse.Namespace, mocker: MockerFixture) -> N
Setup.create_executable(args.build_command, "x86_64")
symlink_text_mock.assert_called_once()
unlink_text_mock.assert_called_once()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Setup.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -81,3 +81,10 @@ def test_imply_with_report(args: argparse.Namespace, configuration: Configuratio
Status.run(args, "x86_64", configuration, True)
load_mock.assert_called_once()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Status.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -86,3 +86,10 @@ def test_imply_with_report(args: argparse.Namespace, configuration: Configuratio
StatusUpdate.run(args, "x86_64", configuration, True)
load_mock.assert_called_once()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not StatusUpdate.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -22,6 +22,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.access = UserAccess.Read
args.as_service = False
args.no_reload = False
args.secure = False
args.remove = False
return args
@ -227,11 +228,23 @@ def test_write_configuration(configuration: Configuration, mocker: MockerFixture
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.write_configuration(configuration)
User.write_configuration(configuration, secure=True)
write_mock.assert_called_once()
chmod_mock.assert_called_once()
def test_write_configuration_insecure(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must write configuration without setting file permissions
"""
mocker.patch("pathlib.Path.open")
mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.write_configuration(configuration, secure=False)
chmod_mock.assert_not_called()
def test_write_configuration_not_loaded(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must do nothing in case if configuration is not loaded
@ -241,6 +254,13 @@ def test_write_configuration_not_loaded(configuration: Configuration, mocker: Mo
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
chmod_mock = mocker.patch("pathlib.Path.chmod")
User.write_configuration(configuration)
User.write_configuration(configuration, secure=True)
write_mock.assert_not_called()
chmod_mock.assert_not_called()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not User.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -31,6 +31,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
run_mock.assert_called_once()
def test_disallow_auto_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Web.ALLOW_AUTO_ARCHITECTURE_RUN
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run

View File

@ -41,43 +41,64 @@ def test_multiple_architectures(parser: argparse.ArgumentParser) -> None:
must accept multiple architectures
"""
args = parser.parse_args(["-a", "x86_64", "-a", "i686", "config"])
assert len(args.architecture) == 2
assert args.architecture == ["x86_64", "i686"]
def test_subparsers_add(parser: argparse.ArgumentParser) -> None:
def test_subparsers_add_architecture(parser: argparse.ArgumentParser) -> None:
"""
add command must imply empty architectures list
add command must correctly parse architecture list
"""
args = parser.parse_args(["add", "ahriman"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "add", "ahriman"])
assert args.architecture == ["x86_64"]
def test_subparsers_check(parser: argparse.ArgumentParser) -> None:
"""
check command must imply empty architecture list, no-aur, no-manual and dry-run
check command must imply no-aur, no-manual and dry-run
"""
args = parser.parse_args(["check"])
assert args.architecture == []
assert not args.no_aur
assert args.no_manual
assert args.dry_run
def test_subparsers_check_architecture(parser: argparse.ArgumentParser) -> None:
"""
check command must correctly parse architecture list
"""
args = parser.parse_args(["check"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "check"])
assert args.architecture == ["x86_64"]
def test_subparsers_clean(parser: argparse.ArgumentParser) -> None:
"""
clean command must imply empty architectures list, unsafe and no-log
clean command must imply unsafe and no-log
"""
args = parser.parse_args(["clean"])
assert args.architecture == []
assert args.no_log
assert args.unsafe
def test_subparsers_clean_architecture(parser: argparse.ArgumentParser) -> None:
"""
clean command must correctly parse architecture list
"""
args = parser.parse_args(["clean"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "clean"])
assert args.architecture == ["x86_64"]
def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
"""
config command must imply lock, no-log, no-report and unsafe
"""
args = parser.parse_args(["config"])
args = parser.parse_args(["-a", "x86_64", "config"])
assert args.architecture == ["x86_64"]
assert args.lock is None
assert args.no_log
assert args.no_report
@ -88,7 +109,8 @@ def test_subparsers_init(parser: argparse.ArgumentParser) -> None:
"""
init command must imply no_report
"""
args = parser.parse_args(["init"])
args = parser.parse_args(["-a", "x86_64", "init"])
assert args.architecture == ["x86_64"]
assert args.no_report
@ -102,36 +124,42 @@ def test_subparsers_key_import(parser: argparse.ArgumentParser) -> None:
assert args.no_report
def test_subparsers_rebuild(parser: argparse.ArgumentParser) -> None:
def test_subparsers_key_import_architecture(parser: argparse.ArgumentParser) -> None:
"""
rebuild command must imply empty architectures list
check command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "key-import", "key"])
assert args.architecture == [""]
def test_subparsers_rebuild_architecture(parser: argparse.ArgumentParser) -> None:
"""
rebuild command must correctly parse architecture list
"""
args = parser.parse_args(["rebuild"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "rebuild"])
assert args.architecture == ["x86_64"]
def test_subparsers_remove(parser: argparse.ArgumentParser) -> None:
def test_subparsers_remove_architecture(parser: argparse.ArgumentParser) -> None:
"""
remove command must imply empty architectures list
remove command must correctly parse architecture list
"""
args = parser.parse_args(["remove", "ahriman"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "remove", "ahriman"])
assert args.architecture == ["x86_64"]
def test_subparsers_remove_unknown(parser: argparse.ArgumentParser) -> None:
def test_subparsers_report_architecture(parser: argparse.ArgumentParser) -> None:
"""
remove-unknown command must imply empty architectures list
"""
args = parser.parse_args(["remove-unknown"])
assert args.architecture == []
def test_subparsers_report(parser: argparse.ArgumentParser) -> None:
"""
report command must imply empty architectures list
report command must correctly parse architecture list
"""
args = parser.parse_args(["report"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "report"])
assert args.architecture == ["x86_64"]
def test_subparsers_search(parser: argparse.ArgumentParser) -> None:
@ -146,12 +174,21 @@ def test_subparsers_search(parser: argparse.ArgumentParser) -> None:
assert args.unsafe
def test_subparsers_search_architecture(parser: argparse.ArgumentParser) -> None:
"""
search command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "search", "ahriman"])
assert args.architecture == [""]
def test_subparsers_setup(parser: argparse.ArgumentParser) -> None:
"""
setup command must imply lock, no-log, no-report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "setup", "--packager", "John Doe <john@doe.com>",
"--repository", "aur-clone"])
assert args.architecture == ["x86_64"]
assert args.lock is None
assert args.no_log
assert args.no_report
@ -180,12 +217,14 @@ def test_subparsers_setup_option_sign_target(parser: argparse.ArgumentParser) ->
assert all(isinstance(target, SignSettings) for target in args.sign_target)
def test_subparsers_sign(parser: argparse.ArgumentParser) -> None:
def test_subparsers_sign_architecture(parser: argparse.ArgumentParser) -> None:
"""
sign command must imply empty architectures list
sign command must correctly parse architecture list
"""
args = parser.parse_args(["sign"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "sign"])
assert args.architecture == ["x86_64"]
def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
@ -193,6 +232,7 @@ def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
status command must imply lock, no-log, no-report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "status"])
assert args.architecture == ["x86_64"]
assert args.lock is None
assert args.no_log
assert args.no_report
@ -204,6 +244,7 @@ def test_subparsers_status_update(parser: argparse.ArgumentParser) -> None:
status-update command must imply lock, no-log, no-report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "status-update"])
assert args.architecture == ["x86_64"]
assert args.lock is None
assert args.no_log
assert args.no_report
@ -220,20 +261,24 @@ def test_subparsers_status_update_option_status(parser: argparse.ArgumentParser)
assert isinstance(args.status, BuildStatusEnum)
def test_subparsers_sync(parser: argparse.ArgumentParser) -> None:
def test_subparsers_sync_architecture(parser: argparse.ArgumentParser) -> None:
"""
sync command must imply empty architectures list
sync command must correctly parse architecture list
"""
args = parser.parse_args(["sync"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "sync"])
assert args.architecture == ["x86_64"]
def test_subparsers_update(parser: argparse.ArgumentParser) -> None:
def test_subparsers_update_architecture(parser: argparse.ArgumentParser) -> None:
"""
update command must imply empty architectures list
update command must correctly parse architecture list
"""
args = parser.parse_args(["update"])
assert args.architecture == []
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "update"])
assert args.architecture == ["x86_64"]
def test_subparsers_user(parser: argparse.ArgumentParser) -> None:
@ -248,6 +293,14 @@ def test_subparsers_user(parser: argparse.ArgumentParser) -> None:
assert args.unsafe
def test_subparsers_user_architecture(parser: argparse.ArgumentParser) -> None:
"""
user command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "user", "username"])
assert args.architecture == [""]
def test_subparsers_user_option_role(parser: argparse.ArgumentParser) -> None:
"""
user command must convert role option to useraccess instance
@ -263,6 +316,7 @@ def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
web command must imply lock, no_report and parser
"""
args = parser.parse_args(["-a", "x86_64", "web"])
assert args.architecture == ["x86_64"]
assert args.lock is None
assert args.no_report
assert args.parser is not None and args.parser()

View File

@ -6,6 +6,7 @@ from unittest import mock
from ahriman.application.application import Application
from ahriman.core.tree import Leaf, Tree
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
def test_finalize(application: Application, mocker: MockerFixture) -> None:
@ -108,12 +109,11 @@ def test_add_directory(application: Application, package_ahriman: Package, mocke
must add packages from directory
"""
mocker.patch("ahriman.application.application.Application._known_packages", return_value=set())
mocker.patch("pathlib.Path.is_dir", return_value=True)
iterdir_mock = mocker.patch("pathlib.Path.iterdir",
return_value=[package.filepath for package in package_ahriman.packages.values()])
move_mock = mocker.patch("shutil.move")
application.add([package_ahriman.base], False)
application.add([package_ahriman.base], PackageSource.Directory, False)
iterdir_mock.assert_called_once()
move_mock.assert_called_once()
@ -126,7 +126,7 @@ def test_add_manual(application: Application, package_ahriman: Package, mocker:
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
fetch_mock = mocker.patch("ahriman.core.build_tools.task.Task.fetch")
application.add([package_ahriman.base], True)
application.add([package_ahriman.base], PackageSource.AUR, True)
fetch_mock.assert_called_once()
@ -140,7 +140,7 @@ def test_add_manual_with_dependencies(application: Application, package_ahriman:
mocker.patch("ahriman.core.build_tools.task.Task.fetch")
dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies")
application.add([package_ahriman.base], False)
application.add([package_ahriman.base], PackageSource.AUR, False)
dependencies_mock.assert_called_once()
@ -149,10 +149,9 @@ def test_add_package(application: Application, package_ahriman: Package, mocker:
must add package from archive
"""
mocker.patch("ahriman.application.application.Application._known_packages", return_value=set())
mocker.patch("pathlib.Path.is_file", return_value=True)
move_mock = mocker.patch("shutil.move")
application.add([package_ahriman.base], False)
application.add([package_ahriman.base], PackageSource.Archive, False)
move_mock.assert_called_once()

View File

@ -109,40 +109,6 @@ async def test_check_credentials(auth: Auth, user: User) -> None:
assert await auth.check_credentials(None, None)
async def test_is_safe_request(auth: Auth) -> None:
"""
must validate safe request
"""
# login and logout are always safe
assert await auth.is_safe_request("/user-api/v1/login", UserAccess.Write)
assert await auth.is_safe_request("/user-api/v1/logout", UserAccess.Write)
auth.allowed_paths.add("/safe")
auth.allowed_paths_groups.add("/unsafe/safe")
assert await auth.is_safe_request("/safe", UserAccess.Write)
assert not await auth.is_safe_request("/unsafe", UserAccess.Write)
assert await auth.is_safe_request("/unsafe/safe", UserAccess.Write)
assert await auth.is_safe_request("/unsafe/safe/suffix", UserAccess.Write)
async def test_is_safe_request_empty(auth: Auth) -> None:
"""
must not allow requests without path
"""
assert not await auth.is_safe_request(None, UserAccess.Read)
assert not await auth.is_safe_request("", UserAccess.Read)
async def test_is_safe_request_read_only(auth: Auth) -> None:
"""
must allow read-only requests if it is set in settings
"""
assert await auth.is_safe_request("/", UserAccess.Read)
auth.allow_read_only = True
assert await auth.is_safe_request("/unsafe", UserAccess.Read)
async def test_known_username(auth: Auth, user: User) -> None:
"""
must allow any username

View File

@ -42,7 +42,7 @@ def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None:
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add(["ahriman", "linux"], now=False)
spawn_mock.assert_called_with("add", "ahriman", "linux")
spawn_mock.assert_called_with("add", "ahriman", "linux", source="aur")
def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
@ -51,7 +51,7 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None:
"""
spawn_mock = mocker.patch("ahriman.core.spawn.Spawn.spawn_process")
spawner.packages_add(["ahriman", "linux"], now=True)
spawn_mock.assert_called_with("add", "ahriman", "linux", now="")
spawn_mock.assert_called_with("add", "ahriman", "linux", source="aur", now="")
def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None:

View File

@ -0,0 +1,47 @@
from pytest_mock import MockerFixture
from ahriman.models.package_source import PackageSource
def test_resolve_non_auto() -> None:
"""
must resolve non auto type to itself
"""
for source in filter(lambda src: src != PackageSource.Auto, PackageSource):
assert source.resolve("") == source
def test_resolve_archive(mocker: MockerFixture) -> None:
"""
must resolve auto type into the archive
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("pathlib.Path.is_file", return_value=True)
assert PackageSource.Auto.resolve("linux-5.14.2.arch1-2-x86_64.pkg.tar.zst") == PackageSource.Archive
def test_resolve_directory(mocker: MockerFixture) -> None:
"""
must resolve auto type into the directory
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", return_value=False)
assert PackageSource.Auto.resolve("path") == PackageSource.Directory
def test_resolve_aur(mocker: MockerFixture) -> None:
"""
must resolve auto type into the AUR package
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("pathlib.Path.is_file", return_value=False)
assert PackageSource.Auto.resolve("package") == PackageSource.AUR
def test_resolve_aur_not_package_like(mocker: MockerFixture) -> None:
"""
must resolve auto type into the AUR package if it is file, but does not look like a package archive
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("pathlib.Path.is_file", return_value=True)
assert PackageSource.Auto.resolve("package") == PackageSource.AUR

View File

@ -64,3 +64,20 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
application["validator"]._users[generated.username] = generated
return application
@pytest.fixture
def application_with_debug(configuration: Configuration, user: User, spawner: Spawn,
mocker: MockerFixture) -> web.Application:
"""
application fixture with debug enabled
:param configuration: configuration fixture
:param user: user descriptor fixture
:param spawner: spawner fixture
:param mocker: mocker object
:return: application test instance
"""
configuration.set_option("web", "debug", "yes")
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
mocker.patch("pathlib.Path.mkdir")
return setup_service("x86_64", configuration, spawner)

View File

@ -43,60 +43,74 @@ async def test_permits(authorization_policy: AuthorizationPolicy, user: User) ->
assert not await authorization_policy.permits(user.username, user.access, "/endpoint")
async def test_auth_handler_api(auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_api(mocker: MockerFixture) -> None:
"""
must ask for status permission for api calls
"""
aiohttp_request = pytest.helpers.request("", "/status-api", "GET")
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
request_handler.get_permission.return_value = UserAccess.Read
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler(auth)
handler = auth_handler()
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
async def test_auth_handler_api_post(auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_api_no_method(mocker: MockerFixture) -> None:
"""
must ask for write permission if handler does not have get_permission method
"""
aiohttp_request = pytest.helpers.request("", "/status-api", "GET")
request_handler = AsyncMock()
request_handler.get_permission = None
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler()
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
async def test_auth_handler_api_post(mocker: MockerFixture) -> None:
"""
must ask for status permission for api calls with POST
"""
aiohttp_request = pytest.helpers.request("", "/status-api", "POST")
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
request_handler.get_permission.return_value = UserAccess.Write
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler(auth)
handler = auth_handler()
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
async def test_auth_handler_read(auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_read(mocker: MockerFixture) -> None:
"""
must ask for read permission for api calls with GET
"""
for method in ("GET", "HEAD", "OPTIONS"):
aiohttp_request = pytest.helpers.request("", "", method)
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
request_handler.get_permission.return_value = UserAccess.Read
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler(auth)
handler = auth_handler()
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
async def test_auth_handler_write(auth: Auth, mocker: MockerFixture) -> None:
async def test_auth_handler_write(mocker: MockerFixture) -> None:
"""
must ask for read permission for api calls with POST
"""
for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"):
aiohttp_request = pytest.helpers.request("", "", method)
request_handler = AsyncMock()
mocker.patch("ahriman.core.auth.auth.Auth.is_safe_request", return_value=False)
request_handler.get_permission.return_value = UserAccess.Write
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
handler = auth_handler(auth)
handler = auth_handler()
await handler(aiohttp_request, request_handler)
check_permission_mock.assert_called_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)

View File

@ -45,7 +45,7 @@ def test_run(application: web.Application, mocker: MockerFixture) -> None:
def test_run_with_auth(application_with_auth: web.Application, mocker: MockerFixture) -> None:
"""
must run application
must run application with enabled authorization
"""
port = 8080
application_with_auth["configuration"].set_option("web", "port", str(port))
@ -54,3 +54,16 @@ def test_run_with_auth(application_with_auth: web.Application, mocker: MockerFix
run_server(application_with_auth)
run_application_mock.assert_called_with(application_with_auth, host="127.0.0.1", port=port,
handle_signals=False, access_log=pytest.helpers.anyvar(int))
def test_run_with_debug(application_with_debug: web.Application, mocker: MockerFixture) -> None:
"""
must run application with enabled debug panel
"""
port = 8080
application_with_debug["configuration"].set_option("web", "port", str(port))
run_application_mock = mocker.patch("aiohttp.web.run_app")
run_server(application_with_debug)
run_application_mock.assert_called_with(application_with_debug, host="127.0.0.1", port=port,
handle_signals=False, access_log=pytest.helpers.anyvar(int))

View File

@ -1,6 +1,20 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.add import AddView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await AddView.get_permission(request) == UserAccess.Write
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
@ -10,18 +24,7 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None:
response = await client.post("/service-api/v1/add", json={"packages": ["ahriman"]})
assert response.ok
add_mock.assert_called_with(["ahriman"], True)
async def test_post_now(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post and run build
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/add", json={"packages": ["ahriman"], "build_now": False})
assert response.ok
add_mock.assert_called_with(["ahriman"], False)
add_mock.assert_called_with(["ahriman"], now=True)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
@ -43,4 +46,4 @@ async def test_post_update(client: TestClient, mocker: MockerFixture) -> None:
response = await client.post("/service-api/v1/update", json={"packages": ["ahriman"]})
assert response.ok
add_mock.assert_called_with(["ahriman"], True)
add_mock.assert_called_with(["ahriman"], now=True)

View File

@ -1,6 +1,20 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.reload_auth import ReloadAuthView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await ReloadAuthView.get_permission(request) == UserAccess.Write
async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None:
"""

View File

@ -1,6 +1,20 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.remove import RemoveView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await RemoveView.get_permission(request) == UserAccess.Write
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""

View File

@ -0,0 +1,38 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.request import RequestView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await RequestView.get_permission(request) == UserAccess.Read
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/request", json={"packages": ["ahriman"]})
assert response.ok
add_mock.assert_called_with(["ahriman"], now=False)
async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None:
"""
must raise exception on missing packages payload
"""
add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add")
response = await client.post("/service-api/v1/request")
assert response.status == 400
add_mock.assert_not_called()

View File

@ -1,8 +1,21 @@
import aur
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.service.search import SearchView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await SearchView.get_permission(request) == UserAccess.Read
async def test_get(client: TestClient, aur_package_ahriman: aur.Package, mocker: MockerFixture) -> None:
"""

View File

@ -1,7 +1,23 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.user_access import UserAccess
from ahriman.web.views.status.ahriman import AhrimanView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await AhrimanView.get_permission(request) == UserAccess.Read
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await AhrimanView.get_permission(request) == UserAccess.Write
async def test_get(client: TestClient) -> None:

View File

@ -1,7 +1,23 @@
import pytest
from pytest_aiohttp import TestClient
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.status.package import PackageView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await PackageView.get_permission(request) == UserAccess.Read
for method in ("DELETE", "POST"):
request = pytest.helpers.request("", "", method)
assert await PackageView.get_permission(request) == UserAccess.Write
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:

View File

@ -1,8 +1,24 @@
import pytest
from pytest_aiohttp import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.status.packages import PackagesView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await PackagesView.get_permission(request) == UserAccess.Read
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await PackagesView.get_permission(request) == UserAccess.Write
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:

View File

@ -1,9 +1,22 @@
import pytest
from pytest_aiohttp import TestClient
import ahriman.version as version
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.status.status import StatusView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await StatusView.get_permission(request) == UserAccess.Read
async def test_get(client: TestClient, package_ahriman: Package) -> None:

View File

@ -33,6 +33,16 @@ def test_validator(base: BaseView) -> None:
assert base.validator
async def test_get_permission(base: BaseView) -> None:
"""
must search for permission attribute in class
"""
for method in ("DELETE", "GET", "HEAD", "POST"):
request = pytest.helpers.request(base.request.app, "", method)
setattr(BaseView, f"{method.upper()}_PERMISSION", "permission")
assert await base.get_permission(request) == "permission"
async def test_extract_data_json(base: BaseView) -> None:
"""
must parse and return json

View File

@ -1,5 +1,19 @@
import pytest
from pytest_aiohttp import TestClient
from ahriman.models.user_access import UserAccess
from ahriman.web.views.index import IndexView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET", "HEAD"):
request = pytest.helpers.request("", "", method)
assert await IndexView.get_permission(request) == UserAccess.Safe
async def test_get(client_with_auth: TestClient) -> None:
"""
@ -34,3 +48,11 @@ async def test_get_static(client: TestClient) -> None:
"""
response = await client.get("/static/favicon.ico")
assert response.ok
async def test_get_static_with_auth(client_with_auth: TestClient) -> None:
"""
must return static files
"""
response = await client_with_auth.get("/static/favicon.ico")
assert response.ok

View File

@ -1,9 +1,21 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.auth.oauth import OAuth
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess
from ahriman.web.views.user.login import LoginView
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 LoginView.get_permission(request) == UserAccess.Safe
async def test_get_default_validator(client_with_auth: TestClient) -> None:

View File

@ -1,7 +1,21 @@
import pytest
from aiohttp.test_utils import TestClient
from aiohttp.web import HTTPUnauthorized
from pytest_mock import MockerFixture
from ahriman.models.user_access import UserAccess
from ahriman.web.views.user.logout import LogoutView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await LogoutView.get_permission(request) == UserAccess.Safe
async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None:
"""

View File

@ -9,12 +9,12 @@ repositories = core extra community multilib
root = /
[auth]
allow_read_only = no
client_id = client_id
client_secret = client_secret
oauth_provider = GoogleClient
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
salt = salt
safe_build_status = no
[build]
archbuild_flags =
@ -62,6 +62,9 @@ region = eu-central-1
secret_key =
[web]
debug = no
debug_check_host = no
debug_allowed_hosts =
host = 127.0.0.1
static_path = ../web/templates/static
templates = ../web/templates