mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
parent
5674b7b388
commit
47de715d7d
@ -3,7 +3,7 @@
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
.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
|
||||
ArcH Linux ReposItory MANager
|
||||
|
||||
@ -97,6 +97,9 @@ rebuild repository
|
||||
\fBahriman\fR \fI\,repo-remove-unknown\/\fR
|
||||
remove unknown packages
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-report\/\fR
|
||||
generate report
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-restore\/\fR
|
||||
restore repository data
|
||||
.TP
|
||||
@ -109,6 +112,9 @@ sign packages
|
||||
\fBahriman\fR \fI\,repo-status-update\/\fR
|
||||
update repository status
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-sync\/\fR
|
||||
sync repository
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-triggers\/\fR
|
||||
run triggers
|
||||
.TP
|
||||
@ -405,6 +411,11 @@ just perform check for packages without removal
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
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
|
||||
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
|
||||
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
|
||||
usage: ahriman repo-triggers [-h]
|
||||
usage: ahriman repo-triggers [-h] [trigger ...]
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
.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
|
||||
|
||||
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.
|
||||
|
||||
.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
|
||||
|
||||
.TP
|
||||
@ -553,7 +574,8 @@ user access level
|
||||
set file permissions to user\-only
|
||||
|
||||
.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
|
||||
|
||||
@ -567,7 +589,7 @@ filter users by username
|
||||
return non\-zero exit status if result is empty
|
||||
|
||||
.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
|
||||
|
||||
.SH COMMAND \fI\,'ahriman user-remove'\/\fR
|
||||
|
@ -20,6 +20,14 @@ ahriman.core.database.migrations.m001\_package\_source module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
---------------
|
||||
|
||||
|
@ -4,14 +4,6 @@ ahriman.web.views.status package
|
||||
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
|
||||
---------------------------------------
|
||||
|
||||
|
@ -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.
|
||||
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
|
||||
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
|
||||
* ``safe_build_status`` - allow requesting status page without authorization, boolean, required.
|
||||
* ``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).
|
||||
|
||||
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")
|
||||
|
||||
def run(self, result, packages):
|
||||
notify(result, self.slack_url, channel, username)
|
||||
notify(result, self.slack_url, self.channel, self.username)
|
||||
|
||||
Setup the trigger
|
||||
-----------------
|
||||
|
@ -13,7 +13,7 @@ target = disabled
|
||||
max_age = 604800
|
||||
oauth_provider = GoogleClient
|
||||
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
|
||||
safe_build_status = yes
|
||||
allow_read_only = yes
|
||||
|
||||
[build]
|
||||
archbuild_flags =
|
||||
@ -37,7 +37,6 @@ target = console
|
||||
use_utf = yes
|
||||
|
||||
[email]
|
||||
full_template_path = /usr/share/ahriman/templates/repo-index.jinja2
|
||||
no_empty_report = yes
|
||||
template_path = /usr/share/ahriman/templates/email-index.jinja2
|
||||
ssl = disabled
|
||||
|
@ -14,28 +14,29 @@
|
||||
|
||||
<div class="container">
|
||||
<h1>ahriman
|
||||
{% if auth.authenticated %}
|
||||
<img src="https://img.shields.io/badge/version-{{ version }}-informational" alt="{{ version }}">
|
||||
<img src="https://img.shields.io/badge/repository-{{ repository | replace("-", "--") }}-informational" alt="{{ repository }}">
|
||||
<img src="https://img.shields.io/badge/architecture-{{ architecture }}-informational" alt="{{ architecture }}">
|
||||
<img src="https://img.shields.io/badge/service%20status-{{ service.status }}-{{ service.status_color }}" alt="{{ service.status }}" title="{{ service.timestamp }}">
|
||||
{% endif %}
|
||||
<img id="badge-version" src="https://img.shields.io/badge/version-unknown-informational" alt="unknown">
|
||||
<img id="badge-repository" src="https://img.shields.io/badge/repository-unknown-informational" alt="unknown">
|
||||
<img id="badge-architecture" src="https://img.shields.io/badge/architecture-unknown-informational" alt="unknown">
|
||||
<img id="badge-status" src="https://img.shields.io/badge/service%20status-unknown-inactive" alt="unknown">
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
{% 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">
|
||||
<i class="fa fa-plus"></i> add
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled>
|
||||
<i class="fa fa-play"></i> update
|
||||
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" disabled hidden>
|
||||
<i class="bi bi-play"></i> update
|
||||
</button>
|
||||
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled>
|
||||
<i class="fa fa-trash"></i> remove
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="packages" class="table table-striped table-hover"
|
||||
@ -53,42 +54,22 @@
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-sortable="true"
|
||||
data-sort-reset="true"
|
||||
data-sort-name="base"
|
||||
data-sort-order="asc"
|
||||
data-toggle="table"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false">package base</th>
|
||||
<th data-sortable="true">version</th>
|
||||
<th data-sortable="true">packages</th>
|
||||
<th data-sortable="true" data-visible="false">groups</th>
|
||||
<th data-sortable="true" data-visible="false">licenses</th>
|
||||
<th data-sortable="true">last update</th>
|
||||
<th data-sortable="true">status</th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="base">package base</th>
|
||||
<th data-sortable="true" data-field="version">version</th>
|
||||
<th data-sortable="true" data-field="packages">packages</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="groups">groups</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="licenses">licenses</th>
|
||||
<th data-sortable="true" data-field="timestamp">last update</th>
|
||||
<th data-sortable="true" data-cell-style="statusFormat" data-field="status">status</th>
|
||||
</tr>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@ -122,11 +103,14 @@
|
||||
{% include "build-status/login-modal.jinja2" %}
|
||||
{% endif %}
|
||||
|
||||
{% include "build-status/package-actions-modals.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>
|
||||
|
||||
|
@ -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>
|
@ -26,4 +26,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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-search-clear-button="true"
|
||||
data-sortable="true"
|
||||
data-sort-reset="true"
|
||||
data-sort-name="base"
|
||||
data-sort-order="asc"
|
||||
data-toggle="table">
|
||||
<thead class="table-primary">
|
||||
<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://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://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.js"></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.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>
|
||||
$("#packages").bootstrapTable({
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script src="https://kit.fontawesome.com/0d6d6d5226.js" crossorigin="anonymous"></script>
|
||||
<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@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<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">
|
||||
|
||||
|
6
setup.py
6
setup.py
@ -69,9 +69,11 @@ setup(
|
||||
"package/share/ahriman/templates/telegram-index.jinja2",
|
||||
]),
|
||||
("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/package-actions-modals.jinja2",
|
||||
"package/share/ahriman/templates/build-status/package-actions-script.jinja2",
|
||||
"package/share/ahriman/templates/build-status/package-add-modal.jinja2",
|
||||
"package/share/ahriman/templates/build-status/success-modal.jinja2",
|
||||
"package/share/ahriman/templates/build-status/table.jinja2",
|
||||
]),
|
||||
("share/ahriman/templates/static", [
|
||||
"package/share/ahriman/templates/static/favicon.ico",
|
||||
|
@ -52,8 +52,8 @@ class Status(Handler):
|
||||
# we are using reporter here
|
||||
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter
|
||||
if args.ahriman:
|
||||
ahriman = client.get_self()
|
||||
StatusPrinter(ahriman).print(args.info)
|
||||
service_status = client.get_internal()
|
||||
StatusPrinter(service_status.status).print(args.info)
|
||||
if args.package:
|
||||
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
|
||||
[client.get(base) for base in args.package],
|
||||
|
@ -37,7 +37,7 @@ class Auth:
|
||||
enabled(bool): indicates if authorization is enabled
|
||||
logger(logging.Logger): class logger
|
||||
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:
|
||||
@ -50,7 +50,7 @@ class Auth:
|
||||
"""
|
||||
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.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:
|
||||
InternalStatus: current internal (web) service status
|
||||
"""
|
||||
return InternalStatus()
|
||||
|
||||
def get_self(self) -> BuildStatus: # pylint: disable=no-self-use
|
||||
"""
|
||||
get ahriman status itself
|
||||
|
||||
Returns:
|
||||
BuildStatus: current ahriman status
|
||||
"""
|
||||
return BuildStatus()
|
||||
return InternalStatus(BuildStatus())
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
|
@ -57,16 +57,6 @@ class WebClient(Client):
|
||||
self.__session = requests.session()
|
||||
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
|
||||
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))
|
||||
except Exception:
|
||||
self.logger.exception("could not get web service status")
|
||||
return InternalStatus()
|
||||
|
||||
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()
|
||||
return InternalStatus(BuildStatus())
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
@ -265,7 +236,7 @@ class WebClient(Client):
|
||||
payload = {"status": status.value}
|
||||
|
||||
try:
|
||||
response = self.__session.post(self._ahriman_url, json=payload)
|
||||
response = self.__session.post(self._status_url, json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not update service status: %s", exception_response_text(e))
|
||||
|
@ -46,40 +46,6 @@ class BuildStatusEnum(str, Enum):
|
||||
Failed = "failed"
|
||||
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
|
||||
class BuildStatus:
|
||||
|
@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.counters import Counters
|
||||
|
||||
|
||||
@ -31,12 +32,14 @@ class InternalStatus:
|
||||
internal server status
|
||||
|
||||
Attributes:
|
||||
status(BuildStatus): service status
|
||||
architecture(Optional[str]): repository architecture
|
||||
packages(Counters): packages statuses counter object
|
||||
repository(Optional[str]): repository name
|
||||
version(Optional[str]): service version
|
||||
"""
|
||||
|
||||
status: BuildStatus
|
||||
architecture: Optional[str] = None
|
||||
packages: Counters = field(default=Counters(total=0))
|
||||
repository: Optional[str] = None
|
||||
@ -54,7 +57,8 @@ class InternalStatus:
|
||||
InternalStatus: internal status
|
||||
"""
|
||||
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,
|
||||
repository=dump.get("repository"),
|
||||
version=dump.get("version"))
|
||||
|
@ -142,9 +142,7 @@ class User:
|
||||
Returns:
|
||||
bool: True in case if user is allowed to do this request and False otherwise
|
||||
"""
|
||||
if self.access == UserAccess.Write:
|
||||
return True # everything is allowed
|
||||
return self.access == required
|
||||
return self.access.permits(required)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@ -25,12 +27,31 @@ class UserAccess(str, Enum):
|
||||
web user access enumeration
|
||||
|
||||
Attributes:
|
||||
Safe(UserAccess): (class attribute) user can access the page without authorization,
|
||||
should not be used for user configuration
|
||||
Unauthorized(UserAccess): (class attribute) user can access specific resources which are marked as available
|
||||
without authorization (e.g. login, logout, static)
|
||||
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"
|
||||
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)
|
||||
|
||||
|
||||
def auth_handler() -> MiddlewareType:
|
||||
def auth_handler(allow_read_only: bool) -> MiddlewareType:
|
||||
"""
|
||||
authorization and authentication middleware
|
||||
|
||||
Args:
|
||||
allow_read_only: allow
|
||||
|
||||
Returns:
|
||||
MiddlewareType: built middleware
|
||||
"""
|
||||
@ -102,10 +105,14 @@ def auth_handler() -> MiddlewareType:
|
||||
permission = await permission_method(request)
|
||||
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
|
||||
handler_instance = getattr(handler, "__self__", None)
|
||||
permission = UserAccess.Safe if isinstance(handler_instance, StaticResource) else UserAccess.Write
|
||||
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:
|
||||
permission = UserAccess.Write
|
||||
if permission != UserAccess.Safe:
|
||||
await aiohttp_security.check_permission(request, permission, request.path)
|
||||
|
||||
return await handler(request)
|
||||
@ -133,6 +140,6 @@ def setup_auth(application: web.Application, validator: Auth) -> web.Application
|
||||
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
||||
|
||||
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
||||
application.middlewares.append(auth_handler())
|
||||
application.middlewares.append(auth_handler(validator.allow_read_only))
|
||||
|
||||
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.request import RequestView
|
||||
from ahriman.web.views.service.search import SearchView
|
||||
from ahriman.web.views.status.ahriman import AhrimanView
|
||||
from ahriman.web.views.status.package import PackageView
|
||||
from ahriman.web.views.status.packages import PackagesView
|
||||
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
|
||||
|
||||
* 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
|
||||
* 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
|
||||
* 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
|
||||
* 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_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_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_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_post("/user-api/v1/login", LoginView)
|
||||
|
@ -101,7 +101,7 @@ class BaseView(View):
|
||||
Returns:
|
||||
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
|
||||
|
||||
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 ahriman import version
|
||||
from ahriman.core.auth.helpers import authorized_userid
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
@ -34,37 +32,19 @@ class IndexView(BaseView):
|
||||
|
||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||
|
||||
* architecture - repository architecture, string, 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
|
||||
* enabled - whether authorization is enabled by configuration or not, boolean, required
|
||||
* username - authenticated username if any, string, null means not authenticated
|
||||
* index_url - url to the repository index, string, optional
|
||||
* packages - sorted list of packages properties, required
|
||||
* base, string
|
||||
* depends, sorted list of strings
|
||||
* 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
|
||||
* 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:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get 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")
|
||||
async def get(self) -> Dict[str, Any]:
|
||||
@ -74,43 +54,15 @@ class IndexView(BaseView):
|
||||
Returns:
|
||||
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)
|
||||
authenticated = not self.validator.enabled or self.validator.safe_build_status or auth_username is not None
|
||||
auth = {
|
||||
"authenticated": authenticated,
|
||||
"control": self.validator.auth_control,
|
||||
"enabled": self.validator.enabled,
|
||||
"username": auth_username,
|
||||
}
|
||||
|
||||
return {
|
||||
"architecture": self.service.architecture,
|
||||
"auth": auth,
|
||||
"index_url": self.configuration.get("web", "index_url", fallback=None),
|
||||
"packages": packages,
|
||||
"repository": self.service.repository.name,
|
||||
"service": service,
|
||||
"version": version.__version__,
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class AddView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Write
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
|
@ -31,7 +31,7 @@ class RemoveView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Write
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
|
@ -31,7 +31,7 @@ class RequestView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Read
|
||||
POST_PERMISSION = UserAccess.Reporter
|
||||
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
|
@ -35,7 +35,7 @@ class SearchView(BaseView):
|
||||
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:
|
||||
"""
|
||||
|
@ -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
|
||||
"""
|
||||
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Write
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
||||
|
||||
async def get(self) -> Response:
|
||||
|
@ -34,7 +34,7 @@ class PackagesView(BaseView):
|
||||
"""
|
||||
|
||||
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
|
||||
POST_PERMISSION = UserAccess.Write
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
|
@ -17,9 +17,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import Response, json_response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -33,9 +34,11 @@ class StatusView(BaseView):
|
||||
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.Full
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
@ -46,9 +49,34 @@ class StatusView(BaseView):
|
||||
"""
|
||||
counters = Counters.from_packages(self.service.packages)
|
||||
status = InternalStatus(
|
||||
status=self.service.status,
|
||||
architecture=self.service.architecture,
|
||||
packages=counters,
|
||||
repository=self.service.repository.name,
|
||||
version=version.__version__)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Safe
|
||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
|
||||
|
||||
async def get(self) -> None:
|
||||
"""
|
||||
|
@ -32,7 +32,7 @@ class LogoutView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Safe
|
||||
POST_PERMISSION = UserAccess.Unauthorized
|
||||
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
|
@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, package_ahr
|
||||
"""
|
||||
args = _default_args(args)
|
||||
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",
|
||||
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
|
||||
(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.exit_code = True
|
||||
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=[])
|
||||
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.exit_code = False
|
||||
args.password = "pa55w0rd"
|
||||
args.role = UserAccess.Read
|
||||
args.role = UserAccess.Reporter
|
||||
args.secure = False
|
||||
return args
|
||||
|
||||
|
@ -519,7 +519,7 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non
|
||||
"""
|
||||
args = parser.parse_args(["user-add", "username"])
|
||||
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)
|
||||
|
||||
|
||||
@ -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
|
||||
"""
|
||||
args = parser.parse_args(["user-list", "--role", "write"])
|
||||
args = parser.parse_args(["user-list", "--role", "full"])
|
||||
assert isinstance(args.role, UserAccess)
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@ from unittest import mock
|
||||
from ahriman import version
|
||||
from ahriman.application.lock import Lock
|
||||
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
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ def test_check_version(lock: Lock, mocker: MockerFixture) -> None:
|
||||
must check version correctly
|
||||
"""
|
||||
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")
|
||||
|
||||
lock.check_version()
|
||||
@ -69,7 +69,7 @@ def test_check_version_mismatch(lock: Lock, mocker: MockerFixture) -> None:
|
||||
must check mismatched version correctly
|
||||
"""
|
||||
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")
|
||||
|
||||
lock.check_version()
|
||||
|
@ -426,7 +426,7 @@ def user() -> User:
|
||||
Returns:
|
||||
User: user descriptor instance
|
||||
"""
|
||||
return User("user", "pa55w0rd", UserAccess.Read)
|
||||
return User("user", "pa55w0rd", UserAccess.Reporter)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -71,4 +71,4 @@ async def test_verify_access(auth: Auth, user: User) -> None:
|
||||
must allow any access
|
||||
"""
|
||||
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)
|
||||
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
|
||||
"""
|
||||
first = User("1", "", UserAccess.Read)
|
||||
second = User("2", "", UserAccess.Write)
|
||||
second = User("2", "", UserAccess.Full)
|
||||
third = User("3", "", UserAccess.Read)
|
||||
|
||||
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
|
||||
"""
|
||||
first = User("1", "", UserAccess.Read)
|
||||
second = User("2", "", UserAccess.Write)
|
||||
second = User("2", "", UserAccess.Full)
|
||||
third = User("3", "", UserAccess.Read)
|
||||
|
||||
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
|
||||
"""
|
||||
first = User("1", "", UserAccess.Read)
|
||||
second = User("2", "", UserAccess.Write)
|
||||
second = User("2", "", UserAccess.Full)
|
||||
third = User("3", "", UserAccess.Read)
|
||||
|
||||
database.user_update(first)
|
||||
@ -72,7 +72,7 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None:
|
||||
database.user_update(third)
|
||||
|
||||
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:
|
||||
@ -92,6 +92,6 @@ def test_user_update(database: SQLite, user: User) -> None:
|
||||
assert database.user_get(user.username) == user
|
||||
|
||||
new_user = user.hash_password("salt")
|
||||
new_user.access = UserAccess.Write
|
||||
new_user.access = UserAccess.Full
|
||||
database.user_update(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.status.client import Client
|
||||
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.package import Package
|
||||
|
||||
@ -51,14 +51,11 @@ def test_get_internal(client: Client) -> None:
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def test_get_self(client: Client) -> None:
|
||||
"""
|
||||
must return unknown status for service
|
||||
"""
|
||||
assert client.get_self().status == BuildStatusEnum.Unknown
|
||||
assert actual == expected
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
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:
|
||||
@ -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
|
||||
"""
|
||||
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
|
||||
assert web_client.get_internal() == InternalStatus()
|
||||
|
||||
|
||||
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
|
||||
assert web_client.get_internal().architecture is 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_tpacpi-bat-git_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" / "package-actions-modals.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "package-actions-script.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.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" / "utils" / "bootstrap-scripts.jinja2",
|
||||
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
||||
|
@ -54,7 +54,8 @@ def internal_status(counters: Counters) -> InternalStatus:
|
||||
Returns:
|
||||
InternalStatus: internal status test instance
|
||||
"""
|
||||
return InternalStatus(architecture="x86_64",
|
||||
return InternalStatus(status=BuildStatus(),
|
||||
architecture="x86_64",
|
||||
packages=counters,
|
||||
version=version.__version__,
|
||||
repository="aur-clone")
|
||||
|
@ -4,31 +4,6 @@ import time
|
||||
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:
|
||||
"""
|
||||
must construct status object from None
|
||||
|
@ -6,9 +6,10 @@ def test_from_option(user: User) -> None:
|
||||
"""
|
||||
must generate user from options
|
||||
"""
|
||||
user.access = UserAccess.Read
|
||||
assert User.from_option(user.username, user.password) == user
|
||||
# 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.access) == user
|
||||
|
||||
@ -72,16 +73,16 @@ def test_verify_access_read(user: User) -> None:
|
||||
"""
|
||||
user.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:
|
||||
"""
|
||||
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.Write)
|
||||
assert user.verify_access(UserAccess.Full)
|
||||
|
||||
|
||||
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
|
||||
|
||||
from asyncio import BaseEventLoop
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient
|
||||
from collections import namedtuple
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import ahriman.core.auth.helpers
|
||||
|
||||
from ahriman.core.auth import OAuth
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
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.object(ahriman.core.auth.helpers, "_has_aiohttp_security", False)
|
||||
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
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient
|
||||
from pytest_mock import MockerFixture
|
||||
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
|
||||
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)
|
||||
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:
|
||||
"""
|
||||
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
|
||||
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)
|
||||
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:
|
||||
@ -88,12 +121,12 @@ async def test_auth_handler_api_post(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
aiohttp_request = pytest.helpers.request("", "/status-api", "POST")
|
||||
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")
|
||||
|
||||
handler = auth_handler()
|
||||
handler = auth_handler(allow_read_only=False)
|
||||
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:
|
||||
@ -106,7 +139,7 @@ async def test_auth_handler_read(mocker: MockerFixture) -> None:
|
||||
request_handler.get_permission.return_value = UserAccess.Read
|
||||
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)
|
||||
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"):
|
||||
aiohttp_request = pytest.helpers.request("", "", method)
|
||||
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")
|
||||
|
||||
handler = auth_handler()
|
||||
handler = auth_handler(allow_read_only=False)
|
||||
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:
|
||||
|
@ -1,13 +1,7 @@
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -23,61 +17,3 @@ def base(application: web.Application) -> BaseView:
|
||||
BaseView: generated base view fixture
|
||||
"""
|
||||
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",):
|
||||
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:
|
||||
|
@ -13,7 +13,7 @@ async def test_get_permission() -> None:
|
||||
"""
|
||||
for method in ("POST",):
|
||||
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:
|
||||
|
@ -13,7 +13,7 @@ async def test_get_permission() -> None:
|
||||
"""
|
||||
for method in ("POST",):
|
||||
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:
|
||||
|
@ -14,7 +14,7 @@ async def test_get_permission() -> None:
|
||||
"""
|
||||
for method in ("GET", "HEAD"):
|
||||
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:
|
||||
|
@ -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
|
||||
for method in ("DELETE", "POST"):
|
||||
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:
|
||||
|
@ -18,7 +18,7 @@ async def test_get_permission() -> None:
|
||||
assert await PackagesView.get_permission(request) == UserAccess.Read
|
||||
for method in ("POST",):
|
||||
request = pytest.helpers.request("", "", method)
|
||||
assert await PackagesView.get_permission(request) == UserAccess.Write
|
||||
assert await PackagesView.get_permission(request) == UserAccess.Full
|
||||
|
||||
|
||||
async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
|
@ -1,10 +1,12 @@
|
||||
import pytest
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.status.status import StatusView
|
||||
@ -17,6 +19,9 @@ async def test_get_permission() -> None:
|
||||
for method in ("GET", "HEAD"):
|
||||
request = pytest.helpers.request("", "", method)
|
||||
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:
|
||||
@ -33,3 +38,37 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||
assert json["version"] == version.__version__
|
||||
assert json["packages"]
|
||||
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"):
|
||||
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:
|
||||
|
@ -14,7 +14,7 @@ async def test_get_permission() -> None:
|
||||
"""
|
||||
for method in ("GET", "POST"):
|
||||
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:
|
||||
|
@ -14,7 +14,7 @@ async def test_get_permission() -> None:
|
||||
"""
|
||||
for method in ("POST",):
|
||||
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:
|
||||
|
@ -14,7 +14,7 @@ client_secret = client_secret
|
||||
oauth_provider = GoogleClient
|
||||
oauth_scopes = https://www.googleapis.com/auth/userinfo.email
|
||||
salt = salt
|
||||
safe_build_status = no
|
||||
allow_read_only = no
|
||||
|
||||
[build]
|
||||
archbuild_flags =
|
||||
|
Loading…
Reference in New Issue
Block a user