From 47de715d7d1f3adbce643271524a0fe98112391c Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Fri, 20 May 2022 22:29:36 +0300 Subject: [PATCH] dynamic html load (#63) * dynamic html load * split by classes --- docs/ahriman.1 | 34 +++- docs/ahriman.core.database.migrations.rst | 8 + docs/ahriman.web.views.status.rst | 8 - docs/configuration.rst | 2 +- docs/triggers.rst | 2 +- package/share/ahriman/settings/ahriman.ini | 3 +- .../ahriman/templates/build-status.jinja2 | 72 ++++----- .../build-status/failed-modal.jinja2 | 28 ++++ .../templates/build-status/login-modal.jinja2 | 2 +- .../package-actions-modals.jinja2 | 60 ------- .../package-actions-script.jinja2 | 95 ----------- .../build-status/package-add-modal.jinja2 | 62 ++++++++ .../build-status/success-modal.jinja2 | 28 ++++ .../templates/build-status/table.jinja2 | 147 ++++++++++++++++++ .../share/ahriman/templates/repo-index.jinja2 | 3 +- .../templates/utils/bootstrap-scripts.jinja2 | 8 +- .../ahriman/templates/utils/style.jinja2 | 6 +- setup.py | 6 +- src/ahriman/application/handlers/status.py | 4 +- src/ahriman/core/auth/auth.py | 4 +- .../database/migrations/m002_user_access.py | 33 ++++ src/ahriman/core/status/client.py | 11 +- src/ahriman/core/status/web_client.py | 33 +--- src/ahriman/models/build_status.py | 34 ---- src/ahriman/models/internal_status.py | 6 +- src/ahriman/models/user.py | 4 +- src/ahriman/models/user_access.py | 31 +++- src/ahriman/web/middlewares/auth_handler.py | 17 +- src/ahriman/web/routes.py | 11 +- src/ahriman/web/views/base.py | 2 +- src/ahriman/web/views/index.py | 50 +----- src/ahriman/web/views/service/add.py | 2 +- src/ahriman/web/views/service/remove.py | 2 +- src/ahriman/web/views/service/request.py | 2 +- src/ahriman/web/views/service/search.py | 2 +- src/ahriman/web/views/status/ahriman.py | 71 --------- src/ahriman/web/views/status/package.py | 2 +- src/ahriman/web/views/status/packages.py | 2 +- src/ahriman/web/views/status/status.py | 30 +++- src/ahriman/web/views/user/login.py | 2 +- src/ahriman/web/views/user/logout.py | 2 +- .../handlers/test_handler_status.py | 4 +- .../handlers/test_handler_users.py | 2 +- tests/ahriman/application/test_ahriman.py | 4 +- tests/ahriman/application/test_lock.py | 6 +- tests/ahriman/conftest.py | 2 +- tests/ahriman/core/auth/test_auth.py | 2 +- tests/ahriman/core/auth/test_mapping.py | 2 +- .../migrations/test_m002_user_access.py | 8 + .../operations/test_auth_operations.py | 10 +- tests/ahriman/core/status/test_client.py | 13 +- tests/ahriman/core/status/test_web_client.py | 45 +----- tests/ahriman/core/test_util.py | 6 +- tests/ahriman/models/conftest.py | 3 +- tests/ahriman/models/test_build_status.py | 25 --- tests/ahriman/models/test_user.py | 9 +- tests/ahriman/models/test_user_access.py | 51 ++++++ tests/ahriman/web/conftest.py | 62 ++++++++ .../web/middlewares/test_auth_handler.py | 53 +++++-- tests/ahriman/web/views/conftest.py | 64 -------- .../views/service/test_views_service_add.py | 2 +- .../service/test_views_service_remove.py | 2 +- .../service/test_views_service_request.py | 2 +- .../service/test_views_service_search.py | 2 +- .../views/status/test_views_status_ahriman.py | 65 -------- .../views/status/test_views_status_package.py | 2 +- .../status/test_views_status_packages.py | 2 +- .../views/status/test_views_status_status.py | 39 +++++ tests/ahriman/web/views/test_views_index.py | 2 +- .../web/views/user/test_views_user_login.py | 2 +- .../web/views/user/test_views_user_logout.py | 2 +- tests/testresources/core/ahriman.ini | 2 +- 72 files changed, 720 insertions(+), 706 deletions(-) create mode 100644 package/share/ahriman/templates/build-status/failed-modal.jinja2 delete mode 100644 package/share/ahriman/templates/build-status/package-actions-modals.jinja2 delete mode 100644 package/share/ahriman/templates/build-status/package-actions-script.jinja2 create mode 100644 package/share/ahriman/templates/build-status/package-add-modal.jinja2 create mode 100644 package/share/ahriman/templates/build-status/success-modal.jinja2 create mode 100644 package/share/ahriman/templates/build-status/table.jinja2 create mode 100644 src/ahriman/core/database/migrations/m002_user_access.py delete mode 100644 src/ahriman/web/views/status/ahriman.py create mode 100644 tests/ahriman/core/database/migrations/test_m002_user_access.py delete mode 100644 tests/ahriman/web/views/status/test_views_status_ahriman.py diff --git a/docs/ahriman.1 b/docs/ahriman.1 index ddb1fd0b..8f690230 100644 --- a/docs/ahriman.1 +++ b/docs/ahriman.1 @@ -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 diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst index 2e3563a3..08dc66fb 100644 --- a/docs/ahriman.core.database.migrations.rst +++ b/docs/ahriman.core.database.migrations.rst @@ -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 --------------- diff --git a/docs/ahriman.web.views.status.rst b/docs/ahriman.web.views.status.rst index 334a9dd3..a5dcd410 100644 --- a/docs/ahriman.web.views.status.rst +++ b/docs/ahriman.web.views.status.rst @@ -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 --------------------------------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index 1bce3fac..c7356f1b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -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. diff --git a/docs/triggers.rst b/docs/triggers.rst index 7cd8d0ae..c38cee3f 100644 --- a/docs/triggers.rst +++ b/docs/triggers.rst @@ -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 ----------------- diff --git a/package/share/ahriman/settings/ahriman.ini b/package/share/ahriman/settings/ahriman.ini index f3a1ce71..ab3e3472 100644 --- a/package/share/ahriman/settings/ahriman.ini +++ b/package/share/ahriman/settings/ahriman.ini @@ -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 diff --git a/package/share/ahriman/templates/build-status.jinja2 b/package/share/ahriman/templates/build-status.jinja2 index bfbc582f..95854026 100644 --- a/package/share/ahriman/templates/build-status.jinja2 +++ b/package/share/ahriman/templates/build-status.jinja2 @@ -14,28 +14,29 @@

ahriman - {% if auth.authenticated %} - {{ version }} - {{ repository }} - {{ architecture }} - {{ service.status }} - {% endif %} + unknown + unknown + unknown + unknown

{% if not auth.enabled or auth.username is not none %} - - - {% endif %} +
- - - - - - - + + + + + + + - - - {% if auth.authenticated %} - {% for package in packages %} - - - - - - - - - - - {% endfor %} - {% else %} - - - - {% endif %} -
package baseversionpackagesgroupslicenseslast updatestatuspackage baseversionpackagesgroupslicenseslast updatestatus
{% if package.web_url is not none %}{{ package.base }}{% else %}{{ package.base }}{% endif %}{{ package.version }}{{ package.packages|join("
"|safe) }}
{{ package.groups|join("
"|safe) }}
{{ package.licenses|join("
"|safe) }}
{{ package.timestamp }}{{ package.status }}
In order to see statuses you must login first.
@@ -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" %} diff --git a/package/share/ahriman/templates/build-status/failed-modal.jinja2 b/package/share/ahriman/templates/build-status/failed-modal.jinja2 new file mode 100644 index 00000000..6cf3136f --- /dev/null +++ b/package/share/ahriman/templates/build-status/failed-modal.jinja2 @@ -0,0 +1,28 @@ + + + diff --git a/package/share/ahriman/templates/build-status/login-modal.jinja2 b/package/share/ahriman/templates/build-status/login-modal.jinja2 index 386b02f5..f10320bb 100644 --- a/package/share/ahriman/templates/build-status/login-modal.jinja2 +++ b/package/share/ahriman/templates/build-status/login-modal.jinja2 @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/package/share/ahriman/templates/build-status/package-actions-modals.jinja2 b/package/share/ahriman/templates/build-status/package-actions-modals.jinja2 deleted file mode 100644 index 4295184a..00000000 --- a/package/share/ahriman/templates/build-status/package-actions-modals.jinja2 +++ /dev/null @@ -1,60 +0,0 @@ - - - - - \ No newline at end of file diff --git a/package/share/ahriman/templates/build-status/package-actions-script.jinja2 b/package/share/ahriman/templates/build-status/package-actions-script.jinja2 deleted file mode 100644 index 80c26d48..00000000 --- a/package/share/ahriman/templates/build-status/package-actions-script.jinja2 +++ /dev/null @@ -1,95 +0,0 @@ - \ No newline at end of file diff --git a/package/share/ahriman/templates/build-status/package-add-modal.jinja2 b/package/share/ahriman/templates/build-status/package-add-modal.jinja2 new file mode 100644 index 00000000..8483e36e --- /dev/null +++ b/package/share/ahriman/templates/build-status/package-add-modal.jinja2 @@ -0,0 +1,62 @@ + + + diff --git a/package/share/ahriman/templates/build-status/success-modal.jinja2 b/package/share/ahriman/templates/build-status/success-modal.jinja2 new file mode 100644 index 00000000..cab3b54b --- /dev/null +++ b/package/share/ahriman/templates/build-status/success-modal.jinja2 @@ -0,0 +1,28 @@ + + + diff --git a/package/share/ahriman/templates/build-status/table.jinja2 b/package/share/ahriman/templates/build-status/table.jinja2 new file mode 100644 index 00000000..e5f349f6 --- /dev/null +++ b/package/share/ahriman/templates/build-status/table.jinja2 @@ -0,0 +1,147 @@ + \ No newline at end of file diff --git a/package/share/ahriman/templates/repo-index.jinja2 b/package/share/ahriman/templates/repo-index.jinja2 index f99392b4..5abc79ab 100644 --- a/package/share/ahriman/templates/repo-index.jinja2 +++ b/package/share/ahriman/templates/repo-index.jinja2 @@ -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"> diff --git a/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 b/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 index 67c7395d..c23f16c1 100644 --- a/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 +++ b/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 @@ -4,12 +4,12 @@ - - + + - + - + - + + - + diff --git a/setup.py b/setup.py index e31826bb..50546948 100644 --- a/setup.py +++ b/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", diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py index d20a5e09..6339fd85 100644 --- a/src/ahriman/application/handlers/status.py +++ b/src/ahriman/application/handlers/status.py @@ -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], diff --git a/src/ahriman/core/auth/auth.py b/src/ahriman/core/auth/auth.py index f2236214..c9c90797 100644 --- a/src/ahriman/core/auth/auth.py +++ b/src/ahriman/core/auth/auth.py @@ -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) diff --git a/src/ahriman/core/database/migrations/m002_user_access.py b/src/ahriman/core/database/migrations/m002_user_access.py new file mode 100644 index 00000000..a2bd75cc --- /dev/null +++ b/src/ahriman/core/database/migrations/m002_user_access.py @@ -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 . +# +__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' + """, +] diff --git a/src/ahriman/core/status/client.py b/src/ahriman/core/status/client.py index 3f3ea277..a673718a 100644 --- a/src/ahriman/core/status/client.py +++ b/src/ahriman/core/status/client.py @@ -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: """ diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py index 5802eb95..50b74c01 100644 --- a/src/ahriman/core/status/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -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)) diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py index b04a694f..437fdcb4 100644 --- a/src/ahriman/models/build_status.py +++ b/src/ahriman/models/build_status.py @@ -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: diff --git a/src/ahriman/models/internal_status.py b/src/ahriman/models/internal_status.py index a021cb2d..23763134 100644 --- a/src/ahriman/models/internal_status.py +++ b/src/ahriman/models/internal_status.py @@ -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")) diff --git a/src/ahriman/models/user.py b/src/ahriman/models/user.py index 0b5ac362..d0615abe 100644 --- a/src/ahriman/models/user.py +++ b/src/ahriman/models/user.py @@ -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: """ diff --git a/src/ahriman/models/user_access.py b/src/ahriman/models/user_access.py index cd58970f..f62d1fd2 100644 --- a/src/ahriman/models/user_access.py +++ b/src/ahriman/models/user_access.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +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 diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py index d3a58b7e..66b14f64 100644 --- a/src/ahriman/web/middlewares/auth_handler.py +++ b/src/ahriman/web/middlewares/auth_handler.py @@ -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 diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py index fcea5825..9dee46ab 100644 --- a/src/ahriman/web/routes.py +++ b/src/ahriman/web/routes.py @@ -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) diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py index 38d04396..3d6f73a9 100644 --- a/src/ahriman/web/views/base.py +++ b/src/ahriman/web/views/base.py @@ -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]: diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index 7792bf53..99a3be70 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -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__, } diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py index f03bd6be..a94c0ab4 100644 --- a/src/ahriman/web/views/service/add.py +++ b/src/ahriman/web/views/service/add.py @@ -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: """ diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/service/remove.py index 6277659a..c7011089 100644 --- a/src/ahriman/web/views/service/remove.py +++ b/src/ahriman/web/views/service/remove.py @@ -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: """ diff --git a/src/ahriman/web/views/service/request.py b/src/ahriman/web/views/service/request.py index e0926d68..c00d21ef 100644 --- a/src/ahriman/web/views/service/request.py +++ b/src/ahriman/web/views/service/request.py @@ -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: """ diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py index b2f98570..d82fe146 100644 --- a/src/ahriman/web/views/service/search.py +++ b/src/ahriman/web/views/service/search.py @@ -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: """ diff --git a/src/ahriman/web/views/status/ahriman.py b/src/ahriman/web/views/status/ahriman.py deleted file mode 100644 index 9fe987fa..00000000 --- a/src/ahriman/web/views/status/ahriman.py +++ /dev/null @@ -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 . -# -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() diff --git a/src/ahriman/web/views/status/package.py b/src/ahriman/web/views/status/package.py index 5d2b604d..fe50d8dd 100644 --- a/src/ahriman/web/views/status/package.py +++ b/src/ahriman/web/views/status/package.py @@ -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: diff --git a/src/ahriman/web/views/status/packages.py b/src/ahriman/web/views/status/packages.py index 701a50a2..7d786b47 100644 --- a/src/ahriman/web/views/status/packages.py +++ b/src/ahriman/web/views/status/packages.py @@ -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: """ diff --git a/src/ahriman/web/views/status/status.py b/src/ahriman/web/views/status/status.py index 32a37d58..9fb7f308 100644 --- a/src/ahriman/web/views/status/status.py +++ b/src/ahriman/web/views/status/status.py @@ -17,9 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -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() diff --git a/src/ahriman/web/views/user/login.py b/src/ahriman/web/views/user/login.py index eebe731a..c57249d9 100644 --- a/src/ahriman/web/views/user/login.py +++ b/src/ahriman/web/views/user/login.py @@ -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: """ diff --git a/src/ahriman/web/views/user/logout.py b/src/ahriman/web/views/user/logout.py index 1e9ebbfe..825eeef3 100644 --- a/src/ahriman/web/views/user/logout.py +++ b/src/ahriman/web/views/user/logout.py @@ -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: """ diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py index d1328fb8..302764ed 100644 --- a/tests/ahriman/application/handlers/test_handler_status.py +++ b/tests/ahriman/application/handlers/test_handler_status.py @@ -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") diff --git a/tests/ahriman/application/handlers/test_handler_users.py b/tests/ahriman/application/handlers/test_handler_users.py index 473c5c73..d1cc1f6e 100644 --- a/tests/ahriman/application/handlers/test_handler_users.py +++ b/tests/ahriman/application/handlers/test_handler_users.py @@ -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 diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 2e6b6936..3d4a9826 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -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) diff --git a/tests/ahriman/application/test_lock.py b/tests/ahriman/application/test_lock.py index 0818218e..f9078713 100644 --- a/tests/ahriman/application/test_lock.py +++ b/tests/ahriman/application/test_lock.py @@ -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() diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index df6cb436..8b431659 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -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 diff --git a/tests/ahriman/core/auth/test_auth.py b/tests/ahriman/core/auth/test_auth.py index 2e94b899..5c37f31b 100644 --- a/tests/ahriman/core/auth/test_auth.py +++ b/tests/ahriman/core/auth/test_auth.py @@ -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) diff --git a/tests/ahriman/core/auth/test_mapping.py b/tests/ahriman/core/auth/test_mapping.py index 0f218f8e..f28c45cd 100644 --- a/tests/ahriman/core/auth/test_mapping.py +++ b/tests/ahriman/core/auth/test_mapping.py @@ -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) diff --git a/tests/ahriman/core/database/migrations/test_m002_user_access.py b/tests/ahriman/core/database/migrations/test_m002_user_access.py new file mode 100644 index 00000000..d6d33053 --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m002_user_access.py @@ -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 diff --git a/tests/ahriman/core/database/operations/test_auth_operations.py b/tests/ahriman/core/database/operations/test_auth_operations.py index e488a3ec..d113c113 100644 --- a/tests/ahriman/core/database/operations/test_auth_operations.py +++ b/tests/ahriman/core/database/operations/test_auth_operations.py @@ -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 diff --git a/tests/ahriman/core/status/test_client.py b/tests/ahriman/core/status/test_client.py index bc1518e3..77a23bc3 100644 --- a/tests/ahriman/core/status/test_client.py +++ b/tests/ahriman/core/status/test_client.py @@ -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: diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py index 9e2eef85..a1db54a6 100644 --- a/tests/ahriman/core/status/test_web_client.py +++ b/tests/ahriman/core/status/test_web_client.py @@ -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: diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index 9cebe1bc..bd125228 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -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", diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index e6eafbe0..14003859 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -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") diff --git a/tests/ahriman/models/test_build_status.py b/tests/ahriman/models/test_build_status.py index 1473d823..a28b539e 100644 --- a/tests/ahriman/models/test_build_status.py +++ b/tests/ahriman/models/test_build_status.py @@ -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 diff --git a/tests/ahriman/models/test_user.py b/tests/ahriman/models/test_user.py index bc327c00..95f90a60 100644 --- a/tests/ahriman/models/test_user.py +++ b/tests/ahriman/models/test_user.py @@ -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: diff --git a/tests/ahriman/models/test_user_access.py b/tests/ahriman/models/test_user_access.py index e69de29b..cfe9cb9e 100644 --- a/tests/ahriman/models/test_user_access.py +++ b/tests/ahriman/models/test_user_access.py @@ -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) diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index 8253f6fe..e1e77ad1 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -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)) diff --git a/tests/ahriman/web/middlewares/test_auth_handler.py b/tests/ahriman/web/middlewares/test_auth_handler.py index bd5c299c..4dc40b7e 100644 --- a/tests/ahriman/web/middlewares/test_auth_handler.py +++ b/tests/ahriman/web/middlewares/test_auth_handler.py @@ -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: diff --git a/tests/ahriman/web/views/conftest.py b/tests/ahriman/web/views/conftest.py index 1f64f2eb..847ddafa 100644 --- a/tests/ahriman/web/views/conftest.py +++ b/tests/ahriman/web/views/conftest.py @@ -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)) diff --git a/tests/ahriman/web/views/service/test_views_service_add.py b/tests/ahriman/web/views/service/test_views_service_add.py index f989fa0c..69fd4fb9 100644 --- a/tests/ahriman/web/views/service/test_views_service_add.py +++ b/tests/ahriman/web/views/service/test_views_service_add.py @@ -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: diff --git a/tests/ahriman/web/views/service/test_views_service_remove.py b/tests/ahriman/web/views/service/test_views_service_remove.py index 0aedaef8..660b13b5 100644 --- a/tests/ahriman/web/views/service/test_views_service_remove.py +++ b/tests/ahriman/web/views/service/test_views_service_remove.py @@ -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: diff --git a/tests/ahriman/web/views/service/test_views_service_request.py b/tests/ahriman/web/views/service/test_views_service_request.py index 0d0390ad..0a089821 100644 --- a/tests/ahriman/web/views/service/test_views_service_request.py +++ b/tests/ahriman/web/views/service/test_views_service_request.py @@ -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: diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/service/test_views_service_search.py index 2e16d9fd..8b2c96b2 100644 --- a/tests/ahriman/web/views/service/test_views_service_search.py +++ b/tests/ahriman/web/views/service/test_views_service_search.py @@ -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: diff --git a/tests/ahriman/web/views/status/test_views_status_ahriman.py b/tests/ahriman/web/views/status/test_views_status_ahriman.py deleted file mode 100644 index b251d750..00000000 --- a/tests/ahriman/web/views/status/test_views_status_ahriman.py +++ /dev/null @@ -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 diff --git a/tests/ahriman/web/views/status/test_views_status_package.py b/tests/ahriman/web/views/status/test_views_status_package.py index 73656725..0c0bce3f 100644 --- a/tests/ahriman/web/views/status/test_views_status_package.py +++ b/tests/ahriman/web/views/status/test_views_status_package.py @@ -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: diff --git a/tests/ahriman/web/views/status/test_views_status_packages.py b/tests/ahriman/web/views/status/test_views_status_packages.py index 1ed6fd94..2bc18f25 100644 --- a/tests/ahriman/web/views/status/test_views_status_packages.py +++ b/tests/ahriman/web/views/status/test_views_status_packages.py @@ -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: diff --git a/tests/ahriman/web/views/status/test_views_status_status.py b/tests/ahriman/web/views/status/test_views_status_status.py index ef0f3735..8e3d3451 100644 --- a/tests/ahriman/web/views/status/test_views_status_status.py +++ b/tests/ahriman/web/views/status/test_views_status_status.py @@ -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 diff --git a/tests/ahriman/web/views/test_views_index.py b/tests/ahriman/web/views/test_views_index.py index 3b51adfd..f0e33687 100644 --- a/tests/ahriman/web/views/test_views_index.py +++ b/tests/ahriman/web/views/test_views_index.py @@ -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: diff --git a/tests/ahriman/web/views/user/test_views_user_login.py b/tests/ahriman/web/views/user/test_views_user_login.py index 3a381e42..ba070754 100644 --- a/tests/ahriman/web/views/user/test_views_user_login.py +++ b/tests/ahriman/web/views/user/test_views_user_login.py @@ -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: diff --git a/tests/ahriman/web/views/user/test_views_user_logout.py b/tests/ahriman/web/views/user/test_views_user_logout.py index f03c973f..38fbf3eb 100644 --- a/tests/ahriman/web/views/user/test_views_user_logout.py +++ b/tests/ahriman/web/views/user/test_views_user_logout.py @@ -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: diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index a5ab0612..26877e7f 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -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 =