mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
parent
375f9fcfb7
commit
b1dfafe275
@ -3,7 +3,7 @@
|
|||||||
ahriman
|
ahriman
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B ahriman
|
.B ahriman
|
||||||
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-triggers,repo-update,update,user-add,user-list,user-remove,web} ...
|
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,user-add,user-list,user-remove,web} ...
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
ArcH Linux ReposItory MANager
|
ArcH Linux ReposItory MANager
|
||||||
|
|
||||||
@ -97,6 +97,9 @@ rebuild repository
|
|||||||
\fBahriman\fR \fI\,repo-remove-unknown\/\fR
|
\fBahriman\fR \fI\,repo-remove-unknown\/\fR
|
||||||
remove unknown packages
|
remove unknown packages
|
||||||
.TP
|
.TP
|
||||||
|
\fBahriman\fR \fI\,repo-report\/\fR
|
||||||
|
generate report
|
||||||
|
.TP
|
||||||
\fBahriman\fR \fI\,repo-restore\/\fR
|
\fBahriman\fR \fI\,repo-restore\/\fR
|
||||||
restore repository data
|
restore repository data
|
||||||
.TP
|
.TP
|
||||||
@ -109,6 +112,9 @@ sign packages
|
|||||||
\fBahriman\fR \fI\,repo-status-update\/\fR
|
\fBahriman\fR \fI\,repo-status-update\/\fR
|
||||||
update repository status
|
update repository status
|
||||||
.TP
|
.TP
|
||||||
|
\fBahriman\fR \fI\,repo-sync\/\fR
|
||||||
|
sync repository
|
||||||
|
.TP
|
||||||
\fBahriman\fR \fI\,repo-triggers\/\fR
|
\fBahriman\fR \fI\,repo-triggers\/\fR
|
||||||
run triggers
|
run triggers
|
||||||
.TP
|
.TP
|
||||||
@ -405,6 +411,11 @@ just perform check for packages without removal
|
|||||||
\fB\-i\fR, \fB\-\-info\fR
|
\fB\-i\fR, \fB\-\-info\fR
|
||||||
show additional package information
|
show additional package information
|
||||||
|
|
||||||
|
.SH COMMAND \fI\,'ahriman repo-report'\/\fR
|
||||||
|
usage: ahriman repo-report [-h]
|
||||||
|
|
||||||
|
generate repository report according to current settings
|
||||||
|
|
||||||
.SH COMMAND \fI\,'ahriman repo-restore'\/\fR
|
.SH COMMAND \fI\,'ahriman repo-restore'\/\fR
|
||||||
usage: ahriman repo-restore [-h] [-o OUTPUT] path
|
usage: ahriman repo-restore [-h] [-o OUTPUT] path
|
||||||
|
|
||||||
@ -485,11 +496,20 @@ update repository status on the status page
|
|||||||
\fB\-s\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR, \fB\-\-status\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR
|
\fB\-s\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR, \fB\-\-status\fR \fI\,{BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}\/\fR
|
||||||
new status
|
new status
|
||||||
|
|
||||||
|
.SH COMMAND \fI\,'ahriman repo-sync'\/\fR
|
||||||
|
usage: ahriman repo-sync [-h]
|
||||||
|
|
||||||
|
sync repository files to remote server according to current settings
|
||||||
|
|
||||||
.SH COMMAND \fI\,'ahriman repo-triggers'\/\fR
|
.SH COMMAND \fI\,'ahriman repo-triggers'\/\fR
|
||||||
usage: ahriman repo-triggers [-h]
|
usage: ahriman repo-triggers [-h] [trigger ...]
|
||||||
|
|
||||||
run triggers on empty build result as configured by settings
|
run triggers on empty build result as configured by settings
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fBtrigger\fR
|
||||||
|
instead of running all triggers as set by configuration, just process specified ones oin order of metion
|
||||||
|
|
||||||
.SH COMMAND \fI\,'ahriman repo-update'\/\fR
|
.SH COMMAND \fI\,'ahriman repo-update'\/\fR
|
||||||
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
|
usage: ahriman repo-update [-h] [--dry-run] [-e] [--no-aur] [--no-local] [--no-manual] [--no-vcs] [package ...]
|
||||||
|
|
||||||
@ -525,7 +545,8 @@ do not include manual updates
|
|||||||
do not check VCS packages
|
do not check VCS packages
|
||||||
|
|
||||||
.SH COMMAND \fI\,'ahriman user-add'\/\fR
|
.SH COMMAND \fI\,'ahriman user-add'\/\fR
|
||||||
usage: ahriman user-add [-h] [--as-service] [-p PASSWORD] [-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [-s]
|
usage: ahriman user-add [-h] [--as-service] [-p PASSWORD]
|
||||||
|
[-r {UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}] [-s]
|
||||||
username
|
username
|
||||||
|
|
||||||
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
|
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
|
||||||
@ -545,7 +566,7 @@ user password. Blank password will be treated as empty password, which is in par
|
|||||||
authorization type.
|
authorization type.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-r\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR, \fB\-\-role\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR
|
\fB\-r\fR \fI\,{UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}\/\fR, \fB\-\-role\fR \fI\,{UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}\/\fR
|
||||||
user access level
|
user access level
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
@ -553,7 +574,8 @@ user access level
|
|||||||
set file permissions to user\-only
|
set file permissions to user\-only
|
||||||
|
|
||||||
.SH COMMAND \fI\,'ahriman user-list'\/\fR
|
.SH COMMAND \fI\,'ahriman user-list'\/\fR
|
||||||
usage: ahriman user-list [-h] [-e] [-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [username]
|
usage: ahriman user-list [-h] [-e] [-r {UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}]
|
||||||
|
[username]
|
||||||
|
|
||||||
list users from the user mapping and their roles
|
list users from the user mapping and their roles
|
||||||
|
|
||||||
@ -567,7 +589,7 @@ filter users by username
|
|||||||
return non\-zero exit status if result is empty
|
return non\-zero exit status if result is empty
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-r\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR, \fB\-\-role\fR \fI\,{UserAccess.Safe,UserAccess.Read,UserAccess.Write}\/\fR
|
\fB\-r\fR \fI\,{UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}\/\fR, \fB\-\-role\fR \fI\,{UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}\/\fR
|
||||||
filter users by role
|
filter users by role
|
||||||
|
|
||||||
.SH COMMAND \fI\,'ahriman user-remove'\/\fR
|
.SH COMMAND \fI\,'ahriman user-remove'\/\fR
|
||||||
|
@ -20,6 +20,14 @@ ahriman.core.database.migrations.m001\_package\_source module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m002\_user\_access module
|
||||||
|
----------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m002_user_access
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -4,14 +4,6 @@ ahriman.web.views.status package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
ahriman.web.views.status.ahriman module
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: ahriman.web.views.status.ahriman
|
|
||||||
:members:
|
|
||||||
:no-undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
ahriman.web.views.status.package module
|
ahriman.web.views.status.package module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
|
|||||||
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
|
* ``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_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.
|
* ``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.
|
* ``allow_read_only`` - allow requesting status APIs without authorization, boolean, required.
|
||||||
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``create-user`` subcommand).
|
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``create-user`` subcommand).
|
||||||
|
|
||||||
Authorized users are stored inside internal database, if any of external provides are used the password field for non-service users must be empty.
|
Authorized users are stored inside internal database, if any of external provides are used the password field for non-service users must be empty.
|
||||||
|
@ -52,7 +52,7 @@ Obviously you can implement the specified method in class, but for guide purpose
|
|||||||
self.username = configuration.get("slack", "username")
|
self.username = configuration.get("slack", "username")
|
||||||
|
|
||||||
def run(self, result, packages):
|
def run(self, result, packages):
|
||||||
notify(result, self.slack_url, channel, username)
|
notify(result, self.slack_url, self.channel, self.username)
|
||||||
|
|
||||||
Setup the trigger
|
Setup the trigger
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -13,7 +13,7 @@ target = disabled
|
|||||||
max_age = 604800
|
max_age = 604800
|
||||||
oauth_provider = GoogleClient
|
oauth_provider = GoogleClient
|
||||||
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
|
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
|
||||||
safe_build_status = yes
|
allow_read_only = yes
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
archbuild_flags =
|
archbuild_flags =
|
||||||
@ -37,7 +37,6 @@ target = console
|
|||||||
use_utf = yes
|
use_utf = yes
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
full_template_path = /usr/share/ahriman/templates/repo-index.jinja2
|
|
||||||
no_empty_report = yes
|
no_empty_report = yes
|
||||||
template_path = /usr/share/ahriman/templates/email-index.jinja2
|
template_path = /usr/share/ahriman/templates/email-index.jinja2
|
||||||
ssl = disabled
|
ssl = disabled
|
||||||
|
@ -14,28 +14,29 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>ahriman
|
<h1>ahriman
|
||||||
{% if auth.authenticated %}
|
<img id="badge-version" src="https://img.shields.io/badge/version-unknown-informational" alt="unknown">
|
||||||
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
<img id="badge-repository" src="https://img.shields.io/badge/repository-unknown-informational" alt="unknown">
|
||||||
<img src="https://img.shields.io/badge/repository-{{ repository | replace("-", "--") }}-informational" alt="{{ repository }}">
|
<img id="badge-architecture" src="https://img.shields.io/badge/architecture-unknown-informational" alt="unknown">
|
||||||
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
<img id="badge-status" src="https://img.shields.io/badge/service%20status-unknown-inactive" alt="unknown">
|
||||||
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
|
|
||||||
{% endif %}
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
{% if not auth.enabled or auth.username is not none %}
|
{% if not auth.enabled or auth.username is not none %}
|
||||||
<button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
|
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
|
||||||
<i class="fa fa-plus"></i> add
|
<i class="bi bi-plus"></i> add
|
||||||
</button>
|
</button>
|
||||||
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled>
|
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" disabled hidden>
|
||||||
<i class="fa fa-play"></i> update
|
<i class="bi bi-play"></i> update
|
||||||
</button>
|
</button>
|
||||||
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled>
|
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
||||||
<i class="fa fa-trash"></i> remove
|
<i class="bi bi-trash"></i> remove
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<button class="btn btn-secondary" onclick="reload()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> reload
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="packages" class="table table-striped table-hover"
|
<table id="packages" class="table table-striped table-hover"
|
||||||
@ -53,42 +54,22 @@
|
|||||||
data-show-fullscreen="true"
|
data-show-fullscreen="true"
|
||||||
data-show-search-clear-button="true"
|
data-show-search-clear-button="true"
|
||||||
data-sortable="true"
|
data-sortable="true"
|
||||||
data-sort-reset="true"
|
data-sort-name="base"
|
||||||
|
data-sort-order="asc"
|
||||||
data-toggle="table"
|
data-toggle="table"
|
||||||
data-toolbar="#toolbar">
|
data-toolbar="#toolbar">
|
||||||
<thead class="table-primary">
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th data-checkbox="true"></th>
|
<th data-checkbox="true"></th>
|
||||||
<th data-sortable="true" data-switchable="false">package base</th>
|
<th data-sortable="true" data-switchable="false" data-field="base">package base</th>
|
||||||
<th data-sortable="true">version</th>
|
<th data-sortable="true" data-field="version">version</th>
|
||||||
<th data-sortable="true">packages</th>
|
<th data-sortable="true" data-field="packages">packages</th>
|
||||||
<th data-sortable="true" data-visible="false">groups</th>
|
<th data-sortable="true" data-visible="false" data-field="groups">groups</th>
|
||||||
<th data-sortable="true" data-visible="false">licenses</th>
|
<th data-sortable="true" data-visible="false" data-field="licenses">licenses</th>
|
||||||
<th data-sortable="true">last update</th>
|
<th data-sortable="true" data-field="timestamp">last update</th>
|
||||||
<th data-sortable="true">status</th>
|
<th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% if auth.authenticated %}
|
|
||||||
{% for package in packages %}
|
|
||||||
<tr data-package-base="{{ package.base }}">
|
|
||||||
<td data-checkbox="true"></td>
|
|
||||||
<td>{% if package.web_url is not none %}<a href="{{ package.web_url }}" title="{{ package.base }}">{{ package.base }}</a>{% else %}{{ package.base }}{% endif %}</td>
|
|
||||||
<td>{{ package.version }}</td>
|
|
||||||
<td>{{ package.packages|join("<br>"|safe) }}</td>
|
|
||||||
<td>{{ package.groups|join("<br>"|safe) }}</td>
|
|
||||||
<td>{{ package.licenses|join("<br>"|safe) }}</td>
|
|
||||||
<td>{{ package.timestamp }}</td>
|
|
||||||
<td class="table-{{ package.status_color }}">{{ package.status }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="100%">In order to see statuses you must login first.</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -122,11 +103,14 @@
|
|||||||
{% include "build-status/login-modal.jinja2" %}
|
{% include "build-status/login-modal.jinja2" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include "build-status/package-actions-modals.jinja2" %}
|
|
||||||
|
|
||||||
{% include "utils/bootstrap-scripts.jinja2" %}
|
{% include "utils/bootstrap-scripts.jinja2" %}
|
||||||
|
|
||||||
{% include "build-status/package-actions-script.jinja2" %}
|
{% include "build-status/failed-modal.jinja2" %}
|
||||||
|
{% include "build-status/success-modal.jinja2" %}
|
||||||
|
|
||||||
|
{% include "build-status/package-add-modal.jinja2" %}
|
||||||
|
|
||||||
|
{% include "build-status/table.jinja2" %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
<div id="failed-form" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger">
|
||||||
|
<h4 class="modal-title">failed</h4>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Packages update has failed.</p>
|
||||||
|
<p id="error-details"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const failedForm = $("#failed-form");
|
||||||
|
const errorDetails = $("#error-details");
|
||||||
|
failedForm.on("hidden.bs.modal", () => { reload(); });
|
||||||
|
|
||||||
|
function showFailure(details) {
|
||||||
|
errorDetails.text(details);
|
||||||
|
failedForm.modal("show");
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,60 +0,0 @@
|
|||||||
<div id="addForm" tabindex="-1" role="dialog" class="modal fade">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">add new packages</h4>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="package" class="col-sm-2 col-form-label">package</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input id="package" type="text" list="knownPackages" class="form-control" placeholder="AUR package" name="package" required>
|
|
||||||
<datalist id="knownPackages"></datalist>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="failedForm" tabindex="-1" role="dialog" class="modal fade">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header bg-danger">
|
|
||||||
<h4 class="modal-title">failed</h4>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Packages update has failed.</p>
|
|
||||||
<p id="errorDetails"></p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="successForm" tabindex="-1" role="dialog" class="modal fade">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header bg-success">
|
|
||||||
<h4 class="modal-title">success</h4>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Packages update has been run.</p>
|
|
||||||
<ul id="successDetails"></ul>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,95 +0,0 @@
|
|||||||
<script>
|
|
||||||
const $remove = $("#remove");
|
|
||||||
const $update = $("#update");
|
|
||||||
|
|
||||||
const $table = $("#packages");
|
|
||||||
$table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
|
|
||||||
function () {
|
|
||||||
$remove.prop("disabled", !$table.bootstrapTable("getSelections").length);
|
|
||||||
$update.prop("disabled", !$table.bootstrapTable("getSelections").length);
|
|
||||||
})
|
|
||||||
|
|
||||||
const $successForm = $("#successForm");
|
|
||||||
const $successDetails = $("#successDetails");
|
|
||||||
$successForm.on("hidden.bs.modal", function() { window.location.reload(); });
|
|
||||||
|
|
||||||
const $failedForm = $("#failedForm");
|
|
||||||
const $errorDetails = $("#errorDetails");
|
|
||||||
$failedForm.on("hidden.bs.modal", function() { window.location.reload(); });
|
|
||||||
|
|
||||||
const $package = $("#package");
|
|
||||||
const $knownPackages = $("#knownPackages");
|
|
||||||
$package.keyup(function () {
|
|
||||||
const $this = $(this);
|
|
||||||
clearTimeout($this.data("timeout"));
|
|
||||||
|
|
||||||
$this.data("timeout", setTimeout($.proxy(function () {
|
|
||||||
const $value = $package.val();
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: "/service-api/v1/search",
|
|
||||||
data: {"for": $value},
|
|
||||||
type: "GET",
|
|
||||||
dataType: "json",
|
|
||||||
success: function (resp) {
|
|
||||||
const $options = resp.map(function (pkg) {
|
|
||||||
const $option = document.createElement("option");
|
|
||||||
$option.value = pkg.package;
|
|
||||||
$option.innerText = `${pkg.package} (${pkg.description})`;
|
|
||||||
return $option;
|
|
||||||
});
|
|
||||||
$knownPackages.empty().append($options);
|
|
||||||
$this.focus();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, this), 500));
|
|
||||||
})
|
|
||||||
|
|
||||||
function doPackageAction($uri, $packages) {
|
|
||||||
if ($packages.length === 0)
|
|
||||||
return;
|
|
||||||
$.ajax({
|
|
||||||
url: $uri,
|
|
||||||
data: JSON.stringify({packages: $packages}),
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json",
|
|
||||||
success: function (_) {
|
|
||||||
const $details = $packages.map(function (pkg) {
|
|
||||||
const $li = document.createElement("li");
|
|
||||||
$li.innerText = pkg;
|
|
||||||
return $li;
|
|
||||||
});
|
|
||||||
$successDetails.empty().append($details);
|
|
||||||
$successForm.modal("show");
|
|
||||||
},
|
|
||||||
error: function (jqXHR, textStatus, errorThrown) {
|
|
||||||
$errorDetails.text(errorThrown);
|
|
||||||
$failedForm.modal("show");
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelection() {
|
|
||||||
return $.map($table.bootstrapTable("getSelections"), function(row) {
|
|
||||||
return row._data["package-base"];
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPackages() {
|
|
||||||
const $packages = [$package.val()]
|
|
||||||
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()); }
|
|
||||||
|
|
||||||
$(function () {
|
|
||||||
$table.bootstrapTable("uncheckAll");
|
|
||||||
})
|
|
||||||
</script>
|
|
@ -0,0 +1,62 @@
|
|||||||
|
<div id="add-form" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">add new packages</h4>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="package" class="col-sm-2 col-form-label">package</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input id="package-form" type="text" list="known-packages-dlist" class="form-control" placeholder="AUR package" name="package" required>
|
||||||
|
<datalist id="known-packages-dlist"></datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const packageInput = $("#package-form");
|
||||||
|
const knownPackages = $("#known-packages-dlist");
|
||||||
|
packageInput.keyup(() => {
|
||||||
|
clearTimeout(packageInput.data("timeout"));
|
||||||
|
packageInput.data("timeout", setTimeout($.proxy(() => {
|
||||||
|
const value = packageInput.val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/service-api/v1/search",
|
||||||
|
data: {"for": value},
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const options = response.map(pkg => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = pkg.package;
|
||||||
|
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
knownPackages.empty().append(options);
|
||||||
|
packageInput.focus();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, this), 500));
|
||||||
|
});
|
||||||
|
|
||||||
|
function addPackages() {
|
||||||
|
const packages = [packageInput.val()]
|
||||||
|
doPackageAction("/service-api/v1/add", packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPackages() {
|
||||||
|
const packages = [packageInput.val()]
|
||||||
|
doPackageAction("/service-api/v1/request", packages);
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,28 @@
|
|||||||
|
<div id="success-form" tabindex="-1" role="dialog" class="modal fade">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success">
|
||||||
|
<h4 class="modal-title">success</h4>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Packages update has been run.</p>
|
||||||
|
<ul id="success-details"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const successForm = $("#success-form");
|
||||||
|
const successDetails = $("#success-details");
|
||||||
|
successForm.on("hidden.bs.modal", () => { reload(); });
|
||||||
|
|
||||||
|
function showSuccess(details) {
|
||||||
|
successDetails.empty().append(details);
|
||||||
|
successForm.modal("show");
|
||||||
|
}
|
||||||
|
</script>
|
147
package/share/ahriman/templates/build-status/table.jinja2
Normal file
147
package/share/ahriman/templates/build-status/table.jinja2
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<script>
|
||||||
|
const addButton = $("#add-btn");
|
||||||
|
const removeButton = $("#remove-btn");
|
||||||
|
const updateButton = $("#update-btn");
|
||||||
|
|
||||||
|
const table = $("#packages");
|
||||||
|
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
|
||||||
|
() => {
|
||||||
|
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||||
|
updateButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const architectureBadge = $("#badge-architecture");
|
||||||
|
const repositoryBadge = $("#badge-repository");
|
||||||
|
const statusBadge = $("#badge-status");
|
||||||
|
const versionBadge = $("#badge-version");
|
||||||
|
|
||||||
|
function doPackageAction(uri, packages) {
|
||||||
|
if (packages.length === 0)
|
||||||
|
return;
|
||||||
|
$.ajax({
|
||||||
|
url: uri,
|
||||||
|
data: JSON.stringify({packages: packages}),
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
success: _ => {
|
||||||
|
const details = packages.map(pkg => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.innerText = pkg;
|
||||||
|
return li;
|
||||||
|
});
|
||||||
|
showSuccess(details);
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => { showFailure(errorThrown); },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelection() {
|
||||||
|
return table.bootstrapTable("getSelections").map(row => { return row.id; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePackages() { doPackageAction("/service-api/v1/remove", getSelection()); }
|
||||||
|
|
||||||
|
function updatePackages() { doPackageAction("/service-api/v1/add", getSelection()); }
|
||||||
|
|
||||||
|
function hideControls(hidden) {
|
||||||
|
addButton.attr("hidden", hidden);
|
||||||
|
removeButton.attr("hidden", hidden);
|
||||||
|
updateButton.attr("hidden", hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
table.bootstrapTable("showLoading");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/status-api/v1/packages",
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const extractListProperties = (description, property) => {
|
||||||
|
return Object.values(description.packages).map(pkg => {
|
||||||
|
return pkg[property];
|
||||||
|
}).reduce((left, right) => { return left.concat(right); }, []);
|
||||||
|
};
|
||||||
|
const listToTable = data => { return Array.from(new Set(data)).sort().join("<br>"); };
|
||||||
|
|
||||||
|
const payload = response.map(description => {
|
||||||
|
const package_base = description.package.base;
|
||||||
|
const web_url = description.package.remote?.web_url;
|
||||||
|
return {
|
||||||
|
id: description.package.base,
|
||||||
|
base: web_url ? `<a href="${web_url}" title="${package_base}">${package_base}</a>` : package_base,
|
||||||
|
version: description.package.version,
|
||||||
|
packages: listToTable(Object.keys(description.package.packages)),
|
||||||
|
groups: listToTable(extractListProperties(description.package, "groups")),
|
||||||
|
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
||||||
|
timestamp: new Date(1000 * description.status.timestamp).toISOString(),
|
||||||
|
status: description.status.status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
table.bootstrapTable("load", payload);
|
||||||
|
table.bootstrapTable("uncheckAll");
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
hideControls(false);
|
||||||
|
},
|
||||||
|
error: (jqXHR, _, errorThrown) => {
|
||||||
|
hideControls(true);
|
||||||
|
if ((jqXHR.status === 401) || (jqXHR.status === 403)) {
|
||||||
|
// authorization error
|
||||||
|
const text = "In order to see statuses you must login first.";
|
||||||
|
table.find("tr.unauthorized").remove();
|
||||||
|
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${text}</td></tr>`);
|
||||||
|
table.bootstrapTable("hideLoading");
|
||||||
|
} else {
|
||||||
|
// other errors
|
||||||
|
showFailure(errorThrown);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/status-api/v1/status",
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
success: response => {
|
||||||
|
const badgeColor = status => {
|
||||||
|
if (status === "pending") return "yellow";
|
||||||
|
if (status === "building") return "yellow";
|
||||||
|
if (status === "failed") return "critical";
|
||||||
|
if (status === "success") return "success";
|
||||||
|
return "inactive";
|
||||||
|
};
|
||||||
|
|
||||||
|
architectureBadge
|
||||||
|
.attr("src", `https://img.shields.io/badge/architecture-${response.architecture}-informational`)
|
||||||
|
.attr("alt", response.architecture);
|
||||||
|
repositoryBadge
|
||||||
|
.attr("src", `https://img.shields.io/badge/repository-${response.repository.replace(/-/g, "--")}-informational`)
|
||||||
|
.attr("alt", response.repository);
|
||||||
|
statusBadge
|
||||||
|
.attr("src", `https://img.shields.io/badge/service%20status-${response.status.status}-${badgeColor(response.status.status)}`)
|
||||||
|
.attr("alt", response.status.status)
|
||||||
|
.attr("title", `at ${new Date(1000 * response.status.timestamp).toISOString()}`);
|
||||||
|
versionBadge
|
||||||
|
.attr("src", `https://img.shields.io/badge/version-${response.version}-informational`)
|
||||||
|
.attr("alt", response.version);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusFormat(value) {
|
||||||
|
const cellClass = status => {
|
||||||
|
if (status === "pending") return "table-warning";
|
||||||
|
if (status === "building") return "table-warning";
|
||||||
|
if (status === "failed") return "table-danger";
|
||||||
|
if (status === "success") return "table-success";
|
||||||
|
return "table-secondary";
|
||||||
|
};
|
||||||
|
return {classes: cellClass(value)};
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
table.bootstrapTable({});
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
</script>
|
@ -40,7 +40,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
data-show-fullscreen="true"
|
data-show-fullscreen="true"
|
||||||
data-show-search-clear-button="true"
|
data-show-search-clear-button="true"
|
||||||
data-sortable="true"
|
data-sortable="true"
|
||||||
data-sort-reset="true"
|
data-sort-name="base"
|
||||||
|
data-sort-order="asc"
|
||||||
data-toggle="table">
|
data-toggle="table">
|
||||||
<thead class="table-primary">
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||||
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></script>
|
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$("#packages").bootstrapTable({
|
$("#packages").bootstrapTable({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||||
|
|
||||||
<link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
|
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||||
|
|
||||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet">
|
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet">
|
||||||
|
|
||||||
|
6
setup.py
6
setup.py
@ -69,9 +69,11 @@ setup(
|
|||||||
"package/share/ahriman/templates/telegram-index.jinja2",
|
"package/share/ahriman/templates/telegram-index.jinja2",
|
||||||
]),
|
]),
|
||||||
("share/ahriman/templates/build-status", [
|
("share/ahriman/templates/build-status", [
|
||||||
|
"package/share/ahriman/templates/build-status/failed-modal.jinja2",
|
||||||
"package/share/ahriman/templates/build-status/login-modal.jinja2",
|
"package/share/ahriman/templates/build-status/login-modal.jinja2",
|
||||||
"package/share/ahriman/templates/build-status/package-actions-modals.jinja2",
|
"package/share/ahriman/templates/build-status/package-add-modal.jinja2",
|
||||||
"package/share/ahriman/templates/build-status/package-actions-script.jinja2",
|
"package/share/ahriman/templates/build-status/success-modal.jinja2",
|
||||||
|
"package/share/ahriman/templates/build-status/table.jinja2",
|
||||||
]),
|
]),
|
||||||
("share/ahriman/templates/static", [
|
("share/ahriman/templates/static", [
|
||||||
"package/share/ahriman/templates/static/favicon.ico",
|
"package/share/ahriman/templates/static/favicon.ico",
|
||||||
|
@ -52,8 +52,8 @@ class Status(Handler):
|
|||||||
# we are using reporter here
|
# we are using reporter here
|
||||||
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
|
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
|
||||||
if args.ahriman:
|
if args.ahriman:
|
||||||
ahriman = client.get_self()
|
service_status = client.get_internal()
|
||||||
StatusPrinter(ahriman).print(args.info)
|
StatusPrinter(service_status.status).print(args.info)
|
||||||
if args.package:
|
if args.package:
|
||||||
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
|
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
|
||||||
[client.get(base) for base in args.package],
|
[client.get(base) for base in args.package],
|
||||||
|
@ -37,7 +37,7 @@ class Auth:
|
|||||||
enabled(bool): indicates if authorization is enabled
|
enabled(bool): indicates if authorization is enabled
|
||||||
logger(logging.Logger): class logger
|
logger(logging.Logger): class logger
|
||||||
max_age(int): session age in seconds. It will be used for both client side and server side checks
|
max_age(int): session age in seconds. It will be used for both client side and server side checks
|
||||||
safe_build_status(bool): allow read only access to the index page
|
allow_read_only(bool): allow read only access to APIs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
|
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
|
||||||
@ -50,7 +50,7 @@ class Auth:
|
|||||||
"""
|
"""
|
||||||
self.logger = logging.getLogger("http")
|
self.logger = logging.getLogger("http")
|
||||||
|
|
||||||
self.safe_build_status = configuration.getboolean("auth", "safe_build_status")
|
self.allow_read_only = configuration.getboolean("auth", "allow_read_only")
|
||||||
|
|
||||||
self.enabled = provider.is_enabled
|
self.enabled = provider.is_enabled
|
||||||
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
|
self.max_age = configuration.getint("auth", "max_age", fallback=7 * 24 * 3600)
|
||||||
|
33
src/ahriman/core/database/migrations/m002_user_access.py
Normal file
33
src/ahriman/core/database/migrations/m002_user_access.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2022 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""
|
||||||
|
update users set access = 'read' where access = 'safe'
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
update users set access = 'reporter' where access = 'read'
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
update users set access = 'full' where access = 'write'
|
||||||
|
""",
|
||||||
|
]
|
@ -80,16 +80,7 @@ class Client:
|
|||||||
Returns:
|
Returns:
|
||||||
InternalStatus: current internal (web) service status
|
InternalStatus: current internal (web) service status
|
||||||
"""
|
"""
|
||||||
return InternalStatus()
|
return InternalStatus(BuildStatus())
|
||||||
|
|
||||||
def get_self(self) -> BuildStatus: # pylint: disable=no-self-use
|
|
||||||
"""
|
|
||||||
get ahriman status itself
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BuildStatus: current ahriman status
|
|
||||||
"""
|
|
||||||
return BuildStatus()
|
|
||||||
|
|
||||||
def remove(self, base: str) -> None:
|
def remove(self, base: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -57,16 +57,6 @@ class WebClient(Client):
|
|||||||
self.__session = requests.session()
|
self.__session = requests.session()
|
||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
@property
|
|
||||||
def _ahriman_url(self) -> str:
|
|
||||||
"""
|
|
||||||
get url for the service status api
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: full url for web service for ahriman service itself
|
|
||||||
"""
|
|
||||||
return f"{self.address}/status-api/v1/ahriman"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _login_url(self) -> str:
|
def _login_url(self) -> str:
|
||||||
"""
|
"""
|
||||||
@ -201,26 +191,7 @@ class WebClient(Client):
|
|||||||
self.logger.exception("could not get web service status: %s", exception_response_text(e))
|
self.logger.exception("could not get web service status: %s", exception_response_text(e))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception("could not get web service status")
|
self.logger.exception("could not get web service status")
|
||||||
return InternalStatus()
|
return InternalStatus(BuildStatus())
|
||||||
|
|
||||||
def get_self(self) -> BuildStatus:
|
|
||||||
"""
|
|
||||||
get ahriman status itself
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BuildStatus: current ahriman status
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
response = self.__session.get(self._ahriman_url)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
status_json = response.json()
|
|
||||||
return BuildStatus.from_json(status_json)
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
self.logger.exception("could not get service status: %s", exception_response_text(e))
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("could not get service status")
|
|
||||||
return BuildStatus()
|
|
||||||
|
|
||||||
def remove(self, base: str) -> None:
|
def remove(self, base: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -265,7 +236,7 @@ class WebClient(Client):
|
|||||||
payload = {"status": status.value}
|
payload = {"status": status.value}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.__session.post(self._ahriman_url, json=payload)
|
response = self.__session.post(self._status_url, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.HTTPError as e:
|
except requests.HTTPError as e:
|
||||||
self.logger.exception("could not update service status: %s", exception_response_text(e))
|
self.logger.exception("could not update service status: %s", exception_response_text(e))
|
||||||
|
@ -46,40 +46,6 @@ class BuildStatusEnum(str, Enum):
|
|||||||
Failed = "failed"
|
Failed = "failed"
|
||||||
Success = "success"
|
Success = "success"
|
||||||
|
|
||||||
def badges_color(self) -> str:
|
|
||||||
"""
|
|
||||||
convert itself to shield.io badges color
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: shields.io color
|
|
||||||
"""
|
|
||||||
if self == BuildStatusEnum.Pending:
|
|
||||||
return "yellow"
|
|
||||||
if self == BuildStatusEnum.Building:
|
|
||||||
return "yellow"
|
|
||||||
if self == BuildStatusEnum.Failed:
|
|
||||||
return "critical"
|
|
||||||
if self == BuildStatusEnum.Success:
|
|
||||||
return "success"
|
|
||||||
return "inactive"
|
|
||||||
|
|
||||||
def bootstrap_color(self) -> str:
|
|
||||||
"""
|
|
||||||
converts itself to bootstrap color
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: bootstrap color
|
|
||||||
"""
|
|
||||||
if self == BuildStatusEnum.Pending:
|
|
||||||
return "warning"
|
|
||||||
if self == BuildStatusEnum.Building:
|
|
||||||
return "warning"
|
|
||||||
if self == BuildStatusEnum.Failed:
|
|
||||||
return "danger"
|
|
||||||
if self == BuildStatusEnum.Success:
|
|
||||||
return "success"
|
|
||||||
return "secondary"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BuildStatus:
|
class BuildStatus:
|
||||||
|
@ -22,6 +22,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from typing import Any, Dict, Optional, Type
|
from typing import Any, Dict, Optional, Type
|
||||||
|
|
||||||
|
from ahriman.models.build_status import BuildStatus
|
||||||
from ahriman.models.counters import Counters
|
from ahriman.models.counters import Counters
|
||||||
|
|
||||||
|
|
||||||
@ -31,12 +32,14 @@ class InternalStatus:
|
|||||||
internal server status
|
internal server status
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
status(BuildStatus): service status
|
||||||
architecture(Optional[str]): repository architecture
|
architecture(Optional[str]): repository architecture
|
||||||
packages(Counters): packages statuses counter object
|
packages(Counters): packages statuses counter object
|
||||||
repository(Optional[str]): repository name
|
repository(Optional[str]): repository name
|
||||||
version(Optional[str]): service version
|
version(Optional[str]): service version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
status: BuildStatus
|
||||||
architecture: Optional[str] = None
|
architecture: Optional[str] = None
|
||||||
packages: Counters = field(default=Counters(total=0))
|
packages: Counters = field(default=Counters(total=0))
|
||||||
repository: Optional[str] = None
|
repository: Optional[str] = None
|
||||||
@ -54,7 +57,8 @@ class InternalStatus:
|
|||||||
InternalStatus: internal status
|
InternalStatus: internal status
|
||||||
"""
|
"""
|
||||||
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
||||||
return cls(architecture=dump.get("architecture"),
|
return cls(status=BuildStatus.from_json(dump.get("status", {})),
|
||||||
|
architecture=dump.get("architecture"),
|
||||||
packages=counters,
|
packages=counters,
|
||||||
repository=dump.get("repository"),
|
repository=dump.get("repository"),
|
||||||
version=dump.get("version"))
|
version=dump.get("version"))
|
||||||
|
@ -142,9 +142,7 @@ class User:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: True in case if user is allowed to do this request and False otherwise
|
||||||
"""
|
"""
|
||||||
if self.access == UserAccess.Write:
|
return self.access.permits(required)
|
||||||
return True # everything is allowed
|
|
||||||
return self.access == required
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
@ -25,12 +27,31 @@ class UserAccess(str, Enum):
|
|||||||
web user access enumeration
|
web user access enumeration
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
Safe(UserAccess): (class attribute) user can access the page without authorization,
|
Unauthorized(UserAccess): (class attribute) user can access specific resources which are marked as available
|
||||||
should not be used for user configuration
|
without authorization (e.g. login, logout, static)
|
||||||
Read(UserAccess): (class attribute) user can read the page
|
Read(UserAccess): (class attribute) user can read the page
|
||||||
Write(UserAccess): (class attribute) user can modify task and package list
|
Reporter(UserAccess): (class attribute) user can read everything and is able to perform some modifications
|
||||||
|
Full(UserAccess): (class attribute) user has full access
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Safe = "safe"
|
Unauthorized = "unauthorized"
|
||||||
Read = "read"
|
Read = "read"
|
||||||
Write = "write"
|
Reporter = "reporter"
|
||||||
|
Full = "full"
|
||||||
|
|
||||||
|
def permits(self, other: UserAccess) -> bool:
|
||||||
|
"""
|
||||||
|
compare enumeration between each other and check if current permission allows the ``other``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other(UserAccess): other permission to compare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True in case if current permission allows the operation and False otherwise
|
||||||
|
"""
|
||||||
|
for member in UserAccess:
|
||||||
|
if member == other:
|
||||||
|
return True
|
||||||
|
if member == self:
|
||||||
|
return False
|
||||||
|
return False # must never happen
|
||||||
|
@ -89,10 +89,13 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
|
|||||||
return await self.validator.verify_access(user.username, permission, context)
|
return await self.validator.verify_access(user.username, permission, context)
|
||||||
|
|
||||||
|
|
||||||
def auth_handler() -> MiddlewareType:
|
def auth_handler(allow_read_only: bool) -> MiddlewareType:
|
||||||
"""
|
"""
|
||||||
authorization and authentication middleware
|
authorization and authentication middleware
|
||||||
|
|
||||||
|
Args:
|
||||||
|
allow_read_only: allow
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MiddlewareType: built middleware
|
MiddlewareType: built middleware
|
||||||
"""
|
"""
|
||||||
@ -102,10 +105,14 @@ def auth_handler() -> MiddlewareType:
|
|||||||
permission = await permission_method(request)
|
permission = await permission_method(request)
|
||||||
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
|
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
|
||||||
handler_instance = getattr(handler, "__self__", None)
|
handler_instance = getattr(handler, "__self__", None)
|
||||||
permission = UserAccess.Safe if isinstance(handler_instance, StaticResource) else UserAccess.Write
|
permission = UserAccess.Unauthorized if isinstance(handler_instance, StaticResource) else UserAccess.Full
|
||||||
|
else:
|
||||||
|
permission = UserAccess.Full
|
||||||
|
if permission == UserAccess.Unauthorized: # explicit if elif else for better code coverage
|
||||||
|
pass
|
||||||
|
elif allow_read_only and UserAccess.Read.permits(permission):
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
permission = UserAccess.Write
|
|
||||||
if permission != UserAccess.Safe:
|
|
||||||
await aiohttp_security.check_permission(request, permission, request.path)
|
await aiohttp_security.check_permission(request, permission, request.path)
|
||||||
|
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
@ -133,6 +140,6 @@ def setup_auth(application: web.Application, validator: Auth) -> web.Application
|
|||||||
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
||||||
|
|
||||||
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
||||||
application.middlewares.append(auth_handler())
|
application.middlewares.append(auth_handler(validator.allow_read_only))
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
@ -25,7 +25,6 @@ from ahriman.web.views.service.add import AddView
|
|||||||
from ahriman.web.views.service.remove import RemoveView
|
from ahriman.web.views.service.remove import RemoveView
|
||||||
from ahriman.web.views.service.request import RequestView
|
from ahriman.web.views.service.request import RequestView
|
||||||
from ahriman.web.views.service.search import SearchView
|
from ahriman.web.views.service.search import SearchView
|
||||||
from ahriman.web.views.status.ahriman import AhrimanView
|
|
||||||
from ahriman.web.views.status.package import PackageView
|
from ahriman.web.views.status.package import PackageView
|
||||||
from ahriman.web.views.status.packages import PackagesView
|
from ahriman.web.views.status.packages import PackagesView
|
||||||
from ahriman.web.views.status.status import StatusView
|
from ahriman.web.views.status.status import StatusView
|
||||||
@ -55,9 +54,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
|
|
||||||
* POST /service-api/v1/update update packages in repository, actually it is just alias for add
|
* POST /service-api/v1/update update packages in repository, actually it is just alias for add
|
||||||
|
|
||||||
* GET /status-api/v1/ahriman get current service status
|
|
||||||
* POST /status-api/v1/ahriman update service status
|
|
||||||
|
|
||||||
* GET /status-api/v1/packages get all known packages
|
* GET /status-api/v1/packages get all known packages
|
||||||
* POST /status-api/v1/packages force update every package from repository
|
* POST /status-api/v1/packages force update every package from repository
|
||||||
|
|
||||||
@ -65,7 +61,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
* GET /status-api/v1/package/:base get package base status
|
* GET /status-api/v1/package/:base get package base status
|
||||||
* POST /status-api/v1/package/:base update package base status
|
* POST /status-api/v1/package/:base update package base status
|
||||||
|
|
||||||
* GET /status-api/v1/status get web service status itself
|
* GET /status-api/v1/status get service status itself
|
||||||
|
* POST /status-api/v1/status update service status itself
|
||||||
|
|
||||||
* GET /user-api/v1/login OAuth2 handler for login
|
* GET /user-api/v1/login OAuth2 handler for login
|
||||||
* POST /user-api/v1/login login to service
|
* POST /user-api/v1/login login to service
|
||||||
@ -90,9 +87,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
|
|
||||||
application.router.add_post("/service-api/v1/update", AddView)
|
application.router.add_post("/service-api/v1/update", AddView)
|
||||||
|
|
||||||
application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True)
|
|
||||||
application.router.add_post("/status-api/v1/ahriman", AhrimanView)
|
|
||||||
|
|
||||||
application.router.add_get("/status-api/v1/packages", PackagesView, allow_head=True)
|
application.router.add_get("/status-api/v1/packages", PackagesView, allow_head=True)
|
||||||
application.router.add_post("/status-api/v1/packages", PackagesView)
|
application.router.add_post("/status-api/v1/packages", PackagesView)
|
||||||
|
|
||||||
@ -101,6 +95,7 @@ def setup_routes(application: Application, static_path: Path) -> None:
|
|||||||
application.router.add_post("/status-api/v1/packages/{package}", PackageView)
|
application.router.add_post("/status-api/v1/packages/{package}", PackageView)
|
||||||
|
|
||||||
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
|
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
|
||||||
|
application.router.add_post("/status-api/v1/status", StatusView)
|
||||||
|
|
||||||
application.router.add_get("/user-api/v1/login", LoginView)
|
application.router.add_get("/user-api/v1/login", LoginView)
|
||||||
application.router.add_post("/user-api/v1/login", LoginView)
|
application.router.add_post("/user-api/v1/login", LoginView)
|
||||||
|
@ -101,7 +101,7 @@ class BaseView(View):
|
|||||||
Returns:
|
Returns:
|
||||||
UserAccess: extracted permission
|
UserAccess: extracted permission
|
||||||
"""
|
"""
|
||||||
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Write)
|
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
|
||||||
return permission
|
return permission
|
||||||
|
|
||||||
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
|
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||||
|
@ -21,9 +21,7 @@ import aiohttp_jinja2
|
|||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from ahriman import version
|
|
||||||
from ahriman.core.auth.helpers import authorized_userid
|
from ahriman.core.auth.helpers import authorized_userid
|
||||||
from ahriman.core.util import pretty_datetime
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
@ -34,37 +32,19 @@ class IndexView(BaseView):
|
|||||||
|
|
||||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||||
|
|
||||||
* architecture - repository architecture, string, required
|
|
||||||
* auth - authorization descriptor, required
|
* auth - authorization descriptor, required
|
||||||
* authenticated - alias to check if user can see the page, boolean, required
|
|
||||||
* control - HTML to insert for login control, HTML string, required
|
* control - HTML to insert for login control, HTML string, required
|
||||||
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||||
* username - authenticated username if any, string, null means not authenticated
|
* username - authenticated username if any, string, null means not authenticated
|
||||||
* index_url - url to the repository index, string, optional
|
* index_url - url to the repository index, string, optional
|
||||||
* packages - sorted list of packages properties, required
|
|
||||||
* base, string
|
|
||||||
* depends, sorted list of strings
|
|
||||||
* groups, sorted list of strings
|
|
||||||
* licenses, sorted list of strings
|
|
||||||
* packages, sorted list of strings
|
|
||||||
* status, string based on enum value
|
|
||||||
* status_color, string based on enum value
|
|
||||||
* timestamp, pretty printed datetime, string
|
|
||||||
* version, string
|
|
||||||
* web_url, string
|
|
||||||
* repository - repository name, string, required
|
* repository - repository name, string, required
|
||||||
* service - service status properties, required
|
|
||||||
* status, string based on enum value
|
|
||||||
* status_color, string based on enum value
|
|
||||||
* timestamp, pretty printed datetime, string
|
|
||||||
* version - ahriman version, string, required
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Safe
|
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
@aiohttp_jinja2.template("build-status.jinja2")
|
@aiohttp_jinja2.template("build-status.jinja2")
|
||||||
async def get(self) -> Dict[str, Any]:
|
async def get(self) -> Dict[str, Any]:
|
||||||
@ -74,43 +54,15 @@ class IndexView(BaseView):
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: parameters for jinja template
|
Dict[str, Any]: parameters for jinja template
|
||||||
"""
|
"""
|
||||||
# some magic to make it jinja-friendly
|
|
||||||
packages = [
|
|
||||||
{
|
|
||||||
"base": package.base,
|
|
||||||
"depends": package.depends,
|
|
||||||
"groups": package.groups,
|
|
||||||
"licenses": package.licenses,
|
|
||||||
"packages": list(sorted(package.packages)),
|
|
||||||
"status": status.status.value,
|
|
||||||
"status_color": status.status.bootstrap_color(),
|
|
||||||
"timestamp": pretty_datetime(status.timestamp),
|
|
||||||
"version": package.version,
|
|
||||||
"web_url": package.remote.web_url if package.remote is not None else None,
|
|
||||||
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
|
|
||||||
]
|
|
||||||
service = {
|
|
||||||
"status": self.service.status.status.value,
|
|
||||||
"status_color": self.service.status.status.badges_color(),
|
|
||||||
"timestamp": pretty_datetime(self.service.status.timestamp),
|
|
||||||
}
|
|
||||||
|
|
||||||
# auth block
|
|
||||||
auth_username = await authorized_userid(self.request)
|
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 = {
|
auth = {
|
||||||
"authenticated": authenticated,
|
|
||||||
"control": self.validator.auth_control,
|
"control": self.validator.auth_control,
|
||||||
"enabled": self.validator.enabled,
|
"enabled": self.validator.enabled,
|
||||||
"username": auth_username,
|
"username": auth_username,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"architecture": self.service.architecture,
|
|
||||||
"auth": auth,
|
"auth": auth,
|
||||||
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
||||||
"packages": packages,
|
|
||||||
"repository": self.service.repository.name,
|
"repository": self.service.repository.name,
|
||||||
"service": service,
|
|
||||||
"version": version.__version__,
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ class AddView(BaseView):
|
|||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
POST_PERMISSION = UserAccess.Write
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -31,7 +31,7 @@ class RemoveView(BaseView):
|
|||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
POST_PERMISSION = UserAccess.Write
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -31,7 +31,7 @@ class RequestView(BaseView):
|
|||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
POST_PERMISSION = UserAccess.Read
|
POST_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -35,7 +35,7 @@ class SearchView(BaseView):
|
|||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
|
||||||
|
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2022 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 HTTPBadRequest, 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
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
|
||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
|
||||||
"""
|
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
|
||||||
POST_PERMISSION = UserAccess.Write
|
|
||||||
|
|
||||||
async def get(self) -> Response:
|
|
||||||
"""
|
|
||||||
get current service status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response: 200 with service status object
|
|
||||||
"""
|
|
||||||
return json_response(self.service.status.view())
|
|
||||||
|
|
||||||
async def post(self) -> None:
|
|
||||||
"""
|
|
||||||
update service status
|
|
||||||
|
|
||||||
JSON body must be supplied, the following model is used::
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPBadRequest: if bad data is supplied
|
|
||||||
HTTPNoContent: in case of success response
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
data = await self.extract_data()
|
|
||||||
status = BuildStatusEnum(data["status"])
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPBadRequest(reason=str(e))
|
|
||||||
|
|
||||||
self.service.update_self(status)
|
|
||||||
|
|
||||||
raise HTTPNoContent()
|
|
@ -37,7 +37,7 @@ class PackageView(BaseView):
|
|||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Write
|
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
||||||
|
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
|
@ -34,7 +34,7 @@ class PackagesView(BaseView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
||||||
POST_PERMISSION = UserAccess.Write
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from aiohttp.web import Response, json_response
|
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||||
|
|
||||||
from ahriman import version
|
from ahriman import version
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
from ahriman.models.counters import Counters
|
from ahriman.models.counters import Counters
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
@ -33,9 +34,11 @@ class StatusView(BaseView):
|
|||||||
Attributes:
|
Attributes:
|
||||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||||
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
|
||||||
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
||||||
|
POST_PERMISSION = UserAccess.Full
|
||||||
|
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
"""
|
"""
|
||||||
@ -46,9 +49,34 @@ class StatusView(BaseView):
|
|||||||
"""
|
"""
|
||||||
counters = Counters.from_packages(self.service.packages)
|
counters = Counters.from_packages(self.service.packages)
|
||||||
status = InternalStatus(
|
status = InternalStatus(
|
||||||
|
status=self.service.status,
|
||||||
architecture=self.service.architecture,
|
architecture=self.service.architecture,
|
||||||
packages=counters,
|
packages=counters,
|
||||||
repository=self.service.repository.name,
|
repository=self.service.repository.name,
|
||||||
version=version.__version__)
|
version=version.__version__)
|
||||||
|
|
||||||
return json_response(status.view())
|
return json_response(status.view())
|
||||||
|
|
||||||
|
async def post(self) -> None:
|
||||||
|
"""
|
||||||
|
update service status
|
||||||
|
|
||||||
|
JSON body must be supplied, the following model is used::
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
|
||||||
|
}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPBadRequest: if bad data is supplied
|
||||||
|
HTTPNoContent: in case of success response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = await self.extract_data()
|
||||||
|
status = BuildStatusEnum(data["status"])
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPBadRequest(reason=str(e))
|
||||||
|
|
||||||
|
self.service.update_self(status)
|
||||||
|
|
||||||
|
raise HTTPNoContent()
|
||||||
|
@ -34,7 +34,7 @@ class LoginView(BaseView):
|
|||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Safe
|
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
async def get(self) -> None:
|
async def get(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -32,7 +32,7 @@ class LogoutView(BaseView):
|
|||||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||||
"""
|
"""
|
||||||
|
|
||||||
POST_PERMISSION = UserAccess.Safe
|
POST_PERMISSION = UserAccess.Unauthorized
|
||||||
|
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, package_ahr
|
|||||||
"""
|
"""
|
||||||
args = _default_args(args)
|
args = _default_args(args)
|
||||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||||
application_mock = mocker.patch("ahriman.core.status.client.Client.get_self")
|
application_mock = mocker.patch("ahriman.core.status.client.Client.get_internal")
|
||||||
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
|
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
|
||||||
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
|
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
|
||||||
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
|
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
|
||||||
@ -55,7 +55,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
|
|||||||
args = _default_args(args)
|
args = _default_args(args)
|
||||||
args.exit_code = True
|
args.exit_code = True
|
||||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||||
mocker.patch("ahriman.core.status.client.Client.get_self")
|
mocker.patch("ahriman.core.status.client.Client.get_internal")
|
||||||
mocker.patch("ahriman.core.status.client.Client.get", return_value=[])
|
mocker.patch("ahriman.core.status.client.Client.get", return_value=[])
|
||||||
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
|
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
|||||||
args.as_service = False
|
args.as_service = False
|
||||||
args.exit_code = False
|
args.exit_code = False
|
||||||
args.password = "pa55w0rd"
|
args.password = "pa55w0rd"
|
||||||
args.role = UserAccess.Read
|
args.role = UserAccess.Reporter
|
||||||
args.secure = False
|
args.secure = False
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
@ -519,7 +519,7 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
|
|||||||
"""
|
"""
|
||||||
args = parser.parse_args(["user-add", "username"])
|
args = parser.parse_args(["user-add", "username"])
|
||||||
assert isinstance(args.role, UserAccess)
|
assert isinstance(args.role, UserAccess)
|
||||||
args = parser.parse_args(["user-add", "username", "--role", "write"])
|
args = parser.parse_args(["user-add", "username", "--role", "full"])
|
||||||
assert isinstance(args.role, UserAccess)
|
assert isinstance(args.role, UserAccess)
|
||||||
|
|
||||||
|
|
||||||
@ -549,7 +549,7 @@ def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> No
|
|||||||
"""
|
"""
|
||||||
user-list command must convert role option to useraccess instance
|
user-list command must convert role option to useraccess instance
|
||||||
"""
|
"""
|
||||||
args = parser.parse_args(["user-list", "--role", "write"])
|
args = parser.parse_args(["user-list", "--role", "full"])
|
||||||
assert isinstance(args.role, UserAccess)
|
assert isinstance(args.role, UserAccess)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from unittest import mock
|
|||||||
from ahriman import version
|
from ahriman import version
|
||||||
from ahriman.application.lock import Lock
|
from ahriman.application.lock import Lock
|
||||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ def test_check_version(lock: Lock, mocker: MockerFixture) -> None:
|
|||||||
must check version correctly
|
must check version correctly
|
||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
||||||
return_value=InternalStatus(version=version.__version__))
|
return_value=InternalStatus(status=BuildStatus(), version=version.__version__))
|
||||||
logging_mock = mocker.patch("logging.Logger.warning")
|
logging_mock = mocker.patch("logging.Logger.warning")
|
||||||
|
|
||||||
lock.check_version()
|
lock.check_version()
|
||||||
@ -69,7 +69,7 @@ def test_check_version_mismatch(lock: Lock, mocker: MockerFixture) -> None:
|
|||||||
must check mismatched version correctly
|
must check mismatched version correctly
|
||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
mocker.patch("ahriman.core.status.client.Client.get_internal",
|
||||||
return_value=InternalStatus(version="version"))
|
return_value=InternalStatus(status=BuildStatus(), version="version"))
|
||||||
logging_mock = mocker.patch("logging.Logger.warning")
|
logging_mock = mocker.patch("logging.Logger.warning")
|
||||||
|
|
||||||
lock.check_version()
|
lock.check_version()
|
||||||
|
@ -426,7 +426,7 @@ def user() -> User:
|
|||||||
Returns:
|
Returns:
|
||||||
User: user descriptor instance
|
User: user descriptor instance
|
||||||
"""
|
"""
|
||||||
return User("user", "pa55w0rd", UserAccess.Read)
|
return User("user", "pa55w0rd", UserAccess.Reporter)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -71,4 +71,4 @@ async def test_verify_access(auth: Auth, user: User) -> None:
|
|||||||
must allow any access
|
must allow any access
|
||||||
"""
|
"""
|
||||||
assert await auth.verify_access(user.username, user.access, None)
|
assert await auth.verify_access(user.username, user.access, None)
|
||||||
assert await auth.verify_access(user.username, UserAccess.Write, None)
|
assert await auth.verify_access(user.username, UserAccess.Full, None)
|
||||||
|
@ -79,4 +79,4 @@ async def test_verify_access(mapping: Mapping, user: User, mocker: MockerFixture
|
|||||||
"""
|
"""
|
||||||
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=user)
|
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=user)
|
||||||
assert await mapping.verify_access(user.username, user.access, None)
|
assert await mapping.verify_access(user.username, user.access, None)
|
||||||
assert not await mapping.verify_access(user.username, UserAccess.Write, None)
|
assert not await mapping.verify_access(user.username, UserAccess.Full, None)
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
from ahriman.core.database.migrations.m002_user_access import steps
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_package_source() -> None:
|
||||||
|
"""
|
||||||
|
migration must not be empty
|
||||||
|
"""
|
||||||
|
assert steps
|
@ -29,7 +29,7 @@ def test_user_list_filter_by_username(database: SQLite) -> None:
|
|||||||
must return users filtered by its id
|
must return users filtered by its id
|
||||||
"""
|
"""
|
||||||
first = User("1", "", UserAccess.Read)
|
first = User("1", "", UserAccess.Read)
|
||||||
second = User("2", "", UserAccess.Write)
|
second = User("2", "", UserAccess.Full)
|
||||||
third = User("3", "", UserAccess.Read)
|
third = User("3", "", UserAccess.Read)
|
||||||
|
|
||||||
database.user_update(first)
|
database.user_update(first)
|
||||||
@ -46,7 +46,7 @@ def test_user_list_filter_by_access(database: SQLite) -> None:
|
|||||||
must return users filtered by its access
|
must return users filtered by its access
|
||||||
"""
|
"""
|
||||||
first = User("1", "", UserAccess.Read)
|
first = User("1", "", UserAccess.Read)
|
||||||
second = User("2", "", UserAccess.Write)
|
second = User("2", "", UserAccess.Full)
|
||||||
third = User("3", "", UserAccess.Read)
|
third = User("3", "", UserAccess.Read)
|
||||||
|
|
||||||
database.user_update(first)
|
database.user_update(first)
|
||||||
@ -64,7 +64,7 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
|
|||||||
must return users filtered by its access and username
|
must return users filtered by its access and username
|
||||||
"""
|
"""
|
||||||
first = User("1", "", UserAccess.Read)
|
first = User("1", "", UserAccess.Read)
|
||||||
second = User("2", "", UserAccess.Write)
|
second = User("2", "", UserAccess.Full)
|
||||||
third = User("3", "", UserAccess.Read)
|
third = User("3", "", UserAccess.Read)
|
||||||
|
|
||||||
database.user_update(first)
|
database.user_update(first)
|
||||||
@ -72,7 +72,7 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
|
|||||||
database.user_update(third)
|
database.user_update(third)
|
||||||
|
|
||||||
assert database.user_list("1", UserAccess.Read) == [first]
|
assert database.user_list("1", UserAccess.Read) == [first]
|
||||||
assert not database.user_list("1", UserAccess.Write)
|
assert not database.user_list("1", UserAccess.Full)
|
||||||
|
|
||||||
|
|
||||||
def test_user_remove_update(database: SQLite, user: User) -> None:
|
def test_user_remove_update(database: SQLite, user: User) -> None:
|
||||||
@ -92,6 +92,6 @@ def test_user_update(database: SQLite, user: User) -> None:
|
|||||||
assert database.user_get(user.username) == user
|
assert database.user_get(user.username) == user
|
||||||
|
|
||||||
new_user = user.hash_password("salt")
|
new_user = user.hash_password("salt")
|
||||||
new_user.access = UserAccess.Write
|
new_user.access = UserAccess.Full
|
||||||
database.user_update(new_user)
|
database.user_update(new_user)
|
||||||
assert database.user_get(new_user.username) == new_user
|
assert database.user_get(new_user.username) == new_user
|
||||||
|
@ -3,7 +3,7 @@ from pytest_mock import MockerFixture
|
|||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.status.client import Client
|
from ahriman.core.status.client import Client
|
||||||
from ahriman.core.status.web_client import WebClient
|
from ahriman.core.status.web_client import WebClient
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
@ -51,14 +51,11 @@ def test_get_internal(client: Client) -> None:
|
|||||||
"""
|
"""
|
||||||
must return dummy status for web service
|
must return dummy status for web service
|
||||||
"""
|
"""
|
||||||
assert client.get_internal() == InternalStatus()
|
expected = InternalStatus(BuildStatus())
|
||||||
|
actual = client.get_internal()
|
||||||
|
actual.status.timestamp = expected.status.timestamp
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
def test_get_self(client: Client) -> None:
|
|
||||||
"""
|
|
||||||
must return unknown status for service
|
|
||||||
"""
|
|
||||||
assert client.get_self().status == BuildStatusEnum.Unknown
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove(client: Client, package_ahriman: Package) -> None:
|
def test_remove(client: Client, package_ahriman: Package) -> None:
|
||||||
|
@ -13,14 +13,6 @@ from ahriman.models.package import Package
|
|||||||
from ahriman.models.user import User
|
from ahriman.models.user import User
|
||||||
|
|
||||||
|
|
||||||
def test_ahriman_url(web_client: WebClient) -> None:
|
|
||||||
"""
|
|
||||||
must generate service status url correctly
|
|
||||||
"""
|
|
||||||
assert web_client._ahriman_url.startswith(web_client.address)
|
|
||||||
assert web_client._ahriman_url.endswith("/status-api/v1/ahriman")
|
|
||||||
|
|
||||||
|
|
||||||
def test_status_url(web_client: WebClient) -> None:
|
def test_status_url(web_client: WebClient) -> None:
|
||||||
"""
|
"""
|
||||||
must generate package status url correctly
|
must generate package status url correctly
|
||||||
@ -173,7 +165,7 @@ def test_get_internal(web_client: WebClient, mocker: MockerFixture) -> None:
|
|||||||
must return web service status
|
must return web service status
|
||||||
"""
|
"""
|
||||||
response_obj = Response()
|
response_obj = Response()
|
||||||
response_obj._content = json.dumps(InternalStatus(architecture="x86_64").view()).encode("utf8")
|
response_obj._content = json.dumps(InternalStatus(BuildStatus(), architecture="x86_64").view()).encode("utf8")
|
||||||
response_obj.status_code = 200
|
response_obj.status_code = 200
|
||||||
|
|
||||||
requests_mock = mocker.patch("requests.Session.get", return_value=response_obj)
|
requests_mock = mocker.patch("requests.Session.get", return_value=response_obj)
|
||||||
@ -188,7 +180,7 @@ def test_get_internal_failed(web_client: WebClient, mocker: MockerFixture) -> No
|
|||||||
must suppress any exception happened during web service status getting
|
must suppress any exception happened during web service status getting
|
||||||
"""
|
"""
|
||||||
mocker.patch("requests.Session.get", side_effect=Exception())
|
mocker.patch("requests.Session.get", side_effect=Exception())
|
||||||
assert web_client.get_internal() == InternalStatus()
|
assert web_client.get_internal().architecture is None
|
||||||
|
|
||||||
|
|
||||||
def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
||||||
@ -196,38 +188,7 @@ def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFix
|
|||||||
must suppress HTTP exception happened during web service status getting
|
must suppress HTTP exception happened during web service status getting
|
||||||
"""
|
"""
|
||||||
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
|
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
|
||||||
assert web_client.get_internal() == InternalStatus()
|
assert web_client.get_internal().architecture is None
|
||||||
|
|
||||||
|
|
||||||
def test_get_self(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must return service status
|
|
||||||
"""
|
|
||||||
response_obj = Response()
|
|
||||||
response_obj._content = json.dumps(BuildStatus().view()).encode("utf8")
|
|
||||||
response_obj.status_code = 200
|
|
||||||
|
|
||||||
requests_mock = mocker.patch("requests.Session.get", return_value=response_obj)
|
|
||||||
|
|
||||||
result = web_client.get_self()
|
|
||||||
requests_mock.assert_called_once_with(web_client._ahriman_url)
|
|
||||||
assert result.status == BuildStatusEnum.Unknown
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress any exception happened during service status getting
|
|
||||||
"""
|
|
||||||
mocker.patch("requests.Session.get", side_effect=Exception())
|
|
||||||
assert web_client.get_self().status == BuildStatusEnum.Unknown
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress HTTP exception happened during service status getting
|
|
||||||
"""
|
|
||||||
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
|
|
||||||
assert web_client.get_self().status == BuildStatusEnum.Unknown
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
@ -320,9 +320,11 @@ def test_walk(resource_path_root: Path) -> None:
|
|||||||
resource_path_root / "models" / "package_gcc10_srcinfo",
|
resource_path_root / "models" / "package_gcc10_srcinfo",
|
||||||
resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo",
|
resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo",
|
||||||
resource_path_root / "models" / "package_yay_srcinfo",
|
resource_path_root / "models" / "package_yay_srcinfo",
|
||||||
|
resource_path_root / "web" / "templates" / "build-status" / "failed-modal.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "package-actions-modals.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "package-actions-script.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "success-modal.jinja2",
|
||||||
|
resource_path_root / "web" / "templates" / "build-status" / "table.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "static" / "favicon.ico",
|
resource_path_root / "web" / "templates" / "static" / "favicon.ico",
|
||||||
resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
|
resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
||||||
|
@ -54,7 +54,8 @@ def internal_status(counters: Counters) -> InternalStatus:
|
|||||||
Returns:
|
Returns:
|
||||||
InternalStatus: internal status test instance
|
InternalStatus: internal status test instance
|
||||||
"""
|
"""
|
||||||
return InternalStatus(architecture="x86_64",
|
return InternalStatus(status=BuildStatus(),
|
||||||
|
architecture="x86_64",
|
||||||
packages=counters,
|
packages=counters,
|
||||||
version=version.__version__,
|
version=version.__version__,
|
||||||
repository="aur-clone")
|
repository="aur-clone")
|
||||||
|
@ -4,31 +4,6 @@ import time
|
|||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
|
||||||
|
|
||||||
def test_build_status_enum_badges_color() -> None:
|
|
||||||
"""
|
|
||||||
status color must be one of shields.io supported
|
|
||||||
"""
|
|
||||||
SUPPORTED_COLORS = [
|
|
||||||
"brightgreen", "green", "yellowgreen", "yellow", "orange", "red", "blue", "lightgrey",
|
|
||||||
"success", "important", "critical", "informational", "inactive", "blueviolet"
|
|
||||||
]
|
|
||||||
|
|
||||||
for status in BuildStatusEnum:
|
|
||||||
assert status.badges_color() in SUPPORTED_COLORS
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_status_enum_bootstrap_color() -> None:
|
|
||||||
"""
|
|
||||||
status color must be one of bootstrap supported
|
|
||||||
"""
|
|
||||||
SUPPORTED_COLORS = [
|
|
||||||
"primary", "secondary", "success", "danger", "warning", "info", "light", "dark"
|
|
||||||
]
|
|
||||||
|
|
||||||
for status in BuildStatusEnum:
|
|
||||||
assert status.bootstrap_color() in SUPPORTED_COLORS
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_status_init_1() -> None:
|
def test_build_status_init_1() -> None:
|
||||||
"""
|
"""
|
||||||
must construct status object from None
|
must construct status object from None
|
||||||
|
@ -6,9 +6,10 @@ def test_from_option(user: User) -> None:
|
|||||||
"""
|
"""
|
||||||
must generate user from options
|
must generate user from options
|
||||||
"""
|
"""
|
||||||
|
user.access = UserAccess.Read
|
||||||
assert User.from_option(user.username, user.password) == user
|
assert User.from_option(user.username, user.password) == user
|
||||||
# default is read access
|
# default is read access
|
||||||
user.access = UserAccess.Write
|
user.access = UserAccess.Full
|
||||||
assert User.from_option(user.username, user.password) != user
|
assert User.from_option(user.username, user.password) != user
|
||||||
assert User.from_option(user.username, user.password, user.access) == user
|
assert User.from_option(user.username, user.password, user.access) == user
|
||||||
|
|
||||||
@ -72,16 +73,16 @@ def test_verify_access_read(user: User) -> None:
|
|||||||
"""
|
"""
|
||||||
user.access = UserAccess.Read
|
user.access = UserAccess.Read
|
||||||
assert user.verify_access(UserAccess.Read)
|
assert user.verify_access(UserAccess.Read)
|
||||||
assert not user.verify_access(UserAccess.Write)
|
assert not user.verify_access(UserAccess.Full)
|
||||||
|
|
||||||
|
|
||||||
def test_verify_access_write(user: User) -> None:
|
def test_verify_access_write(user: User) -> None:
|
||||||
"""
|
"""
|
||||||
user with write access must be able to do anything
|
user with write access must be able to do anything
|
||||||
"""
|
"""
|
||||||
user.access = UserAccess.Write
|
user.access = UserAccess.Full
|
||||||
assert user.verify_access(UserAccess.Read)
|
assert user.verify_access(UserAccess.Read)
|
||||||
assert user.verify_access(UserAccess.Write)
|
assert user.verify_access(UserAccess.Full)
|
||||||
|
|
||||||
|
|
||||||
def test_repr(user: User) -> None:
|
def test_repr(user: User) -> None:
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_full() -> None:
|
||||||
|
"""
|
||||||
|
full access must allow everything
|
||||||
|
"""
|
||||||
|
assert UserAccess.Full.permits(UserAccess.Full)
|
||||||
|
assert UserAccess.Full.permits(UserAccess.Reporter)
|
||||||
|
assert UserAccess.Full.permits(UserAccess.Read)
|
||||||
|
assert UserAccess.Full.permits(UserAccess.Unauthorized)
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_reporter() -> None:
|
||||||
|
"""
|
||||||
|
reporter access must allow everything except full
|
||||||
|
"""
|
||||||
|
assert not UserAccess.Reporter.permits(UserAccess.Full)
|
||||||
|
assert UserAccess.Reporter.permits(UserAccess.Reporter)
|
||||||
|
assert UserAccess.Reporter.permits(UserAccess.Read)
|
||||||
|
assert UserAccess.Reporter.permits(UserAccess.Unauthorized)
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_read() -> None:
|
||||||
|
"""
|
||||||
|
read access must allow read only and unauthorized
|
||||||
|
"""
|
||||||
|
assert not UserAccess.Read.permits(UserAccess.Full)
|
||||||
|
assert not UserAccess.Read.permits(UserAccess.Reporter)
|
||||||
|
assert UserAccess.Read.permits(UserAccess.Read)
|
||||||
|
assert UserAccess.Read.permits(UserAccess.Unauthorized)
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_unauthorized() -> None:
|
||||||
|
"""
|
||||||
|
unauthorized access must only allow unauthorized
|
||||||
|
"""
|
||||||
|
assert not UserAccess.Unauthorized.permits(UserAccess.Full)
|
||||||
|
assert not UserAccess.Unauthorized.permits(UserAccess.Reporter)
|
||||||
|
assert not UserAccess.Unauthorized.permits(UserAccess.Read)
|
||||||
|
assert UserAccess.Unauthorized.permits(UserAccess.Unauthorized)
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_unknown(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must return False in case if input does not match
|
||||||
|
"""
|
||||||
|
mocker.patch.object(UserAccess, "_member_names_", ["Read"])
|
||||||
|
assert not UserAccess.Full.permits(UserAccess.Unauthorized)
|
@ -1,12 +1,16 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from asyncio import BaseEventLoop
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import ahriman.core.auth.helpers
|
import ahriman.core.auth.helpers
|
||||||
|
|
||||||
|
from ahriman.core.auth import OAuth
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database import SQLite
|
from ahriman.core.database import SQLite
|
||||||
from ahriman.core.spawn import Spawn
|
from ahriman.core.spawn import Spawn
|
||||||
@ -105,3 +109,61 @@ def application_with_debug(configuration: Configuration, user: User, spawner: Sp
|
|||||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||||
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
mocker.patch.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
||||||
return setup_service("x86_64", configuration, spawner)
|
return setup_service("x86_64", configuration, spawner)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(application: web.Application, event_loop: BaseEventLoop,
|
||||||
|
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||||
|
"""
|
||||||
|
web client fixture
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(web.Application): application fixture
|
||||||
|
event_loop(BaseEventLoop): context event loop
|
||||||
|
aiohttp_client(Any): aiohttp client fixture
|
||||||
|
mocker(MockerFixture): mocker object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TestClient: web client test instance
|
||||||
|
"""
|
||||||
|
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
||||||
|
return event_loop.run_until_complete(aiohttp_client(application))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_with_auth(application_with_auth: web.Application, event_loop: BaseEventLoop,
|
||||||
|
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||||
|
"""
|
||||||
|
web client fixture with full authorization functions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application_with_auth(web.Application): application fixture
|
||||||
|
event_loop(BaseEventLoop): context event loop
|
||||||
|
aiohttp_client(Any): aiohttp client fixture
|
||||||
|
mocker(MockerFixture): mocker object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TestClient: web client test instance
|
||||||
|
"""
|
||||||
|
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
||||||
|
return event_loop.run_until_complete(aiohttp_client(application_with_auth))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_with_oauth_auth(application_with_auth: web.Application, event_loop: BaseEventLoop,
|
||||||
|
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
||||||
|
"""
|
||||||
|
web client fixture with full authorization functions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application_with_auth(web.Application): application fixture
|
||||||
|
event_loop(BaseEventLoop): context event loop
|
||||||
|
aiohttp_client(Any): aiohttp client fixture
|
||||||
|
mocker(MockerFixture): mocker object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TestClient: web client test instance
|
||||||
|
"""
|
||||||
|
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
||||||
|
application_with_auth["validator"] = MagicMock(spec=OAuth)
|
||||||
|
return event_loop.run_until_complete(aiohttp_client(application_with_auth))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
@ -63,11 +64,43 @@ async def test_auth_handler_api(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Read
|
request_handler.get_permission.return_value = UserAccess.Read
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler()
|
handler = auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_handler_static(client_with_auth: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must allow static calls
|
||||||
|
"""
|
||||||
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
await client_with_auth.get("/static/favicon.ico")
|
||||||
|
check_permission_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_handler_unauthorized(client_with_auth: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must allow pages with unauthorized access
|
||||||
|
"""
|
||||||
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
await client_with_auth.get("/")
|
||||||
|
check_permission_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_handler_allow_read_only(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must allow pages with allow read only flag
|
||||||
|
"""
|
||||||
|
aiohttp_request = pytest.helpers.request("", "/status-api", "GET")
|
||||||
|
request_handler = AsyncMock()
|
||||||
|
request_handler.get_permission.return_value = UserAccess.Read
|
||||||
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
|
handler = auth_handler(allow_read_only=True)
|
||||||
|
await handler(aiohttp_request, request_handler)
|
||||||
|
check_permission_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
async def test_auth_handler_api_no_method(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
|
must ask for write permission if handler does not have get_permission method
|
||||||
@ -77,9 +110,9 @@ async def test_auth_handler_api_no_method(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission = None
|
request_handler.get_permission = None
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler()
|
handler = auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
||||||
|
|
||||||
|
|
||||||
async def test_auth_handler_api_post(mocker: MockerFixture) -> None:
|
async def test_auth_handler_api_post(mocker: MockerFixture) -> None:
|
||||||
@ -88,12 +121,12 @@ async def test_auth_handler_api_post(mocker: MockerFixture) -> None:
|
|||||||
"""
|
"""
|
||||||
aiohttp_request = pytest.helpers.request("", "/status-api", "POST")
|
aiohttp_request = pytest.helpers.request("", "/status-api", "POST")
|
||||||
request_handler = AsyncMock()
|
request_handler = AsyncMock()
|
||||||
request_handler.get_permission.return_value = UserAccess.Write
|
request_handler.get_permission.return_value = UserAccess.Full
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler()
|
handler = auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
||||||
|
|
||||||
|
|
||||||
async def test_auth_handler_read(mocker: MockerFixture) -> None:
|
async def test_auth_handler_read(mocker: MockerFixture) -> None:
|
||||||
@ -106,7 +139,7 @@ async def test_auth_handler_read(mocker: MockerFixture) -> None:
|
|||||||
request_handler.get_permission.return_value = UserAccess.Read
|
request_handler.get_permission.return_value = UserAccess.Read
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler()
|
handler = auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Read, aiohttp_request.path)
|
||||||
|
|
||||||
@ -118,12 +151,12 @@ async def test_auth_handler_write(mocker: MockerFixture) -> None:
|
|||||||
for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"):
|
for method in ("CONNECT", "DELETE", "PATCH", "POST", "PUT", "TRACE"):
|
||||||
aiohttp_request = pytest.helpers.request("", "", method)
|
aiohttp_request = pytest.helpers.request("", "", method)
|
||||||
request_handler = AsyncMock()
|
request_handler = AsyncMock()
|
||||||
request_handler.get_permission.return_value = UserAccess.Write
|
request_handler.get_permission.return_value = UserAccess.Full
|
||||||
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
check_permission_mock = mocker.patch("aiohttp_security.check_permission")
|
||||||
|
|
||||||
handler = auth_handler()
|
handler = auth_handler(allow_read_only=False)
|
||||||
await handler(aiohttp_request, request_handler)
|
await handler(aiohttp_request, request_handler)
|
||||||
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Write, aiohttp_request.path)
|
check_permission_mock.assert_called_once_with(aiohttp_request, UserAccess.Full, aiohttp_request.path)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_auth(application_with_auth: web.Application, auth: Auth, mocker: MockerFixture) -> None:
|
def test_setup_auth(application_with_auth: web.Application, auth: Auth, mocker: MockerFixture) -> None:
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from asyncio import BaseEventLoop
|
|
||||||
from aiohttp.test_utils import TestClient
|
|
||||||
from pytest_mock import MockerFixture
|
|
||||||
from typing import Any
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
from ahriman.core.auth import OAuth
|
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@ -23,61 +17,3 @@ def base(application: web.Application) -> BaseView:
|
|||||||
BaseView: generated base view fixture
|
BaseView: generated base view fixture
|
||||||
"""
|
"""
|
||||||
return BaseView(pytest.helpers.request(application, "", ""))
|
return BaseView(pytest.helpers.request(application, "", ""))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def client(application: web.Application, event_loop: BaseEventLoop,
|
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
|
||||||
"""
|
|
||||||
web client fixture
|
|
||||||
|
|
||||||
Args:
|
|
||||||
application(web.Application): application fixture
|
|
||||||
event_loop(BaseEventLoop): context event loop
|
|
||||||
aiohttp_client(Any): aiohttp client fixture
|
|
||||||
mocker(MockerFixture): mocker object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
TestClient: web client test instance
|
|
||||||
"""
|
|
||||||
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
|
||||||
return event_loop.run_until_complete(aiohttp_client(application))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def client_with_auth(application_with_auth: web.Application, event_loop: BaseEventLoop,
|
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
|
||||||
"""
|
|
||||||
web client fixture with full authorization functions
|
|
||||||
|
|
||||||
Args:
|
|
||||||
application_with_auth(web.Application): application fixture
|
|
||||||
event_loop(BaseEventLoop): context event loop
|
|
||||||
aiohttp_client(Any): aiohttp client fixture
|
|
||||||
mocker(MockerFixture): mocker object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
TestClient: web client test instance
|
|
||||||
"""
|
|
||||||
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
|
||||||
return event_loop.run_until_complete(aiohttp_client(application_with_auth))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def client_with_oauth_auth(application_with_auth: web.Application, event_loop: BaseEventLoop,
|
|
||||||
aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
|
|
||||||
"""
|
|
||||||
web client fixture with full authorization functions
|
|
||||||
|
|
||||||
Args:
|
|
||||||
application_with_auth(web.Application): application fixture
|
|
||||||
event_loop(BaseEventLoop): context event loop
|
|
||||||
aiohttp_client(Any): aiohttp client fixture
|
|
||||||
mocker(MockerFixture): mocker object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
TestClient: web client test instance
|
|
||||||
"""
|
|
||||||
mocker.patch("pathlib.Path.iterdir", return_value=[])
|
|
||||||
application_with_auth["validator"] = MagicMock(spec=OAuth)
|
|
||||||
return event_loop.run_until_complete(aiohttp_client(application_with_auth))
|
|
||||||
|
@ -13,7 +13,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await AddView.get_permission(request) == UserAccess.Write
|
assert await AddView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
@ -13,7 +13,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await RemoveView.get_permission(request) == UserAccess.Write
|
assert await RemoveView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
@ -13,7 +13,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await RequestView.get_permission(request) == UserAccess.Read
|
assert await RequestView.get_permission(request) == UserAccess.Reporter
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
async def test_post(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
@ -14,7 +14,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET", "HEAD"):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await SearchView.get_permission(request) == UserAccess.Read
|
assert await SearchView.get_permission(request) == UserAccess.Reporter
|
||||||
|
|
||||||
|
|
||||||
async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
|
async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
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:
|
|
||||||
"""
|
|
||||||
must return valid service status
|
|
||||||
"""
|
|
||||||
response = await client.get("/status-api/v1/ahriman")
|
|
||||||
status = BuildStatus.from_json(await response.json())
|
|
||||||
|
|
||||||
assert response.ok
|
|
||||||
assert status.status == BuildStatusEnum.Unknown
|
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client: TestClient) -> None:
|
|
||||||
"""
|
|
||||||
must update service status correctly
|
|
||||||
"""
|
|
||||||
payload = {"status": BuildStatusEnum.Success.value}
|
|
||||||
post_response = await client.post("/status-api/v1/ahriman", json=payload)
|
|
||||||
assert post_response.status == 204
|
|
||||||
|
|
||||||
response = await client.get("/status-api/v1/ahriman")
|
|
||||||
status = BuildStatus.from_json(await response.json())
|
|
||||||
|
|
||||||
assert response.ok
|
|
||||||
assert status.status == BuildStatusEnum.Success
|
|
||||||
|
|
||||||
|
|
||||||
async def test_post_exception(client: TestClient) -> None:
|
|
||||||
"""
|
|
||||||
must raise exception on invalid payload
|
|
||||||
"""
|
|
||||||
post_response = await client.post("/status-api/v1/ahriman", json={})
|
|
||||||
assert post_response.status == 400
|
|
||||||
|
|
||||||
|
|
||||||
async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
exception handler must handle 500 errors
|
|
||||||
"""
|
|
||||||
payload = {"status": BuildStatusEnum.Success.value}
|
|
||||||
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
|
|
||||||
|
|
||||||
post_response = await client.post("/status-api/v1/ahriman", json=payload)
|
|
||||||
assert post_response.status == 500
|
|
@ -17,7 +17,7 @@ async def test_get_permission() -> None:
|
|||||||
assert await PackageView.get_permission(request) == UserAccess.Read
|
assert await PackageView.get_permission(request) == UserAccess.Read
|
||||||
for method in ("DELETE", "POST"):
|
for method in ("DELETE", "POST"):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await PackageView.get_permission(request) == UserAccess.Write
|
assert await PackageView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
|
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||||
|
@ -18,7 +18,7 @@ async def test_get_permission() -> None:
|
|||||||
assert await PackagesView.get_permission(request) == UserAccess.Read
|
assert await PackagesView.get_permission(request) == UserAccess.Read
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await PackagesView.get_permission(request) == UserAccess.Write
|
assert await PackagesView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
|
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
import ahriman.version as version
|
import ahriman.version as version
|
||||||
|
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.status.status import StatusView
|
from ahriman.web.views.status.status import StatusView
|
||||||
@ -17,6 +19,9 @@ async def test_get_permission() -> None:
|
|||||||
for method in ("GET", "HEAD"):
|
for method in ("GET", "HEAD"):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await StatusView.get_permission(request) == UserAccess.Read
|
assert await StatusView.get_permission(request) == UserAccess.Read
|
||||||
|
for method in ("POST",):
|
||||||
|
request = pytest.helpers.request("", "", method)
|
||||||
|
assert await StatusView.get_permission(request) == UserAccess.Full
|
||||||
|
|
||||||
|
|
||||||
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||||
@ -33,3 +38,37 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
|||||||
assert json["version"] == version.__version__
|
assert json["version"] == version.__version__
|
||||||
assert json["packages"]
|
assert json["packages"]
|
||||||
assert json["packages"]["total"] == 1
|
assert json["packages"]["total"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must update service status correctly
|
||||||
|
"""
|
||||||
|
payload = {"status": BuildStatusEnum.Success.value}
|
||||||
|
post_response = await client.post("/status-api/v1/status", json=payload)
|
||||||
|
assert post_response.status == 204
|
||||||
|
|
||||||
|
response = await client.get("/status-api/v1/status")
|
||||||
|
status = InternalStatus.from_json(await response.json())
|
||||||
|
|
||||||
|
assert response.ok
|
||||||
|
assert status.status.status == BuildStatusEnum.Success
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_exception(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must raise exception on invalid payload
|
||||||
|
"""
|
||||||
|
post_response = await client.post("/status-api/v1/status", json={})
|
||||||
|
assert post_response.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_exception_inside(client: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
exception handler must handle 500 errors
|
||||||
|
"""
|
||||||
|
payload = {"status": BuildStatusEnum.Success.value}
|
||||||
|
mocker.patch("ahriman.core.status.watcher.Watcher.update_self", side_effect=Exception())
|
||||||
|
|
||||||
|
post_response = await client.post("/status-api/v1/status", json=payload)
|
||||||
|
assert post_response.status == 500
|
||||||
|
@ -12,7 +12,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("GET", "HEAD"):
|
for method in ("GET", "HEAD"):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await IndexView.get_permission(request) == UserAccess.Safe
|
assert await IndexView.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
async def test_get(client_with_auth: TestClient) -> None:
|
async def test_get(client_with_auth: TestClient) -> None:
|
||||||
|
@ -14,7 +14,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("GET", "POST"):
|
for method in ("GET", "POST"):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await LoginView.get_permission(request) == UserAccess.Safe
|
assert await LoginView.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
async def test_get_default_validator(client_with_auth: TestClient) -> None:
|
async def test_get_default_validator(client_with_auth: TestClient) -> None:
|
||||||
|
@ -14,7 +14,7 @@ async def test_get_permission() -> None:
|
|||||||
"""
|
"""
|
||||||
for method in ("POST",):
|
for method in ("POST",):
|
||||||
request = pytest.helpers.request("", "", method)
|
request = pytest.helpers.request("", "", method)
|
||||||
assert await LogoutView.get_permission(request) == UserAccess.Safe
|
assert await LogoutView.get_permission(request) == UserAccess.Unauthorized
|
||||||
|
|
||||||
|
|
||||||
async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None:
|
async def test_post(client_with_auth: TestClient, mocker: MockerFixture) -> None:
|
||||||
|
@ -14,7 +14,7 @@ client_secret = client_secret
|
|||||||
oauth_provider = GoogleClient
|
oauth_provider = GoogleClient
|
||||||
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
|
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
|
||||||
salt = salt
|
salt = salt
|
||||||
safe_build_status = no
|
allow_read_only = no
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
archbuild_flags =
|
archbuild_flags =
|
||||||
|
Loading…
Reference in New Issue
Block a user