mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-06 10:35:47 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
db195391e4 | |||
59f2992559 | |||
4f06647193 | |||
73a4cee257 | |||
13d00c6f66 | |||
3e032c3515 | |||
d73d5daad3 | |||
f55b44b391 | |||
51b28baf40 | |||
24326f9753 | |||
36c763069d | |||
c9a155bbc4 | |||
182bde5e09 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -94,5 +94,3 @@ ENV/
|
||||
.venv/
|
||||
|
||||
*.tar.xz
|
||||
|
||||
man/
|
||||
|
9
Makefile
9
Makefile
@ -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
401
docs/ahriman.1
Normal 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
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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
|
@ -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 }}
|
||||
|
@ -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>
|
||||
|
@ -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()); }
|
||||
|
4
setup.py
4
setup.py
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -30,6 +30,8 @@ class Dump(Handler):
|
||||
dump configuration handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
_print = print
|
||||
|
||||
@classmethod
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
55
src/ahriman/models/package_source.py
Normal file
55
src/ahriman/models/package_source.py
Normal 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
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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("/")
|
||||
|
@ -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()
|
||||
|
@ -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("/")
|
||||
|
54
src/ahriman/web/views/service/request.py
Normal file
54
src/ahriman/web/views/service/request.py
Normal 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("/")
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
47
tests/ahriman/models/test_package_source.py
Normal file
47
tests/ahriman/models/test_package_source.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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()
|
@ -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:
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
Reference in New Issue
Block a user