From 30b108531a2cfecd3ede768b61e5937abb897911 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Thu, 1 Jun 2023 18:24:16 +0300 Subject: [PATCH] packagers support --- docker/entrypoint.sh | 2 +- docs/ahriman.1 | 36 ++++++-- docs/ahriman.core.database.migrations.rst | 8 ++ docs/ahriman.models.rst | 8 ++ docs/completions/bash/_ahriman | 19 +++-- docs/completions/zsh/_ahriman | 11 ++- docs/configuration.rst | 1 - docs/setup.rst | 2 +- .../ahriman/templates/build-status.jinja2 | 1 + .../templates/build-status/table.jinja2 | 5 +- src/ahriman/application/ahriman.py | 23 +++-- .../application/application/application.py | 36 ++++---- .../application/application_packages.py | 23 ++--- .../application/application_repository.py | 15 ++-- src/ahriman/application/handlers/add.py | 7 +- src/ahriman/application/handlers/patch.py | 2 +- src/ahriman/application/handlers/rebuild.py | 2 +- .../application/handlers/service_updates.py | 2 +- src/ahriman/application/handlers/setup.py | 2 +- .../application/handlers/unsafe_commands.py | 11 ++- src/ahriman/application/handlers/update.py | 5 +- src/ahriman/application/handlers/users.py | 3 +- src/ahriman/core/build_tools/task.py | 11 ++- src/ahriman/core/configuration/schema.py | 4 - .../database/migrations/m008_packagers.py | 85 +++++++++++++++++++ .../database/operations/auth_operations.py | 14 +-- .../database/operations/package_operations.py | 13 +-- .../database/operations/patch_operations.py | 4 +- src/ahriman/core/gitremote/remote_push.py | 4 +- .../core/gitremote/remote_push_trigger.py | 2 +- src/ahriman/core/repository/executor.py | 27 ++++-- .../core/repository/repository_properties.py | 23 +++++ src/ahriman/core/repository/update_handler.py | 6 +- src/ahriman/core/sign/gpg.py | 23 +---- src/ahriman/core/spawn.py | 24 ++++-- src/ahriman/core/support/keyring_trigger.py | 4 +- src/ahriman/core/support/package_creator.py | 2 +- .../support/pkgbuild/keyring_generator.py | 11 ++- .../support/pkgbuild/pkgbuild_generator.py | 4 +- src/ahriman/core/util.py | 35 +++++++- src/ahriman/models/internal_status.py | 5 +- src/ahriman/models/package.py | 33 ++++--- src/ahriman/models/package_description.py | 6 +- src/ahriman/models/packagers.py | 46 ++++++++++ src/ahriman/models/remote_source.py | 6 +- src/ahriman/models/user.py | 17 +++- src/ahriman/web/middlewares/auth_handler.py | 2 +- src/ahriman/web/schemas/package_schema.py | 4 + src/ahriman/web/views/base.py | 13 +++ src/ahriman/web/views/service/add.py | 3 +- src/ahriman/web/views/service/rebuild.py | 3 +- src/ahriman/web/views/service/request.py | 3 +- src/ahriman/web/views/service/update.py | 3 +- .../application/test_application.py | 10 +-- .../application/test_application_packages.py | 30 +++---- .../test_application_repository.py | 14 +-- .../application/handlers/test_handler_add.py | 7 +- .../handlers/test_handler_patch.py | 2 +- .../handlers/test_handler_rebuild.py | 3 +- .../handlers/test_handler_service_updates.py | 2 +- .../handlers/test_handler_unsafe_commands.py | 10 +-- .../handlers/test_handler_update.py | 5 +- .../handlers/test_handler_users.py | 8 +- tests/ahriman/application/test_ahriman.py | 9 +- tests/ahriman/conftest.py | 5 +- tests/ahriman/core/build_tools/test_task.py | 2 +- .../migrations/test_m007_check_depends.py | 4 +- .../migrations/test_m008_packagers.py | 52 ++++++++++++ .../operations/test_auth_operations.py | 26 +++--- .../core/gitremote/test_remote_push.py | 8 +- .../ahriman/core/repository/test_executor.py | 12 ++- .../repository/test_repository_properties.py | 39 ++++++++- .../core/repository/test_update_handler.py | 4 +- tests/ahriman/core/sign/test_gpg.py | 35 ++++---- .../ahriman/core/support/pkgbuild/conftest.py | 6 +- .../pkgbuild/test_keyring_generator.py | 49 ++++++----- .../core/support/test_keyring_trigger.py | 6 +- .../core/support/test_package_creator.py | 2 +- tests/ahriman/core/test_spawn.py | 27 ++++-- tests/ahriman/core/test_util.py | 50 ++++++++++- tests/ahriman/models/conftest.py | 1 + tests/ahriman/models/test_package.py | 16 ++-- tests/ahriman/models/test_packagers.py | 12 +++ tests/ahriman/models/test_user.py | 2 +- .../views/service/test_views_service_add.py | 6 +- .../service/test_views_service_rebuild.py | 6 +- .../service/test_views_service_request.py | 6 +- .../service/test_views_service_update.py | 6 +- tests/ahriman/web/views/test_views_base.py | 21 +++++ 89 files changed, 849 insertions(+), 318 deletions(-) create mode 100644 src/ahriman/core/database/migrations/m008_packagers.py create mode 100644 src/ahriman/models/packagers.py create mode 100644 tests/ahriman/core/database/migrations/test_m008_packagers.py create mode 100644 tests/ahriman/models/test_packagers.py diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index fb6fd55a..53d5f984 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -58,7 +58,7 @@ systemd-machine-id-setup &> /dev/null # otherwise we prepend executable by sudo command if [ -n "$AHRIMAN_FORCE_ROOT" ]; then AHRIMAN_EXECUTABLE=("ahriman") -elif ahriman help-commands-unsafe --command="$*" &> /dev/null; then +elif ahriman help-commands-unsafe -- "$@" &> /dev/null; then AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman") else AHRIMAN_EXECUTABLE=("ahriman") diff --git a/docs/ahriman.1 b/docs/ahriman.1 index cb2923c0..bc575cbc 100644 --- a/docs/ahriman.1 +++ b/docs/ahriman.1 @@ -1,4 +1,4 @@ -.TH AHRIMAN "1" "2023\-05\-28" "ahriman" "Generated Python Manual" +.TH AHRIMAN "1" "2023\-06\-03" "ahriman" "Generated Python Manual" .SH NAME ahriman .SH SYNOPSIS @@ -199,13 +199,12 @@ show help message for application or command and exit show help message for specific command .SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR -usage: ahriman help\-commands\-unsafe [\-h] [\-\-command COMMAND] +usage: ahriman help\-commands\-unsafe [\-h] [command ...] list unsafe commands as defined in default args -.SH OPTIONS \fI\,'ahriman help\-commands\-unsafe'\/\fR .TP -\fB\-\-command\fR \fI\,COMMAND\/\fR +\fBcommand\fR instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise @@ -226,7 +225,7 @@ print application and its dependencies versions .SH COMMAND \fI\,'ahriman package\-add'\/\fR usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-n] [\-y] - [\-s {auto,archive,aur,directory,local,remote,repository}] + [\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] package [package ...] add existing or new package to the build queue @@ -256,6 +255,10 @@ download fresh package databases from the mirror before actions, \-yy to force r \fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR explicitly specify the package source for this command +.TP +\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR +build as user + .SH COMMAND \fI\,'ahriman package\-remove'\/\fR usage: ahriman package\-remove [\-h] package [package ...] @@ -457,7 +460,7 @@ download fresh package databases from the mirror before actions, \-yy to force r .SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-e] - [\-s {unknown,pending,building,failed,success}] + [\-s {unknown,pending,building,failed,success}] [\-u USERNAME] force rebuild whole repository @@ -484,6 +487,10 @@ return non\-zero exit status if result is empty \fB\-s\fR \fI\,{unknown,pending,building,failed,success}\/\fR, \fB\-\-status\fR \fI\,{unknown,pending,building,failed,success}\/\fR filter packages by status. Requires \-\-from\-database to be set +.TP +\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR +build as user + .SH COMMAND \fI\,'ahriman repo\-remove\-unknown'\/\fR usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run] @@ -553,7 +560,7 @@ instead of running all triggers as set by configuration, just process specified .SH COMMAND \fI\,'ahriman repo\-update'\/\fR usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] - [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-vcs | \-\-no\-vcs] [\-y] + [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y] [package ...] check for packages updates and run build process if requested @@ -587,6 +594,10 @@ enable or disable checking of local packages for updates \fB\-\-manual\fR, \fB\-\-no\-manual\fR include or exclude manual updates +.TP +\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR +build as user + .TP \fB\-\-vcs\fR, \fB\-\-no\-vcs\fR fetch actual version of VCS packages @@ -724,7 +735,8 @@ drop into python shell while having created application instead of dropping into shell, just execute the specified code .SH COMMAND \fI\,'ahriman user\-add'\/\fR -usage: ahriman user\-add [\-h] [\-p PASSWORD] [\-r {unauthorized,read,reporter,full}] [\-s] username +usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-r {unauthorized,read,reporter,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 @@ -733,6 +745,14 @@ update user for web services with the given password and role. In case if passwo username for web service .SH OPTIONS \fI\,'ahriman user\-add'\/\fR +.TP +\fB\-\-key\fR \fI\,KEY\/\fR +optional PGP key used by this user. The private key must be imported + +.TP +\fB\-\-packager\fR \fI\,PACKAGER\/\fR +optional packager id used for build process in form of `Name Surname ` + .TP \fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst index 32d278a8..ec26ecb4 100644 --- a/docs/ahriman.core.database.migrations.rst +++ b/docs/ahriman.core.database.migrations.rst @@ -68,6 +68,14 @@ ahriman.core.database.migrations.m007\_check\_depends module :no-undoc-members: :show-inheritance: +ahriman.core.database.migrations.m008\_packagers module +------------------------------------------------------- + +.. automodule:: ahriman.core.database.migrations.m008_packagers + :members: + :no-undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/ahriman.models.rst b/docs/ahriman.models.rst index 669f95e0..e4799fa5 100644 --- a/docs/ahriman.models.rst +++ b/docs/ahriman.models.rst @@ -116,6 +116,14 @@ ahriman.models.package\_source module :no-undoc-members: :show-inheritance: +ahriman.models.packagers module +------------------------------- + +.. automodule:: ahriman.models.packagers + :members: + :no-undoc-members: + :show-inheritance: + ahriman.models.pacman\_synchronization module --------------------------------------------- diff --git a/docs/completions/bash/_ahriman b/docs/completions/bash/_ahriman index 0ba6cfb4..219ead01 100644 --- a/docs/completions/bash/_ahriman +++ b/docs/completions/bash/_ahriman @@ -6,13 +6,13 @@ _shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--confi _shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_help_option_strings=('-h' '--help') -_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help' '--command') +_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help') _shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_help_version_option_strings=('-h' '--help') _shtab_ahriman_version_option_strings=('-h' '--help') -_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source') -_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source') -_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source') +_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username') +_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username') +_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username') _shtab_ahriman_package_remove_option_strings=('-h' '--help') _shtab_ahriman_remove_option_strings=('-h' '--help') _shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status') @@ -31,8 +31,8 @@ _shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help') _shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help') _shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') -_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status') -_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status') +_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username') +_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username') _shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run') _shtab_ahriman_remove_unknown_option_strings=('-h' '--help' '--dry-run') _shtab_ahriman_repo_report_option_strings=('-h' '--help') @@ -45,8 +45,8 @@ _shtab_ahriman_repo_sync_option_strings=('-h' '--help') _shtab_ahriman_sync_option_strings=('-h' '--help') _shtab_ahriman_repo_tree_option_strings=('-h' '--help') _shtab_ahriman_repo_triggers_option_strings=('-h' '--help') -_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') -_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') +_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh') +_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') @@ -65,7 +65,7 @@ _shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--bui _shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') _shtab_ahriman_service_shell_option_strings=('-h' '--help') _shtab_ahriman_shell_option_strings=('-h' '--help') -_shtab_ahriman_user_add_option_strings=('-h' '--help' '-p' '--password' '-r' '--role' '-s' '--secure') +_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-r' '--role' '-s' '--secure') _shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-r' '--role') _shtab_ahriman_user_remove_option_strings=('-h' '--help') _shtab_ahriman_web_option_strings=('-h' '--help') @@ -133,6 +133,7 @@ _shtab_ahriman_search___info_nargs=0 _shtab_ahriman_search___no_info_nargs=0 _shtab_ahriman_help__h_nargs=0 _shtab_ahriman_help___help_nargs=0 +_shtab_ahriman_help_commands_unsafe_pos_0_nargs=* _shtab_ahriman_help_commands_unsafe__h_nargs=0 _shtab_ahriman_help_commands_unsafe___help_nargs=0 _shtab_ahriman_help_updates__h_nargs=0 diff --git a/docs/completions/zsh/_ahriman b/docs/completions/zsh/_ahriman index 12302492..d6d37331 100644 --- a/docs/completions/zsh/_ahriman +++ b/docs/completions/zsh/_ahriman @@ -95,6 +95,7 @@ _shtab_ahriman_add_options=( {-n,--now}"[run update function after]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" + {-u,--username}"[build as user]:username:" "(*):package source (base name, path to local files, remote URL):" ) @@ -151,7 +152,7 @@ _shtab_ahriman_help_options=( _shtab_ahriman_help_commands_unsafe_options=( "(- : *)"{-h,--help}"[show this help message and exit]" - "--command[instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise]:command:" + "(*)::instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise:" ) _shtab_ahriman_help_updates_options=( @@ -192,6 +193,7 @@ _shtab_ahriman_package_add_options=( {-n,--now}"[run update function after]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" + {-u,--username}"[build as user]:username:" "(*):package source (base name, path to local files, remote URL):" ) @@ -227,6 +229,7 @@ _shtab_ahriman_package_update_options=( {-n,--now}"[run update function after]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" + {-u,--username}"[build as user]:username:" "(*):package source (base name, path to local files, remote URL):" ) @@ -263,6 +266,7 @@ _shtab_ahriman_rebuild_options=( "--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]" {-e,--exit-code}"[return non-zero exit status if result is empty]" {-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)" + {-u,--username}"[build as user]:username:" ) _shtab_ahriman_remove_options=( @@ -349,6 +353,7 @@ _shtab_ahriman_repo_rebuild_options=( "--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once.]" {-e,--exit-code}"[return non-zero exit status if result is empty]" {-s,--status}"[filter packages by status. Requires --from-database to be set]:status:(unknown pending building failed success)" + {-u,--username}"[build as user]:username:" ) _shtab_ahriman_repo_remove_unknown_options=( @@ -413,6 +418,7 @@ _shtab_ahriman_repo_update_options=( {-e,--exit-code}"[return non-zero exit status if result is empty]" {--local,--no-local}"[enable or disable checking of local packages for updates]:local:" {--manual,--no-manual}"[include or exclude manual updates]:manual:" + {-u,--username}"[build as user]:username:" {--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "(*)::filter check by package base:" @@ -529,6 +535,7 @@ _shtab_ahriman_update_options=( {-e,--exit-code}"[return non-zero exit status if result is empty]" {--local,--no-local}"[enable or disable checking of local packages for updates]:local:" {--manual,--no-manual}"[include or exclude manual updates]:manual:" + {-u,--username}"[build as user]:username:" {--vcs,--no-vcs}"[fetch actual version of VCS packages]:vcs:" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "(*)::filter check by package base:" @@ -536,6 +543,8 @@ _shtab_ahriman_update_options=( _shtab_ahriman_user_add_options=( "(- : *)"{-h,--help}"[show this help message and exit]" + "--key[optional PGP key used by this user. The private key must be imported]:key:" + "--packager[optional packager id used for build process in form of \`Name Surname \\`]:packager:" {-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type.]:password:" {-r,--role}"[user access level]:role:(unauthorized read reporter full)" {-s,--secure}"[set file permissions to user-only]" diff --git a/docs/configuration.rst b/docs/configuration.rst index c57e80b5..baa85dec 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -87,7 +87,6 @@ Settings for signing packages or repository. Group name can refer to architectur * ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file). * ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled. -* ``key_*`` settings - PGP key which will be used for specific packages, string, optional. For example, if there is ``key_yay`` option the specified key will be used for yay package and default key for others. ``web:*`` groups ---------------- diff --git a/docs/setup.rst b/docs/setup.rst index eb682d20..9b1a673c 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -64,7 +64,7 @@ Initial setup .. code-block:: shell echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman - echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman + echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman chmod 400 /etc/sudoers.d/ahriman This command supports several arguments, kindly refer to its help message. diff --git a/package/share/ahriman/templates/build-status.jinja2 b/package/share/ahriman/templates/build-status.jinja2 index 732c4a87..995088aa 100644 --- a/package/share/ahriman/templates/build-status.jinja2 +++ b/package/share/ahriman/templates/build-status.jinja2 @@ -87,6 +87,7 @@ packages groups licenses + packager last update status diff --git a/package/share/ahriman/templates/build-status/table.jinja2 b/package/share/ahriman/templates/build-status/table.jinja2 index 32ccd1c2..03b327da 100644 --- a/package/share/ahriman/templates/build-status/table.jinja2 +++ b/package/share/ahriman/templates/build-status/table.jinja2 @@ -98,6 +98,7 @@ id: package_base, base: web_url ? `${safe(package_base)}` : safe(package_base), version: safe(description.package.version), + packager: description.package.packager ? safe(description.package.packager) : "", packages: listToTable(Object.keys(description.package.packages)), groups: listToTable(extractListProperties(description.package, "groups")), licenses: listToTable(extractListProperties(description.package, "licenses")), @@ -120,8 +121,8 @@ table.bootstrapTable("hideLoading"); } else { // other errors - const messaga = error => { return `Could not load list of packages: ${error}`; }; - showFailure("Load failure", messaga, jqXHR, errorThrown); + const message = error => { return `Could not load list of packages: ${error}`; }; + showFailure("Load failure", message, jqXHR, errorThrown); } hideControls(true); }, diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 21ba2f7c..47d9f40c 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -27,7 +27,7 @@ from typing import TypeVar from ahriman import version from ahriman.application import handlers -from ahriman.core.util import enum_values +from ahriman.core.util import enum_values, extract_user from ahriman.models.action import Action from ahriman.models.build_status import BuildStatusEnum from ahriman.models.log_handler import LogHandler @@ -187,8 +187,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument """ parser = root.add_parser("help-commands-unsafe", help="list unsafe commands", description="list unsafe commands as defined in default args", formatter_class=_formatter) - parser.add_argument("--command", help="instead of showing commands, just test command line for unsafe subcommand " - "and return 0 in case if command is safe and 1 otherwise") + parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand " + "and return 0 in case if command is safe and 1 otherwise", nargs="*") parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, report=False, quiet=True, unsafe=True, parser=_parser) return parser @@ -262,6 +262,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser: action="count", default=False) parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) + parser.add_argument("-u", "--username", help="build as user", default=extract_user()) parser.set_defaults(handler=handlers.Add) return parser @@ -481,7 +482,8 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " "-yy to force refresh even if up to date", action="count", default=False) - parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False) + parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False, + username=None) return parser @@ -578,6 +580,7 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set", type=BuildStatusEnum, choices=enum_values(BuildStatusEnum)) + parser.add_argument("-u", "--username", help="build as user", default=extract_user()) parser.set_defaults(handler=handlers.Rebuild) return parser @@ -752,6 +755,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser: action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--manual", help="include or exclude manual updates", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("-u", "--username", help="build as user", default=extract_user()) parser.add_argument("--vcs", help="fetch actual version of VCS packages", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " @@ -923,6 +927,9 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser: "root privileges because it performs write to filesystem configuration.", formatter_class=_formatter) parser.add_argument("username", help="username for web service") + parser.add_argument("--key", help="optional PGP key used by this user. The private key must be imported") + parser.add_argument("--packager", help="optional packager id used for build process in form of " + "`Name Surname `") parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, " "which is in particular must be used for OAuth2 authorization type.") parser.add_argument("-r", "--role", help="user access level", @@ -949,8 +956,8 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("username", help="filter users by username", nargs="?") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess)) - parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False, # nosec - password="", quiet=True, unsafe=True) + parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, report=False, + quiet=True, unsafe=True) return parser @@ -968,8 +975,8 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser: description="remove user from the user mapping and update the configuration", formatter_class=_formatter) parser.add_argument("username", help="username for web service") - parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, # nosec - password="", quiet=True) + parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, report=False, + quiet=True) return parser diff --git a/src/ahriman/application/application/application.py b/src/ahriman/application/application/application.py index 4cf8a360..3c453ca8 100644 --- a/src/ahriman/application/application/application.py +++ b/src/ahriman/application/application/application.py @@ -39,7 +39,7 @@ class Application(ApplicationPackages, ApplicationRepository): >>> configuration = Configuration() >>> application = Application("x86_64", configuration, report=True, unsafe=False) >>> # add packages to build queue - >>> application.add(["ahriman"], PackageSource.AUR, without_dependencies=False) + >>> application.add(["ahriman"], PackageSource.AUR) >>> >>> # check for updates >>> updates = application.updates([], aur=True, local=True, manual=True, vcs=True, log_fn=print) @@ -96,21 +96,25 @@ class Application(ApplicationPackages, ApplicationRepository): Args: packages(list[Package]): list of source packages of which dependencies have to be processed process_dependencies(bool): if no set, dependencies will not be processed + + Returns: + list[Package]: updated packages list. Packager for dependencies will be copied from + original package """ - def missing_dependencies(source: Iterable[Package]) -> set[str]: - # build initial list of dependencies - result = set() - for package in source: - result.update(package.depends_build) + def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]: + # append list of known packages with packages which are in current sources + satisfied_packages = known_packages | { + single + for package in source + for single in package.packages_full + } - # remove ones which are already well-known - result = result.difference(known_packages) - - # remove ones which are in this list already - for package in source: - result = result.difference(package.packages_full) - - return result + return { + dependency: package.packager + for package in source + for dependency in package.depends_build + if dependency not in satisfied_packages + } if not process_dependencies or not packages: return packages @@ -119,8 +123,8 @@ class Application(ApplicationPackages, ApplicationRepository): with_dependencies = {package.base: package for package in packages} while missing := missing_dependencies(with_dependencies.values()): - for package_name in missing: - package = Package.from_aur(package_name, self.repository.pacman) + for package_name, username in missing.items(): + package = Package.from_aur(package_name, self.repository.pacman, username) with_dependencies[package.base] = package return list(with_dependencies.values()) diff --git a/src/ahriman/application/application/application_packages.py b/src/ahriman/application/application/application_packages.py index 0c78f6e6..6301031f 100644 --- a/src/ahriman/application/application/application_packages.py +++ b/src/ahriman/application/application/application_packages.py @@ -55,15 +55,15 @@ class ApplicationPackages(ApplicationProperties): dst = self.repository.paths.packages / local_path.name shutil.copy(local_path, dst) - def _add_aur(self, source: str) -> None: + def _add_aur(self, source: str, username: str | None) -> None: """ add package from AUR Args: source(str): package base name + username(str | None): optional override of username for build process """ - package = Package.from_aur(source, self.repository.pacman) - + package = Package.from_aur(source, self.repository.pacman, username) self.database.build_queue_insert(package) self.database.remote_update(package) @@ -81,23 +81,24 @@ class ApplicationPackages(ApplicationProperties): for full_path in filter(package_like, local_dir.iterdir()): self._add_archive(str(full_path)) - def _add_local(self, source: str) -> None: + def _add_local(self, source: str, username: str | None) -> None: """ add package from local PKGBUILDs Args: source(str): path to directory with local source files + username(str | None): optional override of username for build process Raises: UnknownPackageError: if specified package is unknown or doesn't exist """ if (source_dir := Path(source)).is_dir(): - package = Package.from_build(source_dir, self.architecture) + package = Package.from_build(source_dir, self.architecture, username) cache_dir = self.repository.paths.cache_for(package.base) shutil.copytree(source_dir, cache_dir) # copy package to store in caches Sources.init(cache_dir) # we need to run init command in directory where we do have permissions elif (source_dir := self.repository.paths.cache_for(source)).is_dir(): - package = Package.from_build(source_dir, self.architecture) + package = Package.from_build(source_dir, self.architecture, username) else: raise UnknownPackageError(source) @@ -122,29 +123,31 @@ class ApplicationPackages(ApplicationProperties): for chunk in response.iter_content(chunk_size=1024): local_file.write(chunk) - def _add_repository(self, source: str, *_: Any) -> None: + def _add_repository(self, source: str, username: str | None) -> None: """ add package from official repository Args: source(str): package base name + username(str | None): optional override of username for build process """ - package = Package.from_official(source, self.repository.pacman) + package = Package.from_official(source, self.repository.pacman, username) self.database.build_queue_insert(package) self.database.remote_update(package) - def add(self, names: Iterable[str], source: PackageSource) -> None: + def add(self, names: Iterable[str], source: PackageSource, username: str | None = None) -> None: """ add packages for the next build Args: names(Iterable[str]): list of package bases to add source(PackageSource): package source to add + username(str | None, optional): optional override of username for build process (Default value = None) """ for name in names: resolved_source = source.resolve(name) fn = getattr(self, f"_add_{resolved_source.value}") - fn(name) + fn(name, username) def on_result(self, result: Result) -> None: """ diff --git a/src/ahriman/application/application/application_repository.py b/src/ahriman/application/application/application_repository.py index 3fd5bde2..6d67d5ae 100644 --- a/src/ahriman/application/application/application_repository.py +++ b/src/ahriman/application/application/application_repository.py @@ -25,6 +25,7 @@ from ahriman.core.build_tools.sources import Sources from ahriman.core.formatters import UpdatePrinter from ahriman.core.tree import Tree from ahriman.models.package import Package +from ahriman.models.packagers import Packagers from ahriman.models.result import Result @@ -83,7 +84,7 @@ class ApplicationRepository(ApplicationProperties): if archive.filepath is None: self.logger.warning("filepath is empty for %s", package.base) continue # avoid mypy warning - self.repository.sign.process_sign_package(archive.filepath, package.base) + self.repository.sign.process_sign_package(archive.filepath, None) # sign repository database if set self.repository.sign.process_sign_repository(self.repository.repo.repo_path) # process triggers @@ -104,14 +105,14 @@ class ApplicationRepository(ApplicationProperties): packages: list[str] = [] for single in probe.packages: try: - _ = Package.from_aur(single, self.repository.pacman) + _ = Package.from_aur(single, self.repository.pacman, None) except Exception: packages.append(single) return packages def unknown_local(probe: Package) -> list[str]: cache_dir = self.repository.paths.cache_for(probe.base) - local = Package.from_build(cache_dir, self.architecture) + local = Package.from_build(cache_dir, self.architecture, None) packages = set(probe.packages.keys()).difference(local.packages.keys()) return list(packages) @@ -123,12 +124,14 @@ class ApplicationRepository(ApplicationProperties): result.extend(unknown_aur(package)) # local package not found return result - def update(self, updates: Iterable[Package]) -> Result: + def update(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result: """ run package updates Args: updates(Iterable[Package]): list of packages to update + packagers(Packagers | None, optional): optional override of username for build process + (Default value = None) Returns: Result: update result @@ -136,7 +139,7 @@ class ApplicationRepository(ApplicationProperties): def process_update(paths: Iterable[Path], result: Result) -> None: if not paths: return # don't need to process if no update supplied - update_result = self.repository.process_update(paths) + update_result = self.repository.process_update(paths, packagers) self.on_result(result.merge(update_result)) # process built packages @@ -148,7 +151,7 @@ class ApplicationRepository(ApplicationProperties): tree = Tree.resolve(updates) for num, level in enumerate(tree): self.logger.info("processing level #%i %s", num, [package.base for package in level]) - build_result = self.repository.process_build(level) + build_result = self.repository.process_build(level, packagers) packages = self.repository.packages_built() process_update(packages, build_result) diff --git a/src/ahriman/application/handlers/add.py b/src/ahriman/application/handlers/add.py index 414c4299..95a79c92 100644 --- a/src/ahriman/application/handlers/add.py +++ b/src/ahriman/application/handlers/add.py @@ -22,6 +22,7 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.packagers import Packagers class Add(Handler): @@ -45,12 +46,14 @@ class Add(Handler): application = Application(architecture, configuration, report=report, unsafe=unsafe, refresh_pacman_database=args.refresh) application.on_start() - application.add(args.package, args.source) + application.add(args.package, args.source, args.username) if not args.now: return packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, log_fn=application.logger.info) packages = application.with_dependencies(packages, process_dependencies=args.dependencies) - result = application.update(packages) + packagers = Packagers(args.username, {package.base: package.packager for package in packages}) + + result = application.update(packages, packagers) Add.check_if_empty(args.exit_code, result.is_empty) diff --git a/src/ahriman/application/handlers/patch.py b/src/ahriman/application/handlers/patch.py index 385018a6..bd95c20f 100644 --- a/src/ahriman/application/handlers/patch.py +++ b/src/ahriman/application/handlers/patch.py @@ -78,7 +78,7 @@ class Patch(Handler): tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD to current files """ - package = Package.from_build(sources_dir, architecture) + package = Package.from_build(sources_dir, architecture, None) patch = Sources.patch_create(sources_dir, *track) return package.base, PkgbuildPatch(None, patch) diff --git a/src/ahriman/application/handlers/rebuild.py b/src/ahriman/application/handlers/rebuild.py index 17fe3d49..e00ac5ee 100644 --- a/src/ahriman/application/handlers/rebuild.py +++ b/src/ahriman/application/handlers/rebuild.py @@ -57,7 +57,7 @@ class Rebuild(Handler): UpdatePrinter(package, package.version).print(verbose=True) return - result = application.update(updates) + result = application.update(updates, args.username) Rebuild.check_if_empty(args.exit_code, result.is_empty) @staticmethod diff --git a/src/ahriman/application/handlers/service_updates.py b/src/ahriman/application/handlers/service_updates.py index feb2a400..b2d158c5 100644 --- a/src/ahriman/application/handlers/service_updates.py +++ b/src/ahriman/application/handlers/service_updates.py @@ -49,7 +49,7 @@ class ServiceUpdates(Handler): """ application = Application(architecture, configuration, report=report, unsafe=unsafe) - remote = Package.from_aur("ahriman", application.repository.pacman) + remote = Package.from_aur("ahriman", application.repository.pacman, None) release = remote.version.rsplit("-", 1)[-1] # we don't store pkgrel locally, so we just append it local_version = f"{version.__version__}-{release}" diff --git a/src/ahriman/application/handlers/setup.py b/src/ahriman/application/handlers/setup.py index 15de7a50..65d854c5 100644 --- a/src/ahriman/application/handlers/setup.py +++ b/src/ahriman/application/handlers/setup.py @@ -213,7 +213,7 @@ class Setup(Handler): """ command = Setup.build_command(paths.root, prefix, architecture) sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, prefix, architecture) - sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n", encoding="utf8") + sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD:SETENV: {command} *\n", encoding="utf8") sudoers_file.chmod(0o400) # security! @staticmethod diff --git a/src/ahriman/application/handlers/unsafe_commands.py b/src/ahriman/application/handlers/unsafe_commands.py index dde37f59..eab6671d 100644 --- a/src/ahriman/application/handlers/unsafe_commands.py +++ b/src/ahriman/application/handlers/unsafe_commands.py @@ -18,7 +18,6 @@ # along with this program. If not, see . # import argparse -import shlex from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration @@ -47,14 +46,14 @@ class UnsafeCommands(Handler): """ parser = args.parser() unsafe_commands = UnsafeCommands.get_unsafe_commands(parser) - if args.command is None: + if args.command: + UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser) + else: for command in unsafe_commands: StringPrinter(command).print(verbose=True) - else: - UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser) @staticmethod - def check_unsafe(command: str, unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None: + def check_unsafe(command: list[str], unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None: """ check if command is unsafe @@ -63,7 +62,7 @@ class UnsafeCommands(Handler): unsafe_commands(list[str]): list of unsafe commands parser(argparse.ArgumentParser): generated argument parser """ - args = parser.parse_args(shlex.split(command)) + args = parser.parse_args(command) UnsafeCommands.check_if_empty(True, args.command in unsafe_commands) @staticmethod diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py index a90d87d3..77f2cb79 100644 --- a/src/ahriman/application/handlers/update.py +++ b/src/ahriman/application/handlers/update.py @@ -24,6 +24,7 @@ from collections.abc import Callable from ahriman.application.application import Application from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration +from ahriman.models.packagers import Packagers class Update(Handler): @@ -54,7 +55,9 @@ class Update(Handler): return packages = application.with_dependencies(packages, process_dependencies=args.dependencies) - result = application.update(packages) + packagers = Packagers(args.username, {package.base: package.packager for package in packages}) + + result = application.update(packages, packagers) Update.check_if_empty(args.exit_code, result.is_empty) @staticmethod diff --git a/src/ahriman/application/handlers/users.py b/src/ahriman/application/handlers/users.py index 2d570b6e..30adce27 100644 --- a/src/ahriman/application/handlers/users.py +++ b/src/ahriman/application/handlers/users.py @@ -156,4 +156,5 @@ class Users(Handler): if password is None: password = read_password() - return User(username=args.username, password=password, access=args.role) + return User(username=args.username, password=password, access=args.role, + packager_id=args.packager, key=args.key) diff --git a/src/ahriman/core/build_tools/task.py b/src/ahriman/core/build_tools/task.py index 84255f34..617a13a8 100644 --- a/src/ahriman/core/build_tools/task.py +++ b/src/ahriman/core/build_tools/task.py @@ -59,12 +59,13 @@ class Task(LazyLogging): self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[]) self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[]) - def build(self, sources_dir: Path) -> list[Path]: + def build(self, sources_dir: Path, packager: str | None = None) -> list[Path]: """ run package build Args: sources_dir(Path): path to where sources are + packager(str | None, optional): optional packager override (Default value = None) Returns: list[Path]: paths of produced packages @@ -75,12 +76,18 @@ class Task(LazyLogging): command.extend(["--"] + self.makepkg_flags) self.logger.info("using %s for %s", command, self.package.base) + environment: dict[str, str] = {} + if packager is not None: + environment["PACKAGER"] = packager + self.logger.info("using environment variables %s", environment) + Task._check_output( *command, exception=BuildError(self.package.base), cwd=sources_dir, logger=self.logger, - user=self.uid) + user=self.uid, + environment=environment) # well it is not actually correct, but we can deal with it packages = Task._check_output( diff --git a/src/ahriman/core/configuration/schema.py b/src/ahriman/core/configuration/schema.py index 30da4764..aea82ae1 100644 --- a/src/ahriman/core/configuration/schema.py +++ b/src/ahriman/core/configuration/schema.py @@ -191,10 +191,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = { "sign": { "type": "dict", "allow_unknown": True, - "keysrules": { - "type": "string", - "anyof_regex": ["^target$", "^key$", "^key_.*"], - }, "schema": { "target": { "type": "list", diff --git a/src/ahriman/core/database/migrations/m008_packagers.py b/src/ahriman/core/database/migrations/m008_packagers.py new file mode 100644 index 00000000..b54196e4 --- /dev/null +++ b/src/ahriman/core/database/migrations/m008_packagers.py @@ -0,0 +1,85 @@ +# +# Copyright (c) 2021-2023 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 sqlite3 import Connection + +from ahriman.core.alpm.pacman import Pacman +from ahriman.core.configuration import Configuration +from ahriman.core.util import package_like +from ahriman.models.package import Package +from ahriman.models.pacman_synchronization import PacmanSynchronization + + +__all__ = ["migrate_data", "steps"] + + +steps = [ + """ + alter table users add column packager_id + """, + """ + alter table users add column key_id + """, + """ + alter table package_bases add column packager + """, +] + + +def migrate_data(connection: Connection, configuration: Configuration) -> None: + """ + perform data migration + + Args: + connection(Connection): database connection + configuration(Configuration): configuration instance + """ + migrate_package_base_packager(connection, configuration) + + +def migrate_package_base_packager(connection: Connection, configuration: Configuration) -> None: + """ + migrate package packager field + + Args: + connection(Connection): database connection + configuration(Configuration): configuration instance + """ + if not configuration.repository_paths.repository.is_dir(): + return + + _, architecture = configuration.check_loaded() + pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled) + + package_list = [] + for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): + package = Package.from_archive(full_path, pacman, remote=None) + package_list.append({ + "package_base": package.base, + "packager": package.packager, + }) + + connection.executemany( + """ + update package_bases set + packager = :packager + where package_base = :package_base + """, + package_list + ) diff --git a/src/ahriman/core/database/operations/auth_operations.py b/src/ahriman/core/database/operations/auth_operations.py index 67270352..b0153427 100644 --- a/src/ahriman/core/database/operations/auth_operations.py +++ b/src/ahriman/core/database/operations/auth_operations.py @@ -57,8 +57,9 @@ class AuthOperations(Operations): def run(connection: Connection) -> list[User]: return [ - User(username=cursor["username"], password=cursor["password"], access=UserAccess(cursor["access"])) - for cursor in connection.execute( + User(username=row["username"], password=row["password"], access=UserAccess(row["access"]), + packager_id=row["packager_id"], key=row["key_id"]) + for row in connection.execute( """ select * from users where (:username is null or username = :username) and (:access is null or access = :access) @@ -91,12 +92,13 @@ class AuthOperations(Operations): connection.execute( """ insert into users - (username, access, password) + (username, access, password, packager_id, key_id) values - (:username, :access, :password) + (:username, :access, :password, :packager_id, :key_id) on conflict (username) do update set - access = :access, password = :password + access = :access, password = :password, packager_id = :packager_id, key_id = :key_id """, - {"username": user.username.lower(), "access": user.access.value, "password": user.password}) + {"username": user.username.lower(), "access": user.access.value, "password": user.password, + "packager_id": user.packager_id, "key_id": user.key}) self.with_connection(run, commit=True) diff --git a/src/ahriman/core/database/operations/package_operations.py b/src/ahriman/core/database/operations/package_operations.py index ee417c7d..8ddf5ea6 100644 --- a/src/ahriman/core/database/operations/package_operations.py +++ b/src/ahriman/core/database/operations/package_operations.py @@ -76,11 +76,12 @@ class PackageOperations(Operations): connection.execute( """ insert into package_bases - (package_base, version, source, branch, git_url, path, web_url) + (package_base, version, source, branch, git_url, path, web_url, packager) values - (:package_base, :version, :source, :branch, :git_url, :path, :web_url) + (:package_base, :version, :source, :branch, :git_url, :path, :web_url, :packager) on conflict (package_base) do update set - version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source + version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, + source = :source, packager = :packager """, { "package_base": package.base, @@ -90,6 +91,7 @@ class PackageOperations(Operations): "path": package.remote.path if package.remote is not None else None, "web_url": package.remote.web_url if package.remote is not None else None, "source": package.remote.source.value if package.remote is not None else None, + "packager": package.packager, } ) @@ -163,8 +165,9 @@ class PackageOperations(Operations): base=row["package_base"], version=row["version"], remote=RemoteSource.from_json(row), - packages={}) - for row in connection.execute("""select * from package_bases""") + packages={}, + packager=row["packager"] or None, + ) for row in connection.execute("""select * from package_bases""") } @staticmethod diff --git a/src/ahriman/core/database/operations/patch_operations.py b/src/ahriman/core/database/operations/patch_operations.py index 5cfd0cc4..98cbbfc4 100644 --- a/src/ahriman/core/database/operations/patch_operations.py +++ b/src/ahriman/core/database/operations/patch_operations.py @@ -77,8 +77,8 @@ class PatchOperations(Operations): """ def run(connection: Connection) -> list[tuple[str, PkgbuildPatch]]: return [ - (cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"])) - for cursor in connection.execute( + (row["package_base"], PkgbuildPatch(row["variable"], row["patch"])) + for row in connection.execute( """select * from patches where :package_base is null or package_base = :package_base""", {"package_base": package_base}) ] diff --git a/src/ahriman/core/gitremote/remote_push.py b/src/ahriman/core/gitremote/remote_push.py index 9b17179e..25d43a57 100644 --- a/src/ahriman/core/gitremote/remote_push.py +++ b/src/ahriman/core/gitremote/remote_push.py @@ -44,13 +44,13 @@ class RemotePush(LazyLogging): remote_source(RemoteSource): repository remote source (remote pull url and branch) """ - def __init__(self, configuration: Configuration, database: SQLite, section: str) -> None: + def __init__(self, database: SQLite, configuration: Configuration, section: str) -> None: """ default constructor Args: - configuration(Configuration): configuration instance database(SQLite): database instance + configuration(Configuration): configuration instance section(str): settings section name """ self.database = database diff --git a/src/ahriman/core/gitremote/remote_push_trigger.py b/src/ahriman/core/gitremote/remote_push_trigger.py index d5f9a2a1..5475bda0 100644 --- a/src/ahriman/core/gitremote/remote_push_trigger.py +++ b/src/ahriman/core/gitremote/remote_push_trigger.py @@ -105,5 +105,5 @@ class RemotePushTrigger(Trigger): for target in self.targets: section, _ = self.configuration.gettype( target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK) - runner = RemotePush(self.configuration, database, section) + runner = RemotePush(database, self.configuration, section) runner.run(result) diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index 4e3f8ada..75adfca8 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -28,6 +28,7 @@ from ahriman.core.repository.cleaner import Cleaner from ahriman.core.util import safe_filename from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription +from ahriman.models.packagers import Packagers from ahriman.models.result import Result @@ -63,30 +64,35 @@ class Executor(Cleaner): """ raise NotImplementedError - def process_build(self, updates: Iterable[Package]) -> Result: + def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result: """ build packages Args: updates(Iterable[Package]): list of packages properties to build + packagers(Packagers | None, optional): optional override of username for build process + (Default value = None) Returns: Result: build result """ - def build_single(package: Package, local_path: Path) -> None: + def build_single(package: Package, local_path: Path, packager_id: str | None) -> None: self.reporter.set_building(package.base) task = Task(package, self.configuration, self.paths) task.init(local_path, self.database) - built = task.build(local_path) + built = task.build(local_path, packager_id) for src in built: dst = self.paths.packages / src.name shutil.move(src, dst) + packagers = packagers or Packagers() + result = Result() for single in updates: with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name: try: - build_single(single, Path(dir_name)) + packager = self.packager(packagers, single.base) + build_single(single, Path(dir_name), packager.packager_id) result.add_success(single) except Exception: self.reporter.set_failed(single.base) @@ -158,12 +164,14 @@ class Executor(Cleaner): return self.repo.repo_path - def process_update(self, packages: Iterable[Path]) -> Result: + def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result: """ sign packages, add them to repository and update repository database Args: packages(Iterable[Path]): list of filenames to run + packagers(Packagers | None, optional): optional override of username for build process + (Default value = None) Returns: Result: path to repository database @@ -176,13 +184,13 @@ class Executor(Cleaner): shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe) archive.filename = safe - def update_single(name: str | None, package_base: str) -> None: + def update_single(name: str | None, package_base: str, packager_key: str | None) -> None: if name is None: self.logger.warning("received empty package name for base %s", package_base) return # suppress type checking, it never can be none actually # in theory, it might be NOT packages directory, but we suppose it is full_path = self.paths.packages / name - files = self.sign.process_sign_package(full_path, package_base) + files = self.sign.process_sign_package(full_path, packager_key) for src in files: dst = self.paths.repository / safe_filename(src.name) shutil.move(src, dst) @@ -192,14 +200,17 @@ class Executor(Cleaner): current_packages = self.packages() removed_packages: list[str] = [] # list of packages which have been removed from the base updates = self.load_archives(packages) + packagers = packagers or Packagers() result = Result() for local in updates: with self.in_package_context(local.base): try: + packager = self.packager(packagers, local.base) + for description in local.packages.values(): rename(description, local.base) - update_single(description.filename, local.base) + update_single(description.filename, local.base, packager.key) self.reporter.set_success(local) result.add_success(local) diff --git a/src/ahriman/core/repository/repository_properties.py b/src/ahriman/core/repository/repository_properties.py index 2a236f53..edb8abdc 100644 --- a/src/ahriman/core/repository/repository_properties.py +++ b/src/ahriman/core/repository/repository_properties.py @@ -27,8 +27,11 @@ from ahriman.core.sign.gpg import GPG from ahriman.core.status.client import Client from ahriman.core.triggers import TriggerLoader from ahriman.core.util import check_user +from ahriman.models.packagers import Packagers from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.repository_paths import RepositoryPaths +from ahriman.models.user import User +from ahriman.models.user_access import UserAccess class RepositoryProperties(LazyLogging): @@ -83,3 +86,23 @@ class RepositoryProperties(LazyLogging): self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) self.reporter = Client.load(configuration, report=report) self.triggers = TriggerLoader.load(architecture, configuration) + + def packager(self, packagers: Packagers, package_base: str) -> User: + """ + extract packager from configuration having username + + Args: + packagers(Packagers): packagers override holder + package_base(str): package base to lookup + + Returns: + User | None: user found in database if any and empty object otherwise + """ + username = packagers.for_base(package_base) + if username is None: # none to search + return User(username="", password="", access=UserAccess.Read, packager_id=None, key=None) # nosec + + if (user := self.database.user_get(username)) is not None: # found user + return user + # empty user with the username + return User(username=username, password="", access=UserAccess.Read, packager_id=None, key=None) # nosec diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index 1d5f438b..667185ab 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -65,9 +65,9 @@ class UpdateHandler(Cleaner): try: if source == PackageSource.Repository: - remote = Package.from_official(local.base, self.pacman) + remote = Package.from_official(local.base, self.pacman, None) else: - remote = Package.from_aur(local.base, self.pacman) + remote = Package.from_aur(local.base, self.pacman, None) if local.is_outdated( remote, self.paths, @@ -98,7 +98,7 @@ class UpdateHandler(Cleaner): with self.in_package_context(cache_dir.name): try: Sources.fetch(cache_dir, remote=None) - remote = Package.from_build(cache_dir, self.architecture) + remote = Package.from_build(cache_dir, self.architecture, None) local = packages.get(remote.base) if local is None: diff --git a/src/ahriman/core/sign/gpg.py b/src/ahriman/core/sign/gpg.py index b5691a8d..eb79b92e 100644 --- a/src/ahriman/core/sign/gpg.py +++ b/src/ahriman/core/sign/gpg.py @@ -19,7 +19,6 @@ # import requests -from collections.abc import Generator from pathlib import Path from ahriman.core.configuration import Configuration @@ -165,21 +164,6 @@ class GPG(LazyLogging): key_body = self.key_download(server, key) GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger) - def keys(self) -> list[str]: - """ - extract list of keys described in configuration - - Returns: - list[str]: list of unique keys which are set in configuration - """ - def generator() -> Generator[str, None, None]: - if self.default_key is not None: - yield self.default_key - for _, value in filter(lambda pair: pair[0].startswith("key_"), self.configuration["sign"].items()): - yield value - - return sorted(set(generator())) - def process(self, path: Path, key: str) -> list[Path]: """ gpg command wrapper @@ -197,20 +181,21 @@ class GPG(LazyLogging): logger=self.logger) return [path, path.parent / f"{path.name}.sig"] - def process_sign_package(self, path: Path, package_base: str) -> list[Path]: + def process_sign_package(self, path: Path, packager_key: str | None) -> list[Path]: """ sign package if required by configuration Args: path(Path): path to file to sign - package_base(str): package base required to check for key overrides + packager_key(str | None): optional packager key to sign Returns: list[Path]: list of generated files including original file """ if SignSettings.Packages not in self.targets: return [path] - key = self.configuration.get("sign", f"key_{package_base}", fallback=self.default_key) + + key = packager_key or self.default_key if key is None: self.logger.error("no default key set, skip package %s sign", path) return [path] diff --git a/src/ahriman/core/spawn.py b/src/ahriman/core/spawn.py index fc8c55a7..c29e6243 100644 --- a/src/ahriman/core/spawn.py +++ b/src/ahriman/core/spawn.py @@ -78,7 +78,7 @@ class Spawn(Thread, LazyLogging): result = callback(args, architecture) queue.put((process_id, result)) - def _spawn_process(self, command: str, *args: str, **kwargs: str) -> None: + def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> None: """ spawn external ahriman process with supplied arguments @@ -94,6 +94,8 @@ class Spawn(Thread, LazyLogging): arguments.extend(args) # named command arguments for argument, value in kwargs.items(): + if value is None: + continue # skip null values arguments.append(f"--{argument}") if value: arguments.append(value) @@ -122,27 +124,31 @@ class Spawn(Thread, LazyLogging): kwargs = {} if server is None else {"key-server": server} self._spawn_process("service-key-import", key, **kwargs) - def packages_add(self, packages: Iterable[str], *, now: bool) -> None: + def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> None: """ add packages Args: packages(Iterable[str]): packages list to add + username(str | None): optional override of username for build process now(bool): build packages now """ - kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages + # avoid abusing by building non-aur packages + kwargs = {"source": PackageSource.AUR.value, "username": username} if now: kwargs["now"] = "" self._spawn_process("package-add", *packages, **kwargs) - def packages_rebuild(self, depends_on: str) -> None: + def packages_rebuild(self, depends_on: str, username: str | None) -> None: """ rebuild packages which depend on the specified package Args: depends_on(str): packages dependency + username(str | None): optional override of username for build process """ - self._spawn_process("repo-rebuild", **{"depends-on": depends_on}) + kwargs = {"depends-on": depends_on, "username": username} + self._spawn_process("repo-rebuild", **kwargs) def packages_remove(self, packages: Iterable[str]) -> None: """ @@ -153,11 +159,15 @@ class Spawn(Thread, LazyLogging): """ self._spawn_process("package-remove", *packages) - def packages_update(self) -> None: + def packages_update(self, username: str | None) -> None: """ run full repository update + + Args: + username(str | None): optional override of username for build process """ - self._spawn_process("repo-update") + kwargs = {"username": username} + self._spawn_process("repo-update", **kwargs) def run(self) -> None: """ diff --git a/src/ahriman/core/support/keyring_trigger.py b/src/ahriman/core/support/keyring_trigger.py index 6a83e889..fdbd4595 100644 --- a/src/ahriman/core/support/keyring_trigger.py +++ b/src/ahriman/core/support/keyring_trigger.py @@ -19,6 +19,7 @@ # from ahriman.core import context from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite from ahriman.core.sign.gpg import GPG from ahriman.core.support.package_creator import PackageCreator from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator @@ -107,8 +108,9 @@ class KeyringTrigger(Trigger): """ ctx = context.get() sign = ctx.get(ContextKey("sign", GPG)) + database = ctx.get(ContextKey("database", SQLite)) for target in self.targets: - generator = KeyringGenerator(sign, self.configuration, target) + generator = KeyringGenerator(database, sign, self.configuration, target) runner = PackageCreator(self.configuration, generator) runner.run() diff --git a/src/ahriman/core/support/package_creator.py b/src/ahriman/core/support/package_creator.py index 6ab120df..4048d27b 100644 --- a/src/ahriman/core/support/package_creator.py +++ b/src/ahriman/core/support/package_creator.py @@ -67,5 +67,5 @@ class PackageCreator: ctx = context.get() database: SQLite = ctx.get(ContextKey("database", SQLite)) _, architecture = self.configuration.check_loaded() - package = Package.from_build(local_path, architecture) + package = Package.from_build(local_path, architecture, None) database.package_update(package, BuildStatus()) diff --git a/src/ahriman/core/support/pkgbuild/keyring_generator.py b/src/ahriman/core/support/pkgbuild/keyring_generator.py index 0ddb8c3f..c9d90f45 100644 --- a/src/ahriman/core/support/pkgbuild/keyring_generator.py +++ b/src/ahriman/core/support/pkgbuild/keyring_generator.py @@ -21,6 +21,7 @@ from collections.abc import Callable from pathlib import Path from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite from ahriman.core.exceptions import PkgbuildGeneratorError from ahriman.core.sign.gpg import GPG from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator @@ -42,11 +43,12 @@ class KeyringGenerator(PkgbuildGenerator): trusted(list[str]): lif of trusted PGP keys """ - def __init__(self, sign: GPG, configuration: Configuration, section: str) -> None: + def __init__(self, database: SQLite, sign: GPG, configuration: Configuration, section: str) -> None: """ default constructor Args: + database(SQLite): database instance sign(GPG): GPG wrapper instance configuration(Configuration): configuration instance section(str): settings section name @@ -55,7 +57,8 @@ class KeyringGenerator(PkgbuildGenerator): self.name = configuration.repository_name # configuration fields - self.packagers = configuration.getlist(section, "packagers", fallback=sign.keys()) + packager_keys = [packager.key for packager in database.user_list(None, None) if packager.key is not None] + self.packagers = configuration.getlist(section, "packagers", fallback=packager_keys) self.revoked = configuration.getlist(section, "revoked", fallback=[]) self.trusted = configuration.getlist( section, "trusted", fallback=[sign.default_key] if sign.default_key is not None else []) @@ -148,10 +151,10 @@ class KeyringGenerator(PkgbuildGenerator): def install(self) -> str | None: """ - content of the install functions + content of the .install functions Returns: - str | None: content of the install functions if any + str | None: content of the .install functions if any """ # copy-paste from archlinux-keyring return f"""post_upgrade() {{ diff --git a/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py b/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py index 42787e90..b0cef942 100644 --- a/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py +++ b/src/ahriman/core/support/pkgbuild/pkgbuild_generator.py @@ -98,10 +98,10 @@ class PkgbuildGenerator: def install(self) -> str | None: """ - content of the install functions + content of the .install functions Returns: - str | None: content of the install functions if any + str | None: content of the .install functions if any """ def package(self) -> str: diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index da1de322..0c2484a4 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -28,6 +28,7 @@ import requests import subprocess from collections.abc import Callable, Generator, Iterable +from dataclasses import asdict from enum import Enum from pathlib import Path from pwd import getpwuid @@ -40,8 +41,10 @@ from ahriman.models.repository_paths import RepositoryPaths __all__ = [ "check_output", "check_user", + "dataclass_view", "enum_values", "exception_response_text", + "extract_user", "filter_json", "full_version", "package_like", @@ -61,7 +64,8 @@ T = TypeVar("T") def check_output(*args: str, exception: Exception | None = None, cwd: Path | None = None, input_data: str | None = None, - logger: logging.Logger | None = None, user: int | None = None) -> str: + logger: logging.Logger | None = None, user: int | None = None, + environment: dict[str, str] | None = None) -> str: """ subprocess wrapper @@ -73,6 +77,7 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non input_data(str | None, optional): data which will be written to command stdin (Default value = None) logger(logging.Logger | None, optional): logger to log command result if required (Default value = None) user(int | None, optional): run process as specified user (Default value = None) + environment(dict[str, str] | None, optional): optional environment variables if any (Default value = None) Returns: str: command output @@ -106,7 +111,9 @@ def check_output(*args: str, exception: Exception | None = None, cwd: Path | Non if logger is not None: logger.debug(single) - environment = {"HOME": getpwuid(user).pw_dir} if user is not None else {} + environment = environment or {} + if user is not None: + environment["HOME"] = getpwuid(user).pw_dir # FIXME additional workaround for linter and type check which do not know that user arg is supported # pylint: disable=unexpected-keyword-arg with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -163,6 +170,19 @@ def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None: raise UnsafeRunError(current_uid, root_uid) +def dataclass_view(instance: Any) -> dict[str, Any]: + """ + convert dataclass instance to json object + + Args: + instance(Any): dataclass instance + + Returns: + dict[str, Any]: json representation of the dataclass with empty field removed + """ + return asdict(instance, dict_factory=lambda fields: {key: value for key, value in fields if value is not None}) + + def enum_values(enum: type[Enum]) -> list[str]: """ generate list of enumeration values from the source @@ -190,6 +210,17 @@ def exception_response_text(exception: requests.exceptions.RequestException) -> return result +def extract_user() -> str | None: + """ + extract user from system environment + + Returns: + str | None: SUDO_USER in case if set and USER otherwise. It can return None in case if environment has been + cleared before application start + """ + return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER") + + def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]: """ filter json object by fields used for json-to-object conversion diff --git a/src/ahriman/models/internal_status.py b/src/ahriman/models/internal_status.py index 1dbb6444..ca9f643c 100644 --- a/src/ahriman/models/internal_status.py +++ b/src/ahriman/models/internal_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 dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field from typing import Any, Self +from ahriman.core.util import dataclass_view from ahriman.models.build_status import BuildStatus from ahriman.models.counters import Counters @@ -69,4 +70,4 @@ class InternalStatus: Returns: dict[str, Any]: json-friendly dictionary """ - return asdict(self) + return dataclass_view(self) diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 9f63e2a5..6852a4cb 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -23,7 +23,7 @@ from __future__ import annotations import copy from collections.abc import Callable, Generator, Iterable -from dataclasses import asdict, dataclass +from dataclasses import dataclass from pathlib import Path from pyalpm import vercmp # type: ignore[import] from srcinfo.parse import parse_srcinfo # type: ignore[import] @@ -34,7 +34,7 @@ from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb from ahriman.core.exceptions import PackageInfoError from ahriman.core.log import LazyLogging -from ahriman.core.util import check_output, full_version, srcinfo_property_list, utcnow +from ahriman.core.util import check_output, dataclass_view, full_version, srcinfo_property_list, utcnow from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource from ahriman.models.remote_source import RemoteSource @@ -48,6 +48,7 @@ class Package(LazyLogging): Attributes: base(str): package base name + packager(str | None): package packager if available packages(dict[str, PackageDescription): map of package names to their properties. Filled only on load from archive remote(RemoteSource | None): package remote source if applicable @@ -77,6 +78,7 @@ class Package(LazyLogging): version: str remote: RemoteSource | None packages: dict[str, PackageDescription] + packager: str | None = None _check_output = check_output @@ -204,16 +206,18 @@ class Package(LazyLogging): """ package = pacman.handle.load_pkg(str(path)) description = PackageDescription.from_package(package, path) - return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description}) + return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description}, + packager=package.packager) @classmethod - def from_aur(cls, name: str, pacman: Pacman) -> Self: + def from_aur(cls, name: str, pacman: Pacman, packager: str | None = None) -> Self: """ construct package properties from AUR page Args: name(str): package name (either base or normal name) pacman(Pacman): alpm wrapper instance + packager(str | None, optional): packager to be used for this build (Default value = None) Returns: Self: package properties @@ -224,16 +228,19 @@ class Package(LazyLogging): base=package.package_base, version=package.version, remote=remote, - packages={package.name: PackageDescription.from_aur(package)}) + packages={package.name: PackageDescription.from_aur(package)}, + packager=packager, + ) @classmethod - def from_build(cls, path: Path, architecture: str) -> Self: + def from_build(cls, path: Path, architecture: str, packager: str | None = None) -> Self: """ construct package properties from sources directory Args: path(Path): path to package sources directory architecture(str): load package for specific architecture + packager(str | None, optional): packager to be used for this build (Default value = None) Returns: Self: package properties @@ -265,7 +272,7 @@ class Package(LazyLogging): source=PackageSource.Local, ) - return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages) + return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages, packager=packager) @classmethod def from_json(cls, dump: dict[str, Any]) -> Self: @@ -284,16 +291,18 @@ class Package(LazyLogging): for key, value in packages_json.items() } remote = dump.get("remote") or {} - return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages) + return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages, + packager=dump.get("packager")) @classmethod - def from_official(cls, name: str, pacman: Pacman, *, use_syncdb: bool = True) -> Self: + def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True) -> Self: """ construct package properties from official repository page Args: name(str): package name (either base or normal name) pacman(Pacman): alpm wrapper instance + packager(str | None, optional): packager to be used for this build (Default value = None) use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True) Returns: @@ -305,7 +314,9 @@ class Package(LazyLogging): base=package.package_base, version=package.version, remote=remote, - packages={package.name: PackageDescription.from_aur(package)}) + packages={package.name: PackageDescription.from_aur(package)}, + packager=packager, + ) @staticmethod def local_files(path: Path) -> Generator[Path, None, None]: @@ -513,4 +524,4 @@ class Package(LazyLogging): Returns: dict[str, Any]: json-friendly dictionary """ - return asdict(self) + return dataclass_view(self) diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py index 18dd96cb..6a41a260 100644 --- a/src/ahriman/models/package_description.py +++ b/src/ahriman/models/package_description.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from dataclasses import asdict, dataclass, field, fields +from dataclasses import dataclass, field, fields from pathlib import Path from pyalpm import Package # type: ignore[import] from typing import Any, Self -from ahriman.core.util import filter_json, trim_package +from ahriman.core.util import dataclass_view, filter_json, trim_package from ahriman.models.aur_package import AURPackage @@ -172,4 +172,4 @@ class PackageDescription: Returns: dict[str, Any]: json-friendly dictionary """ - return asdict(self) + return dataclass_view(self) diff --git a/src/ahriman/models/packagers.py b/src/ahriman/models/packagers.py new file mode 100644 index 00000000..0fd6ab0c --- /dev/null +++ b/src/ahriman/models/packagers.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2021-2023 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 dataclasses import dataclass, field + + +@dataclass(frozen=True) +class Packagers: + """ + holder for packagers overrides + + Attributes: + default(str | None): default packager username if any to be used if no override for the specified base was found + overrides: dict[str, str | None]: packager username override for specific package base + """ + + default: str | None = None + overrides: dict[str, str | None] = field(default_factory=dict) + + def for_base(self, package_base: str) -> str | None: + """ + extract username for the specified package base + + Args: + package_base(str): package base to lookup + + Returns: + str | None: package base override if set and default packager username otherwise + """ + return self.overrides.get(package_base) or self.default diff --git a/src/ahriman/models/remote_source.py b/src/ahriman/models/remote_source.py index 15c89f95..39552a44 100644 --- a/src/ahriman/models/remote_source.py +++ b/src/ahriman/models/remote_source.py @@ -17,11 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from dataclasses import asdict, dataclass, fields +from dataclasses import dataclass, fields from pathlib import Path from typing import Any, Self -from ahriman.core.util import filter_json +from ahriman.core.util import dataclass_view, filter_json from ahriman.models.package_source import PackageSource @@ -118,4 +118,4 @@ class RemoteSource: Returns: dict[str, Any]: json-friendly dictionary """ - return asdict(self) + return dataclass_view(self) diff --git a/src/ahriman/models/user.py b/src/ahriman/models/user.py index 8c9b1864..08bc58c9 100644 --- a/src/ahriman/models/user.py +++ b/src/ahriman/models/user.py @@ -34,12 +34,14 @@ class User: username(str): username password(str): hashed user password with salt access(UserAccess): user role + packager_id(str | None): packager id to be used. If not set, the default service packager will be used + key(str | None): personal packager key if any. If user id is empty, it is interpreted as default key Examples: Simply create user from database data and perform required validation:: >>> password = User.generate_password(24) - >>> user = User("ahriman", password, UserAccess.Full) + >>> user = User(username="ahriman", password=password, access=UserAccess.Full, packager_id=None, key=None) Since the password supplied may be plain text, the ``hash_password`` method can be used to hash the password:: @@ -61,9 +63,18 @@ class User: username: str password: str access: UserAccess + packager_id: str | None + key: str | None _HASHER = sha512_crypt + def __post_init__(self) -> None: + """ + remove empty fields + """ + object.__setattr__(self, "packager_id", self.packager_id or None) + object.__setattr__(self, "key", self.key or None) + @classmethod def from_option(cls, username: str | None, password: str | None, access: UserAccess = UserAccess.Read) -> Self | None: @@ -80,7 +91,7 @@ class User: """ if username is None or password is None: return None - return cls(username=username, password=password, access=access) + return cls(username=username, password=password, access=access, packager_id=None, key=None) @staticmethod def generate_password(length: int) -> str: @@ -149,4 +160,4 @@ class User: Returns: str: unique string representation """ - return f"User(username={self.username}, access={self.access})" + return f"User(username={self.username}, access={self.access}, packager_id={self.packager_id}, key={self.key})" diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py index 82360f83..f2bc0a35 100644 --- a/src/ahriman/web/middlewares/auth_handler.py +++ b/src/ahriman/web/middlewares/auth_handler.py @@ -148,7 +148,7 @@ def setup_auth(application: Application, configuration: Configuration, validator setup_session(application, storage) authorization_policy = _AuthorizationPolicy(validator) - identity_policy = aiohttp_security.SessionIdentityPolicy() + identity_policy = application["identity"] = aiohttp_security.SessionIdentityPolicy() aiohttp_security.setup(application, identity_policy, authorization_policy) application.middlewares.append(_auth_handler(validator.allow_read_only)) diff --git a/src/ahriman/web/schemas/package_schema.py b/src/ahriman/web/schemas/package_schema.py index 5cbe8117..5747fcb8 100644 --- a/src/ahriman/web/schemas/package_schema.py +++ b/src/ahriman/web/schemas/package_schema.py @@ -44,3 +44,7 @@ class PackageSchema(Schema): keys=fields.String(), values=fields.Nested(PackagePropertiesSchema()), required=True, metadata={ "description": "Packages which belong to this base", }) + packager = fields.String(metadata={ + "description": "packager for the last success package build", + "example": "John Doe ", + }) diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py index 5a57c084..44cf2747 100644 --- a/src/ahriman/web/views/base.py +++ b/src/ahriman/web/views/base.py @@ -183,3 +183,16 @@ class BaseView(View, CorsViewMixin): return response self._raise_allowed_methods() + + async def username(self) -> str | None: + """ + extract username from request if any + + Returns: + str | None: authorized username if any and None otherwise (e.g. if authorization is disabled) + """ + policy = self.request.app.get("identity") + if policy is not None: + identity: str = await policy.identify(self.request) + return identity + return None diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py index e74524fc..2d800b46 100644 --- a/src/ahriman/web/views/service/add.py +++ b/src/ahriman/web/views/service/add.py @@ -67,6 +67,7 @@ class AddView(BaseView): except Exception as e: raise HTTPBadRequest(reason=str(e)) - self.spawner.packages_add(packages, now=True) + username = await self.username() + self.spawner.packages_add(packages, username, now=True) raise HTTPNoContent() diff --git a/src/ahriman/web/views/service/rebuild.py b/src/ahriman/web/views/service/rebuild.py index 7ee12b78..0fe24236 100644 --- a/src/ahriman/web/views/service/rebuild.py +++ b/src/ahriman/web/views/service/rebuild.py @@ -68,6 +68,7 @@ class RebuildView(BaseView): except Exception as e: raise HTTPBadRequest(reason=str(e)) - self.spawner.packages_rebuild(depends_on) + username = await self.username() + self.spawner.packages_rebuild(depends_on, username) raise HTTPNoContent() diff --git a/src/ahriman/web/views/service/request.py b/src/ahriman/web/views/service/request.py index 7e5dd695..54fd1161 100644 --- a/src/ahriman/web/views/service/request.py +++ b/src/ahriman/web/views/service/request.py @@ -67,6 +67,7 @@ class RequestView(BaseView): except Exception as e: raise HTTPBadRequest(reason=str(e)) - self.spawner.packages_add(packages, now=False) + username = await self.username() + self.spawner.packages_add(packages, username, now=False) raise HTTPNoContent() diff --git a/src/ahriman/web/views/service/update.py b/src/ahriman/web/views/service/update.py index a164f563..1f9a4572 100644 --- a/src/ahriman/web/views/service/update.py +++ b/src/ahriman/web/views/service/update.py @@ -57,6 +57,7 @@ class UpdateView(BaseView): Raises: HTTPNoContent: in case of success response """ - self.spawner.packages_update() + username = await self.username() + self.spawner.packages_update(username) raise HTTPNoContent() diff --git a/tests/ahriman/application/application/test_application.py b/tests/ahriman/application/application/test_application.py index 04ba48a8..2cf922ed 100644 --- a/tests/ahriman/application/application/test_application.py +++ b/tests/ahriman/application/application/test_application.py @@ -72,16 +72,16 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p "python-installer": create_package_mock("python-installer"), } - package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda p, _: packages[p]) + package_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda *args: packages[args[0]]) packages_mock = mocker.patch("ahriman.application.application.Application._known_packages", - return_value=["devtools", "python-build", "python-pytest"]) + return_value={"devtools", "python-build", "python-pytest"}) result = application.with_dependencies([package_ahriman], process_dependencies=True) assert {package.base: package for package in result} == packages package_mock.assert_has_calls([ - MockCall(package_python_schedule.base, application.repository.pacman), - MockCall("python", application.repository.pacman), - MockCall("python-installer", application.repository.pacman), + MockCall(package_python_schedule.base, application.repository.pacman, package_ahriman.packager), + MockCall("python", application.repository.pacman, package_ahriman.packager), + MockCall("python-installer", application.repository.pacman, package_ahriman.packager), ], any_order=True) packages_mock.assert_called_once_with() diff --git a/tests/ahriman/application/application/test_application_packages.py b/tests/ahriman/application/application/test_application_packages.py index dd03caa1..1e96a7f4 100644 --- a/tests/ahriman/application/application/test_application_packages.py +++ b/tests/ahriman/application/application/test_application_packages.py @@ -43,7 +43,7 @@ def test_add_aur(application_packages: ApplicationPackages, package_ahriman: Pac build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update") - application_packages._add_aur(package_ahriman.base) + application_packages._add_aur(package_ahriman.base, "packager") build_queue_mock.assert_called_once_with(package_ahriman) update_remote_mock.assert_called_once_with(package_ahriman) @@ -83,7 +83,7 @@ def test_add_local(application_packages: ApplicationPackages, package_ahriman: P copytree_mock = mocker.patch("shutil.copytree") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") - application_packages._add_local(package_ahriman.base) + application_packages._add_local(package_ahriman.base, "packager") is_dir_mock.assert_called_once_with() copytree_mock.assert_called_once_with( Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)) @@ -103,7 +103,7 @@ def test_add_local_cache(application_packages: ApplicationPackages, package_ahri copytree_mock = mocker.patch("shutil.copytree") build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") - application_packages._add_local(package_ahriman.base) + application_packages._add_local(package_ahriman.base, "packager") copytree_mock.assert_not_called() init_mock.assert_not_called() build_queue_mock.assert_called_once_with(package_ahriman) @@ -115,7 +115,7 @@ def test_add_local_missing(application_packages: ApplicationPackages, mocker: Mo """ mocker.patch("pathlib.Path.is_dir", return_value=False) with pytest.raises(UnknownPackageError): - application_packages._add_local("package") + application_packages._add_local("package", "packager") def test_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription, @@ -153,7 +153,7 @@ def test_add_repository(application_packages: ApplicationPackages, package_ahrim build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_insert") update_remote_mock = mocker.patch("ahriman.core.database.SQLite.remote_update") - application_packages._add_repository(package_ahriman.base) + application_packages._add_repository(package_ahriman.base, "packager") build_queue_mock.assert_called_once_with(package_ahriman) update_remote_mock.assert_called_once_with(package_ahriman) @@ -165,8 +165,8 @@ def test_add_add_archive(application_packages: ApplicationPackages, package_ahri """ add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_archive") - application_packages.add([package_ahriman.base], PackageSource.Archive) - add_mock.assert_called_once_with(package_ahriman.base) + application_packages.add([package_ahriman.base], PackageSource.Archive, "packager") + add_mock.assert_called_once_with(package_ahriman.base, "packager") def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: Package, @@ -176,8 +176,8 @@ def test_add_add_aur(application_packages: ApplicationPackages, package_ahriman: """ add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_aur") - application_packages.add([package_ahriman.base], PackageSource.AUR) - add_mock.assert_called_once_with(package_ahriman.base) + application_packages.add([package_ahriman.base], PackageSource.AUR, "packager") + add_mock.assert_called_once_with(package_ahriman.base, "packager") def test_add_add_directory(application_packages: ApplicationPackages, package_ahriman: Package, @@ -187,8 +187,8 @@ def test_add_add_directory(application_packages: ApplicationPackages, package_ah """ add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_directory") - application_packages.add([package_ahriman.base], PackageSource.Directory) - add_mock.assert_called_once_with(package_ahriman.base) + application_packages.add([package_ahriman.base], PackageSource.Directory, "packager") + add_mock.assert_called_once_with(package_ahriman.base, "packager") def test_add_add_local(application_packages: ApplicationPackages, package_ahriman: Package, @@ -198,8 +198,8 @@ def test_add_add_local(application_packages: ApplicationPackages, package_ahrima """ add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_local") - application_packages.add([package_ahriman.base], PackageSource.Local) - add_mock.assert_called_once_with(package_ahriman.base) + application_packages.add([package_ahriman.base], PackageSource.Local, "packager") + add_mock.assert_called_once_with(package_ahriman.base, "packager") def test_add_add_remote(application_packages: ApplicationPackages, package_description_ahriman: PackageDescription, @@ -210,8 +210,8 @@ def test_add_add_remote(application_packages: ApplicationPackages, package_descr add_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._add_remote") url = f"https://host/{package_description_ahriman.filename}" - application_packages.add([url], PackageSource.Remote) - add_mock.assert_called_once_with(url) + application_packages.add([url], PackageSource.Remote, "packager") + add_mock.assert_called_once_with(url, "packager") def test_on_result(application_packages: ApplicationPackages) -> None: diff --git a/tests/ahriman/application/application/test_application_repository.py b/tests/ahriman/application/application/test_application_repository.py index d0b80cd3..2bc613a2 100644 --- a/tests/ahriman/application/application/test_application_repository.py +++ b/tests/ahriman/application/application/test_application_repository.py @@ -76,9 +76,9 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa application_repository.sign([]) sign_package_mock.assert_has_calls([ - MockCall(pytest.helpers.anyvar(int), package_ahriman.base), - MockCall(pytest.helpers.anyvar(int), package_python_schedule.base), - MockCall(pytest.helpers.anyvar(int), package_python_schedule.base), + MockCall(pytest.helpers.anyvar(int), None), + MockCall(pytest.helpers.anyvar(int), None), + MockCall(pytest.helpers.anyvar(int), None), ]) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) on_result_mock.assert_called_once_with(Result()) @@ -111,7 +111,7 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah filename = package_ahriman.packages[package_ahriman.base].filepath application_repository.sign([package_ahriman.base]) - sign_package_mock.assert_called_once_with(filename, package_ahriman.base) + sign_package_mock.assert_called_once_with(filename, None) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) on_result_mock.assert_called_once_with(Result()) @@ -170,9 +170,9 @@ def test_update(application_repository: ApplicationRepository, package_ahriman: on_result_mock = mocker.patch( "ahriman.application.application.application_repository.ApplicationRepository.on_result") - application_repository.update([package_ahriman]) - build_mock.assert_called_once_with([package_ahriman]) - update_mock.assert_has_calls([MockCall(paths), MockCall(paths)]) + application_repository.update([package_ahriman], "username") + build_mock.assert_called_once_with([package_ahriman], "username") + update_mock.assert_has_calls([MockCall(paths, "username"), MockCall(paths, "username")]) on_result_mock.assert_has_calls([MockCall(result), MockCall(result)]) diff --git a/tests/ahriman/application/handlers/test_handler_add.py b/tests/ahriman/application/handlers/test_handler_add.py index a8ed05a1..5e10e930 100644 --- a/tests/ahriman/application/handlers/test_handler_add.py +++ b/tests/ahriman/application/handlers/test_handler_add.py @@ -8,6 +8,7 @@ from ahriman.core.configuration import Configuration from ahriman.core.repository import Repository from ahriman.models.package import Package from ahriman.models.package_source import PackageSource +from ahriman.models.packagers import Packagers from ahriman.models.result import Result @@ -27,6 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.refresh = 0 args.source = PackageSource.Auto args.dependencies = True + args.username = "username" return args @@ -42,7 +44,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") Add.run(args, "x86_64", configuration, report=False, unsafe=False) - application_mock.assert_called_once_with(args.package, args.source) + application_mock.assert_called_once_with(args.package, args.source, args.username) dependencies_mock.assert_not_called() on_start_mock.assert_called_once_with() @@ -67,7 +69,8 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration Add.run(args, "x86_64", configuration, report=False, unsafe=False) updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False, log_fn=pytest.helpers.anyvar(int)) - application_mock.assert_called_once_with([package_ahriman]) + application_mock.assert_called_once_with([package_ahriman], + Packagers(args.username, {package_ahriman.base: "packager"})) dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies) check_mock.assert_called_once_with(False, False) diff --git a/tests/ahriman/application/handlers/test_handler_patch.py b/tests/ahriman/application/handlers/test_handler_patch.py index 58e2ceb0..e3628b71 100644 --- a/tests/ahriman/application/handlers/test_handler_patch.py +++ b/tests/ahriman/application/handlers/test_handler_patch.py @@ -109,7 +109,7 @@ def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value) assert Patch.patch_create_from_diff(path, "x86_64", ["*.diff"]) == (package_ahriman.base, patch) - package_mock.assert_called_once_with(path, "x86_64") + package_mock.assert_called_once_with(path, "x86_64", None) sources_mock.assert_called_once_with(path, "*.diff") diff --git a/tests/ahriman/application/handlers/test_handler_rebuild.py b/tests/ahriman/application/handlers/test_handler_rebuild.py index 4582e309..1918cb66 100644 --- a/tests/ahriman/application/handlers/test_handler_rebuild.py +++ b/tests/ahriman/application/handlers/test_handler_rebuild.py @@ -28,6 +28,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.from_database = False args.exit_code = False args.status = None + args.username = "username" return args @@ -50,7 +51,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration: Rebuild.run(args, "x86_64", configuration, report=False, unsafe=False) extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database) application_packages_mock.assert_called_once_with([package_ahriman], None) - application_mock.assert_called_once_with([package_ahriman]) + application_mock.assert_called_once_with([package_ahriman], args.username) check_mock.assert_has_calls([MockCall(False, False), MockCall(False, False)]) on_start_mock.assert_called_once_with() diff --git a/tests/ahriman/application/handlers/test_handler_service_updates.py b/tests/ahriman/application/handlers/test_handler_service_updates.py index 80811466..d62815f0 100644 --- a/tests/ahriman/application/handlers/test_handler_service_updates.py +++ b/tests/ahriman/application/handlers/test_handler_service_updates.py @@ -36,7 +36,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository: check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") ServiceUpdates.run(args, "x86_64", configuration, report=False, unsafe=False) - package_mock.assert_called_once_with(package_ahriman.base, repository.pacman) + package_mock.assert_called_once_with(package_ahriman.base, repository.pacman, None) application_mock.assert_called_once_with(verbose=True, separator=" -> ") check_mock.assert_called_once_with(args.exit_code, True) diff --git a/tests/ahriman/application/handlers/test_handler_unsafe_commands.py b/tests/ahriman/application/handlers/test_handler_unsafe_commands.py index 2d3d953d..29513530 100644 --- a/tests/ahriman/application/handlers/test_handler_unsafe_commands.py +++ b/tests/ahriman/application/handlers/test_handler_unsafe_commands.py @@ -19,7 +19,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: argparse.Namespace: generated arguments for these test cases """ args.parser = _parser - args.command = None + args.command = [] return args @@ -42,14 +42,14 @@ def test_run_check(args: argparse.Namespace, configuration: Configuration, mocke must run command and check if command is unsafe """ args = _default_args(args) - args.command = "clean" + args.command = ["clean"] commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands", return_value=["command"]) check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe") UnsafeCommands.run(args, "x86_64", configuration, report=False, unsafe=False) commands_mock.assert_called_once_with(pytest.helpers.anyvar(int)) - check_mock.assert_called_once_with("clean", ["command"], pytest.helpers.anyvar(int)) + check_mock.assert_called_once_with(["clean"], ["command"], pytest.helpers.anyvar(int)) def test_check_unsafe(mocker: MockerFixture) -> None: @@ -57,7 +57,7 @@ def test_check_unsafe(mocker: MockerFixture) -> None: must check if command is unsafe """ check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - UnsafeCommands.check_unsafe("service-clean", ["service-clean"], _parser()) + UnsafeCommands.check_unsafe(["service-clean"], ["service-clean"], _parser()) check_mock.assert_called_once_with(True, True) @@ -66,7 +66,7 @@ def test_check_unsafe_safe(mocker: MockerFixture) -> None: must check if command is safe """ check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") - UnsafeCommands.check_unsafe("package-status", ["service-clean"], _parser()) + UnsafeCommands.check_unsafe(["package-status"], ["service-clean"], _parser()) check_mock.assert_called_once_with(True, False) diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py index c83462f7..8ce3898f 100644 --- a/tests/ahriman/application/handlers/test_handler_update.py +++ b/tests/ahriman/application/handlers/test_handler_update.py @@ -9,6 +9,7 @@ from ahriman.application.handlers import Update from ahriman.core.configuration import Configuration from ahriman.core.repository import Repository from ahriman.models.package import Package +from ahriman.models.packagers import Packagers from ahriman.models.result import Result @@ -31,6 +32,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.manual = True args.vcs = True args.refresh = 0 + args.username = "username" return args @@ -51,7 +53,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration: on_start_mock = mocker.patch("ahriman.application.application.Application.on_start") Update.run(args, "x86_64", configuration, report=False, unsafe=False) - application_mock.assert_called_once_with([package_ahriman]) + application_mock.assert_called_once_with([package_ahriman], + Packagers(args.username, {package_ahriman.base: "packager"})) updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, log_fn=pytest.helpers.anyvar(int)) dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies) diff --git a/tests/ahriman/application/handlers/test_handler_users.py b/tests/ahriman/application/handlers/test_handler_users.py index 53cdd2d4..381c1b0c 100644 --- a/tests/ahriman/application/handlers/test_handler_users.py +++ b/tests/ahriman/application/handlers/test_handler_users.py @@ -27,6 +27,8 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.username = "user" args.action = Action.Update args.exit_code = False + args.key = "key" + args.packager = "packager" args.password = "pa55w0rd" args.role = UserAccess.Reporter args.secure = False @@ -38,7 +40,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, database: S must run command """ args = _default_args(args) - user = User(username=args.username, password=args.password, access=args.role) + user = User(username=args.username, password=args.password, access=args.role, + packager_id=args.packager, key=args.key) mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.models.user.User.hash_password", return_value=user) get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.Users.configuration_get") @@ -61,7 +64,8 @@ def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration, must create configuration if salt was not set """ args = _default_args(args) - user = User(username=args.username, password=args.password, access=args.role) + user = User(username=args.username, password=args.password, access=args.role, + packager_id=args.packager, key=args.key) mocker.patch("ahriman.core.database.SQLite.load", return_value=database) mocker.patch("ahriman.models.user.User.hash_password", return_value=user) get_auth_configuration_mock = mocker.patch("ahriman.application.handlers.Users.configuration_get") diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 46826bd2..171306ac 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -351,13 +351,14 @@ def test_subparsers_repo_backup_architecture(parser: argparse.ArgumentParser) -> def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None: """ - repo-check command must imply dependencies, dry-run, aur and manual + repo-check command must imply dependencies, dry-run, aur, manual and username """ args = parser.parse_args(["repo-check"]) assert not args.dependencies assert args.dry_run assert args.aur assert not args.manual + assert args.username is None def test_subparsers_repo_check_architecture(parser: argparse.ArgumentParser) -> None: @@ -757,14 +758,13 @@ def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> Non def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None: """ - user-list command must imply action, architecture, lock, report, password, quiet and unsafe + user-list command must imply action, architecture, lock, report, quiet and unsafe """ args = parser.parse_args(["user-list"]) assert args.action == Action.List assert args.architecture == [""] assert args.lock is None assert not args.report - assert args.password is not None assert args.quiet assert args.unsafe @@ -787,14 +787,13 @@ def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> No def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None: """ - user-remove command must imply action, architecture, lock, report, password and quiet + user-remove command must imply action, architecture, lock, report and quiet """ args = parser.parse_args(["user-remove", "username"]) assert args.action == Action.Remove assert args.architecture == [""] assert args.lock is None assert not args.report - assert args.password is not None assert args.quiet diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 42fde054..0bcb3559 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -265,7 +265,8 @@ def package_ahriman(package_description_ahriman: PackageDescription, remote_sour base="ahriman", version="2.6.0-1", remote=remote_source, - packages=packages) + packages=packages, + packager="packager") @pytest.fixture @@ -499,7 +500,7 @@ def user() -> User: Returns: User: user descriptor instance """ - return User(username="user", password="pa55w0rd", access=UserAccess.Reporter) + return User(username="user", password="pa55w0rd", access=UserAccess.Reporter, packager_id="packager", key="key") @pytest.fixture diff --git a/tests/ahriman/core/build_tools/test_task.py b/tests/ahriman/core/build_tools/test_task.py index 44b5669b..e2a7f50f 100644 --- a/tests/ahriman/core/build_tools/test_task.py +++ b/tests/ahriman/core/build_tools/test_task.py @@ -10,7 +10,7 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None: must build package """ check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output") - task_ahriman.build(Path("ahriman")) + task_ahriman.build(Path("ahriman"), "packager") check_output_mock.assert_called() diff --git a/tests/ahriman/core/database/migrations/test_m007_check_depends.py b/tests/ahriman/core/database/migrations/test_m007_check_depends.py index aa7525fd..53908bee 100644 --- a/tests/ahriman/core/database/migrations/test_m007_check_depends.py +++ b/tests/ahriman/core/database/migrations/test_m007_check_depends.py @@ -27,7 +27,7 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock def test_migrate_package_depends(connection: Connection, configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must update make and opt depends list + must update check depends list """ mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath]) @@ -45,7 +45,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None: """ - must skip update make and opt depends list if no repository directory found + must skip update check depends list if no repository directory found """ mocker.patch("pathlib.Path.is_dir", return_value=False) migrate_package_check_depends(connection, configuration) diff --git a/tests/ahriman/core/database/migrations/test_m008_packagers.py b/tests/ahriman/core/database/migrations/test_m008_packagers.py new file mode 100644 index 00000000..71e626ed --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m008_packagers.py @@ -0,0 +1,52 @@ +import pytest + +from pytest_mock import MockerFixture +from sqlite3 import Connection + +from ahriman.core.configuration import Configuration +from ahriman.core.database.migrations.m008_packagers import migrate_data, migrate_package_base_packager, steps +from ahriman.models.package import Package + + +def test_migration_packagers() -> None: + """ + migration must not be empty + """ + assert steps + + +def test_migrate_data(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must perform data migration + """ + depends_mock = mocker.patch("ahriman.core.database.migrations.m008_packagers.migrate_package_base_packager") + migrate_data(connection, configuration) + depends_mock.assert_called_once_with(connection, configuration) + + +def test_migrate_package_base_packager(connection: Connection, configuration: Configuration, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must update packagers + """ + mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.packages[package_ahriman.base].filepath]) + package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman) + + migrate_package_base_packager(connection, configuration) + package_mock.assert_called_once_with( + package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int), remote=None) + connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ + "package_base": package_ahriman.base, + "packager": package_ahriman.packager, + }]) + + +def test_migrate_package_depends_skip(connection: Connection, configuration: Configuration, + mocker: MockerFixture) -> None: + """ + must skip update packagers if no repository directory found + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) + migrate_package_base_packager(connection, configuration) + connection.executemany.assert_not_called() diff --git a/tests/ahriman/core/database/operations/test_auth_operations.py b/tests/ahriman/core/database/operations/test_auth_operations.py index 1af5c344..8af74293 100644 --- a/tests/ahriman/core/database/operations/test_auth_operations.py +++ b/tests/ahriman/core/database/operations/test_auth_operations.py @@ -16,21 +16,22 @@ def test_user_list(database: SQLite, user: User) -> None: must return all users """ database.user_update(user) - database.user_update(User(username=user.password, password=user.username, access=user.access)) + second = User(username=user.password, password=user.username, access=user.access, packager_id=None, key=None) + database.user_update(second) users = database.user_list(None, None) assert len(users) == 2 assert user in users - assert User(username=user.password, password=user.username, access=user.access) in users + assert second in users def test_user_list_filter_by_username(database: SQLite) -> None: """ must return users filtered by its id """ - first = User(username="1", password="", access=UserAccess.Read) - second = User(username="2", password="", access=UserAccess.Full) - third = User(username="3", password="", access=UserAccess.Read) + first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None) + second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None) + third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None) database.user_update(first) database.user_update(second) @@ -45,9 +46,9 @@ def test_user_list_filter_by_access(database: SQLite) -> None: """ must return users filtered by its access """ - first = User(username="1", password="", access=UserAccess.Read) - second = User(username="2", password="", access=UserAccess.Full) - third = User(username="3", password="", access=UserAccess.Read) + first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None) + second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None) + third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None) database.user_update(first) database.user_update(second) @@ -63,9 +64,9 @@ def test_user_list_filter_by_username_access(database: SQLite) -> None: """ must return users filtered by its access and username """ - first = User(username="1", password="", access=UserAccess.Read) - second = User(username="2", password="", access=UserAccess.Full) - third = User(username="3", password="", access=UserAccess.Read) + first = User(username="1", password="", access=UserAccess.Read, packager_id=None, key=None) + second = User(username="2", password="", access=UserAccess.Full, packager_id=None, key=None) + third = User(username="3", password="", access=UserAccess.Read, packager_id=None, key=None) database.user_update(first) database.user_update(second) @@ -91,6 +92,7 @@ def test_user_update(database: SQLite, user: User) -> None: database.user_update(user) assert database.user_get(user.username) == user - new_user = User(username=user.username, password=user.hash_password("salt").password, access=UserAccess.Full) + new_user = User(username=user.username, password=user.hash_password("salt").password, access=UserAccess.Full, + packager_id=None, key="new key") database.user_update(new_user) assert database.user_get(new_user.username) == new_user diff --git a/tests/ahriman/core/gitremote/test_remote_push.py b/tests/ahriman/core/gitremote/test_remote_push.py index 0e06f5b8..205efebc 100644 --- a/tests/ahriman/core/gitremote/test_remote_push.py +++ b/tests/ahriman/core/gitremote/test_remote_push.py @@ -32,7 +32,7 @@ def test_package_update(database: SQLite, configuration: Configuration, package_ fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_get", return_value=[patch1, patch2]) patches_write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write") - runner = RemotePush(configuration, database, "gitremote") + runner = RemotePush(database, configuration, "gitremote") assert runner.package_update(package_ahriman, local) == package_ahriman.base glob_mock.assert_called_once_with(".git*") @@ -56,7 +56,7 @@ def test_packages_update(database: SQLite, configuration: Configuration, result: """ update_mock = mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.package_update", return_value=[package_ahriman.base]) - runner = RemotePush(configuration, database, "gitremote") + runner = RemotePush(database, configuration, "gitremote") local = Path("local") assert list(runner.packages_update(result, local)) @@ -71,7 +71,7 @@ def test_run(database: SQLite, configuration: Configuration, result: Result, pac mocker.patch("ahriman.core.gitremote.remote_push.RemotePush.packages_update", return_value=[package_ahriman.base]) fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") push_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.push") - runner = RemotePush(configuration, database, "gitremote") + runner = RemotePush(database, configuration, "gitremote") runner.run(result) fetch_mock.assert_called_once_with(pytest.helpers.anyvar(int), runner.remote_source) @@ -85,7 +85,7 @@ def test_run_failed(database: SQLite, configuration: Configuration, result: Resu must reraise exception on error occurred """ mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception()) - runner = RemotePush(configuration, database, "gitremote") + runner = RemotePush(database, configuration, "gitremote") with pytest.raises(GitRemoteError): runner.run(result) diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py index 0275cbf5..c2c477c9 100644 --- a/tests/ahriman/core/repository/test_executor.py +++ b/tests/ahriman/core/repository/test_executor.py @@ -6,6 +6,8 @@ from unittest.mock import call as MockCall from ahriman.core.repository.executor import Executor from ahriman.models.package import Package +from ahriman.models.packagers import Packagers +from ahriman.models.user import User def test_load_archives(executor: Executor) -> None: @@ -33,7 +35,7 @@ def test_process_build(executor: Executor, package_ahriman: Package, mocker: Moc move_mock = mocker.patch("shutil.move") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") - executor.process_build([package_ahriman]) + executor.process_build([package_ahriman], Packagers("packager")) # must move files (once) move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) # must update status @@ -157,7 +159,7 @@ def test_process_remove_unknown(executor: Executor, package_ahriman: Package, mo status_client_mock.assert_called_once_with(package_ahriman.base) -def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: +def test_process_update(executor: Executor, package_ahriman: Package, user: User, mocker: MockerFixture) -> None: """ must run update process """ @@ -168,14 +170,16 @@ def test_process_update(executor: Executor, package_ahriman: Package, mocker: Mo sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package", side_effect=lambda fn, _: [fn]) status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success") remove_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove") + packager_mock = mocker.patch("ahriman.core.repository.executor.Executor.packager", return_value=user) filepath = next(package.filepath for package in package_ahriman.packages.values()) # must return complete - assert executor.process_update([filepath]) + assert executor.process_update([filepath], Packagers("packager")) + packager_mock.assert_called_once_with(Packagers("packager"), "ahriman") # must move files (once) move_mock.assert_called_once_with(executor.paths.packages / filepath, executor.paths.repository / filepath) # must sign package - sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, package_ahriman.base) + sign_package_mock.assert_called_once_with(executor.paths.packages / filepath, user.key) # must add package repo_add_mock.assert_called_once_with(executor.paths.repository / filepath) # must update status diff --git a/tests/ahriman/core/repository/test_repository_properties.py b/tests/ahriman/core/repository/test_repository_properties.py index 552df736..cbc45f75 100644 --- a/tests/ahriman/core/repository/test_repository_properties.py +++ b/tests/ahriman/core/repository/test_repository_properties.py @@ -4,6 +4,10 @@ from ahriman.core.configuration import Configuration from ahriman.core.database import SQLite from ahriman.core.exceptions import UnsafeRunError from ahriman.core.repository.repository_properties import RepositoryProperties +from ahriman.models.packagers import Packagers +from ahriman.models.pacman_synchronization import PacmanSynchronization +from ahriman.models.user import User +from ahriman.models.user_access import UserAccess def test_create_tree_on_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None: @@ -12,7 +16,8 @@ def test_create_tree_on_load(configuration: Configuration, database: SQLite, moc """ mocker.patch("ahriman.core.repository.repository_properties.check_user") tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") - RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, refresh_pacman_database=0) + RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, + refresh_pacman_database=PacmanSynchronization.Disabled) tree_create_mock.assert_called_once_with() @@ -23,6 +28,36 @@ def test_create_tree_on_load_unsafe(configuration: Configuration, database: SQLi """ mocker.patch("ahriman.core.repository.repository_properties.check_user", side_effect=UnsafeRunError(0, 1)) tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") - RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, refresh_pacman_database=0) + RepositoryProperties("x86_64", configuration, database, report=False, unsafe=False, + refresh_pacman_database=PacmanSynchronization.Disabled) tree_create_mock.assert_not_called() + + +def test_packager(repository: RepositoryProperties, mocker: MockerFixture) -> None: + """ + must extract packager + """ + database_mock = mocker.patch("ahriman.core.database.SQLite.user_get") + assert repository.packager(Packagers("username", {}), "base") + database_mock.assert_called_once_with("username") + + +def test_packager_empty(repository: RepositoryProperties, mocker: MockerFixture) -> None: + """ + must return empty user if username was not set + """ + database_mock = mocker.patch("ahriman.core.database.SQLite.user_get") + user = User(username="", password="", access=UserAccess.Read, packager_id=None, key=None) + assert repository.packager(Packagers(), "base") == user + database_mock.assert_not_called() + + +def test_packager_empty_result(repository: RepositoryProperties, mocker: MockerFixture) -> None: + """ + must return empty user if it wasn't found in database + """ + database_mock = mocker.patch("ahriman.core.database.SQLite.user_get", return_value=None) + user = User(username="username", password="", access=UserAccess.Read, packager_id=None, key=None) + assert repository.packager(Packagers(user.username), "base") == user + database_mock.assert_called_once_with(user.username) diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index b1d8690e..d190242b 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -74,7 +74,7 @@ def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Pack package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman] - package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman) + package_load_mock.assert_called_once_with(package_ahriman.base, update_handler.pacman, None) def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Package, @@ -120,7 +120,7 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, assert update_handler.updates_local(vcs=True) == [package_ahriman] fetch_mock.assert_called_once_with(Path(package_ahriman.base), remote=None) - package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64") + package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64", None) status_client_mock.assert_called_once_with(package_ahriman.base) package_is_outdated_mock.assert_called_once_with( package_ahriman, update_handler.paths, diff --git a/tests/ahriman/core/sign/test_gpg.py b/tests/ahriman/core/sign/test_gpg.py index e5427ffd..16d1f0fd 100644 --- a/tests/ahriman/core/sign/test_gpg.py +++ b/tests/ahriman/core/sign/test_gpg.py @@ -135,21 +135,6 @@ def test_key_import(gpg: GPG, mocker: MockerFixture) -> None: check_output_mock.assert_called_once_with("gpg", "--import", input_data="key", logger=pytest.helpers.anyvar(int)) -def test_keys(gpg: GPG) -> None: - """ - must extract keys - """ - assert gpg.keys() == [] - - gpg.default_key = "key" - assert gpg.keys() == [gpg.default_key] - - gpg.configuration.set_option("sign", "key_a", "key1") - gpg.configuration.set_option("sign", "key_b", "key1") - gpg.configuration.set_option("sign", "key_c", "key2") - assert gpg.keys() == ["key", "key1", "key2"] - - def test_process(gpg_with_key: GPG, mocker: MockerFixture) -> None: """ must call process method correctly @@ -170,7 +155,7 @@ def test_process_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> Non gpg_with_key.targets = {SignSettings.Packages} assert gpg_with_key.process_sign_package(Path("a"), "a") == result - process_mock.assert_called_once_with(Path("a"), "key") + process_mock.assert_called_once_with(Path("a"), "a") def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None: @@ -182,7 +167,19 @@ def test_process_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> Non gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository} assert gpg_with_key.process_sign_package(Path("a"), "a") == result - process_mock.assert_called_once_with(Path("a"), "key") + process_mock.assert_called_once_with(Path("a"), "a") + + +def test_process_sign_package_3(gpg_with_key: GPG, mocker: MockerFixture) -> None: + """ + must sign package with default key if none passed + """ + result = [Path("a"), Path("a.sig")] + process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result) + + gpg_with_key.targets = {SignSettings.Packages} + assert gpg_with_key.process_sign_package(Path("a"), None) == result + process_mock.assert_called_once_with(Path("a"), gpg_with_key.default_key) def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None: @@ -211,7 +208,7 @@ def test_process_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None: """ process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") gpg.targets = {SignSettings.Packages} - gpg.process_sign_package(Path("a"), "a") + gpg.process_sign_package(Path("a"), None) process_mock.assert_not_called() @@ -221,7 +218,7 @@ def test_process_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None: """ process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") gpg.targets = {SignSettings.Packages, SignSettings.Repository} - gpg.process_sign_package(Path("a"), "a") + gpg.process_sign_package(Path("a"), None) process_mock.assert_not_called() diff --git a/tests/ahriman/core/support/pkgbuild/conftest.py b/tests/ahriman/core/support/pkgbuild/conftest.py index af061f1d..2851d5a2 100644 --- a/tests/ahriman/core/support/pkgbuild/conftest.py +++ b/tests/ahriman/core/support/pkgbuild/conftest.py @@ -1,24 +1,26 @@ import pytest from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite from ahriman.core.sign.gpg import GPG from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator @pytest.fixture -def keyring_generator(gpg: GPG, configuration: Configuration) -> KeyringGenerator: +def keyring_generator(database: SQLite, gpg: GPG, configuration: Configuration) -> KeyringGenerator: """ fixture for keyring pkgbuild generator Args: + database(SQLite): database fixture gpg(GPG): empty GPG fixture configuration(Configuration): configuration fixture Returns: KeyringGenerator: keyring generator test instance """ - return KeyringGenerator(gpg, configuration, "keyring") + return KeyringGenerator(database, gpg, configuration, "keyring") @pytest.fixture diff --git a/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py b/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py index 5ae8b70d..44ba059c 100644 --- a/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py +++ b/tests/ahriman/core/support/pkgbuild/test_keyring_generator.py @@ -5,84 +5,87 @@ from pytest_mock import MockerFixture from unittest.mock import MagicMock, call as MockCall from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite from ahriman.core.exceptions import PkgbuildGeneratorError from ahriman.core.sign.gpg import GPG from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator +from ahriman.models.user import User -def test_init_packagers(gpg: GPG, configuration: Configuration, mocker: MockerFixture) -> None: +def test_init_packagers(database: SQLite, gpg: GPG, configuration: Configuration, user: User, + mocker: MockerFixture) -> None: """ must extract packagers keys """ - mocker.patch("ahriman.core.sign.gpg.GPG.keys", return_value=["key"]) + mocker.patch("ahriman.core.database.SQLite.user_list", return_value=[user]) - assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key"] + assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key"] configuration.set_option("keyring", "packagers", "key1") - assert KeyringGenerator(gpg, configuration, "keyring").packagers == ["key1"] + assert KeyringGenerator(database, gpg, configuration, "keyring").packagers == ["key1"] -def test_init_revoked(gpg: GPG, configuration: Configuration) -> None: +def test_init_revoked(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must extract revoked keys """ - assert KeyringGenerator(gpg, configuration, "keyring").revoked == [] + assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == [] configuration.set_option("keyring", "revoked", "key1") - assert KeyringGenerator(gpg, configuration, "keyring").revoked == ["key1"] + assert KeyringGenerator(database, gpg, configuration, "keyring").revoked == ["key1"] -def test_init_trusted(gpg: GPG, configuration: Configuration) -> None: +def test_init_trusted(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must extract trusted keys """ - assert KeyringGenerator(gpg, configuration, "keyring").trusted == [] + assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == [] gpg.default_key = "key" - assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key"] + assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key"] configuration.set_option("keyring", "trusted", "key1") - assert KeyringGenerator(gpg, configuration, "keyring").trusted == ["key1"] + assert KeyringGenerator(database, gpg, configuration, "keyring").trusted == ["key1"] -def test_license(gpg: GPG, configuration: Configuration) -> None: +def test_license(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct licenses list """ - assert KeyringGenerator(gpg, configuration, "keyring").license == ["Unlicense"] + assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["Unlicense"] configuration.set_option("keyring", "license", "GPL MPL") - assert KeyringGenerator(gpg, configuration, "keyring").license == ["GPL", "MPL"] + assert KeyringGenerator(database, gpg, configuration, "keyring").license == ["GPL", "MPL"] -def test_pkgdesc(gpg: GPG, configuration: Configuration) -> None: +def test_pkgdesc(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct pkgdesc property """ - assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring" + assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "aur-clone PGP keyring" configuration.set_option("keyring", "description", "description") - assert KeyringGenerator(gpg, configuration, "keyring").pkgdesc == "description" + assert KeyringGenerator(database, gpg, configuration, "keyring").pkgdesc == "description" -def test_pkgname(gpg: GPG, configuration: Configuration) -> None: +def test_pkgname(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct pkgname property """ - assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "aur-clone-keyring" + assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "aur-clone-keyring" configuration.set_option("keyring", "package", "keyring") - assert KeyringGenerator(gpg, configuration, "keyring").pkgname == "keyring" + assert KeyringGenerator(database, gpg, configuration, "keyring").pkgname == "keyring" -def test_url(gpg: GPG, configuration: Configuration) -> None: +def test_url(database: SQLite, gpg: GPG, configuration: Configuration) -> None: """ must generate correct url property """ - assert KeyringGenerator(gpg, configuration, "keyring").url == "" + assert KeyringGenerator(database, gpg, configuration, "keyring").url == "" configuration.set_option("keyring", "homepage", "homepage") - assert KeyringGenerator(gpg, configuration, "keyring").url == "homepage" + assert KeyringGenerator(database, gpg, configuration, "keyring").url == "homepage" def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/support/test_keyring_trigger.py b/tests/ahriman/core/support/test_keyring_trigger.py index 8c82703f..cf171ef1 100644 --- a/tests/ahriman/core/support/test_keyring_trigger.py +++ b/tests/ahriman/core/support/test_keyring_trigger.py @@ -1,6 +1,8 @@ from pytest_mock import MockerFixture +from unittest.mock import call as MockCall from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite from ahriman.core.sign.gpg import GPG from ahriman.core.support import KeyringTrigger from ahriman.models.context_key import ContextKey @@ -21,10 +23,10 @@ def test_on_start(configuration: Configuration, mocker: MockerFixture) -> None: """ must run report for specified targets """ - gpg_mock = mocker.patch("ahriman.core._Context.get") + context_mock = mocker.patch("ahriman.core._Context.get") run_mock = mocker.patch("ahriman.core.support.package_creator.PackageCreator.run") trigger = KeyringTrigger("x86_64", configuration) trigger.on_start() - gpg_mock.assert_called_once_with(ContextKey("sign", GPG)) + context_mock.assert_has_calls([MockCall(ContextKey("sign", GPG)), MockCall(ContextKey("database", SQLite))]) run_mock.assert_called_once_with() diff --git a/tests/ahriman/core/support/test_package_creator.py b/tests/ahriman/core/support/test_package_creator.py index 616df485..6a472e94 100644 --- a/tests/ahriman/core/support/test_package_creator.py +++ b/tests/ahriman/core/support/test_package_creator.py @@ -35,6 +35,6 @@ def test_run(package_creator: PackageCreator, database: SQLite, mocker: MockerFi write_mock.assert_called_once_with(local_path) init_mock.assert_called_once_with(local_path) - package_mock.assert_called_once_with(local_path, "x86_64") + package_mock.assert_called_once_with(local_path, "x86_64", None) database_mock.assert_called_once_with(ContextKey("database", SQLite)) insert_mock.assert_called_once_with(package, pytest.helpers.anyvar(int)) diff --git a/tests/ahriman/core/test_spawn.py b/tests/ahriman/core/test_spawn.py index e4dcee04..625bc10c 100644 --- a/tests/ahriman/core/test_spawn.py +++ b/tests/ahriman/core/test_spawn.py @@ -42,7 +42,7 @@ def test_spawn_process(spawner: Spawn, mocker: MockerFixture) -> None: """ start_mock = mocker.patch("multiprocessing.Process.start") - spawner._spawn_process("add", "ahriman", now="", maybe="?") + spawner._spawn_process("add", "ahriman", now="", maybe="?", none=None) start_mock.assert_called_once_with() spawner.args_parser.parse_args.assert_called_once_with( spawner.command_arguments + [ @@ -74,8 +74,8 @@ def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None: must call package addition """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") - spawner.packages_add(["ahriman", "linux"], now=False) - spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur") + spawner.packages_add(["ahriman", "linux"], None, now=False) + spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None) def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None: @@ -83,8 +83,17 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None: must call package addition with update """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") - spawner.packages_add(["ahriman", "linux"], now=True) - spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", now="") + spawner.packages_add(["ahriman", "linux"], None, now=True) + spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None, now="") + + +def test_packages_add_with_username(spawner: Spawn, mocker: MockerFixture) -> None: + """ + must call package addition with username + """ + spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") + spawner.packages_add(["ahriman", "linux"], "username", now=False) + spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username="username") def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None: @@ -92,8 +101,8 @@ def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None: must call package rebuild """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") - spawner.packages_rebuild("python") - spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python"}) + spawner.packages_rebuild("python", "packager") + spawn_mock.assert_called_once_with("repo-rebuild", **{"depends-on": "python", "username": "packager"}) def test_packages_remove(spawner: Spawn, mocker: MockerFixture) -> None: @@ -110,8 +119,8 @@ def test_packages_update(spawner: Spawn, mocker: MockerFixture) -> None: must call repo update """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") - spawner.packages_update() - spawn_mock.assert_called_once_with("repo-update") + spawner.packages_update("packager") + spawn_mock.assert_called_once_with("repo-update", username="packager") def test_run(spawner: Spawn, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index a53a3d0c..86c08d88 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -11,9 +11,9 @@ from typing import Any from unittest.mock import MagicMock from ahriman.core.exceptions import BuildError, OptionError, UnsafeRunError -from ahriman.core.util import check_output, check_user, enum_values, exception_response_text, filter_json, \ - full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, srcinfo_property, \ - srcinfo_property_list, trim_package, utcnow, walk +from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, exception_response_text,\ + extract_user, filter_json, full_version, package_like, partition, pretty_datetime, pretty_size, safe_filename, \ + srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk from ahriman.models.package import Package from ahriman.models.package_source import PackageSource from ahriman.models.repository_paths import RepositoryPaths @@ -91,6 +91,16 @@ def test_check_output_with_user(passwd: Any, mocker: MockerFixture) -> None: getpwuid_mock.assert_called_once_with(user) +def test_check_output_with_user_and_environment(passwd: Any, mocker: MockerFixture) -> None: + """ + must run set environment if both environment and user are set + """ + mocker.patch("ahriman.core.util.getpwuid", return_value=passwd) + user = os.getuid() + assert check_output("python", "-c", """import os; print(os.getenv("HOME"), os.getenv("VAR"))""", + environment={"VAR": "VALUE"}, user=user) == f"{passwd.pw_dir} VALUE" + + def test_check_output_failure(mocker: MockerFixture) -> None: """ must process exception correctly @@ -155,6 +165,23 @@ def test_check_user_unsafe(mocker: MockerFixture) -> None: check_user(paths, unsafe=True) +def test_dataclass_view(package_ahriman: Package) -> None: + """ + must serialize dataclasses + """ + assert Package.from_json(dataclass_view(package_ahriman)) == package_ahriman + + +def test_dataclass_view_without_none(package_ahriman: Package) -> None: + """ + must serialize dataclasses with None fields removed + """ + package_ahriman.packager = None + result = dataclass_view(package_ahriman) + assert "packager" not in result + assert Package.from_json(result) == package_ahriman + + def test_exception_response_text() -> None: """ must parse HTTP response to string @@ -174,6 +201,23 @@ def test_exception_response_text_empty() -> None: assert exception_response_text(exception) == "" +def test_extract_user() -> None: + """ + must extract user from system environment + """ + os.environ["USER"] = "user" + assert extract_user() == "user" + + os.environ["SUDO_USER"] = "sudo" + assert extract_user() == "sudo" + + os.environ["DOAS_USER"] = "doas" + assert extract_user() == "sudo" + + del os.environ["SUDO_USER"] + assert extract_user() == "doas" + + def test_filter_json(package_ahriman: Package) -> None: """ must filter fields by known list diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index 15c8bf8b..239d1460 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -116,6 +116,7 @@ def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock: type(mock).name = PropertyMock(return_value=aur_package_ahriman.name) type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends) type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends) + type(mock).packager = PropertyMock(return_value="packager") type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides) type(mock).version = PropertyMock(return_value=aur_package_ahriman.version) type(mock).url = PropertyMock(return_value=aur_package_ahriman.url) diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index 633cbe2e..30477f46 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -56,7 +56,7 @@ def test_depends_build_with_version_and_overlap(mocker: MockerFixture, resource_ srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) - package_gcc10 = Package.from_build(Path("local"), "x86_64") + package_gcc10 = Package.from_build(Path("local"), "x86_64", None) assert package_gcc10.depends_build == { "glibc", "zstd", # depends "doxygen", "binutils", "git", "libmpc", "python", # make depends @@ -168,10 +168,11 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, pac """ mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman) - package = Package.from_aur(package_ahriman.base, pacman) + package = Package.from_aur(package_ahriman.base, pacman, package_ahriman.packager) assert package_ahriman.base == package.base assert package_ahriman.version == package.version assert package_ahriman.packages.keys() == package.packages.keys() + assert package_ahriman.packager == package.packager def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None: @@ -181,7 +182,7 @@ def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_pa srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text() mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) - package = Package.from_build(Path("path"), "x86_64") + package = Package.from_build(Path("path"), "x86_64", "packager") assert package_ahriman.packages.keys() == package.packages.keys() package_ahriman.packages = package.packages # we are not going to test PackageDescription here package_ahriman.remote = package.remote @@ -195,7 +196,7 @@ def test_from_build_multiple_packages(mocker: MockerFixture, resource_path_root: srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text() mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) - package = Package.from_build(Path("path"), "x86_64") + package = Package.from_build(Path("path"), "x86_64", None) assert package.packages == { "gcc10": PackageDescription( depends=["gcc10-libs=10.3.0-2", "binutils>=2.28", "libmpc", "zstd"], @@ -225,7 +226,7 @@ def test_from_build_architecture(mocker: MockerFixture, resource_path_root: Path srcinfo = (resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo").read_text() mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) - package = Package.from_build(Path("path"), "x86_64") + package = Package.from_build(Path("path"), "x86_64", None) assert package.packages == { "jellyfin-ffmpeg5-bin": PackageDescription( depends=["glibc"], @@ -254,7 +255,7 @@ def test_from_build_failed(package_ahriman: Package, mocker: MockerFixture) -> N mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) with pytest.raises(PackageInfoError): - Package.from_build(Path("path"), "x86_64") + Package.from_build(Path("path"), "x86_64", None) def test_from_json_view_1(package_ahriman: Package) -> None: @@ -285,10 +286,11 @@ def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage """ mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman) - package = Package.from_official(package_ahriman.base, pacman) + package = Package.from_official(package_ahriman.base, pacman, package_ahriman.packager) assert package_ahriman.base == package.base assert package_ahriman.version == package.version assert package_ahriman.packages.keys() == package.packages.keys() + assert package_ahriman.packager == package.packager def test_local_files(mocker: MockerFixture, resource_path_root: Path) -> None: diff --git a/tests/ahriman/models/test_packagers.py b/tests/ahriman/models/test_packagers.py new file mode 100644 index 00000000..9e581836 --- /dev/null +++ b/tests/ahriman/models/test_packagers.py @@ -0,0 +1,12 @@ +from ahriman.models.package import Package +from ahriman.models.packagers import Packagers + + +def test_for_base(package_ahriman: Package) -> None: + """ + must return username used for base package + """ + assert Packagers(None, {package_ahriman.base: "packager"}).for_base(package_ahriman.base) == "packager" + assert Packagers("default", {package_ahriman.base: "packager"}).for_base("random") == "default" + assert Packagers("default").for_base(package_ahriman.base) == "default" + assert Packagers().for_base(package_ahriman.base) is None diff --git a/tests/ahriman/models/test_user.py b/tests/ahriman/models/test_user.py index 046b2949..43015829 100644 --- a/tests/ahriman/models/test_user.py +++ b/tests/ahriman/models/test_user.py @@ -8,7 +8,7 @@ def test_from_option(user: User) -> None: """ must generate user from options """ - user = replace(user, access=UserAccess.Read) + user = replace(user, access=UserAccess.Read, packager_id=None, key=None) assert User.from_option(user.username, user.password) == user # default is read access user = replace(user, access=UserAccess.Full) 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 7ebec648..5f1aed71 100644 --- a/tests/ahriman/web/views/service/test_views_service_add.py +++ b/tests/ahriman/web/views/service/test_views_service_add.py @@ -2,6 +2,7 @@ import pytest from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture +from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess from ahriman.web.views.service.add import AddView @@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None: must call post request correctly """ add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add") + user_mock = AsyncMock() + user_mock.return_value = "username" + mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock) request_schema = pytest.helpers.schema_request(AddView.post) payload = {"packages": ["ahriman"]} assert not request_schema.validate(payload) response = await client.post("/api/v1/service/add", json=payload) assert response.ok - add_mock.assert_called_once_with(["ahriman"], now=True) + add_mock.assert_called_once_with(["ahriman"], "username", now=True) async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_rebuild.py b/tests/ahriman/web/views/service/test_views_service_rebuild.py index 698a8766..00bb9705 100644 --- a/tests/ahriman/web/views/service/test_views_service_rebuild.py +++ b/tests/ahriman/web/views/service/test_views_service_rebuild.py @@ -2,6 +2,7 @@ import pytest from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture +from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess from ahriman.web.views.service.rebuild import RebuildView @@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None: must call post request correctly """ rebuild_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rebuild") + user_mock = AsyncMock() + user_mock.return_value = "username" + mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock) request_schema = pytest.helpers.schema_request(RebuildView.post) payload = {"packages": ["python", "ahriman"]} assert not request_schema.validate(payload) response = await client.post("/api/v1/service/rebuild", json=payload) assert response.ok - rebuild_mock.assert_called_once_with("python") + rebuild_mock.assert_called_once_with("python", "username") async def test_post_exception(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 12f3d30b..6e6f8b62 100644 --- a/tests/ahriman/web/views/service/test_views_service_request.py +++ b/tests/ahriman/web/views/service/test_views_service_request.py @@ -2,6 +2,7 @@ import pytest from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture +from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess from ahriman.web.views.service.request import RequestView @@ -21,13 +22,16 @@ async def test_post(client: TestClient, mocker: MockerFixture) -> None: must call post request correctly """ add_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_add") + user_mock = AsyncMock() + user_mock.return_value = "username" + mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock) request_schema = pytest.helpers.schema_request(RequestView.post) payload = {"packages": ["ahriman"]} assert not request_schema.validate(payload) response = await client.post("/api/v1/service/request", json=payload) assert response.ok - add_mock.assert_called_once_with(["ahriman"], now=False) + add_mock.assert_called_once_with(["ahriman"], "username", now=False) async def test_post_exception(client: TestClient, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_update.py b/tests/ahriman/web/views/service/test_views_service_update.py index 3f588df6..3ae66350 100644 --- a/tests/ahriman/web/views/service/test_views_service_update.py +++ b/tests/ahriman/web/views/service/test_views_service_update.py @@ -1,5 +1,6 @@ from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture +from unittest.mock import AsyncMock async def test_post_update(client: TestClient, mocker: MockerFixture) -> None: @@ -7,7 +8,10 @@ async def test_post_update(client: TestClient, mocker: MockerFixture) -> None: must call post request correctly for alias """ update_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_update") + user_mock = AsyncMock() + user_mock.return_value = "username" + mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock) response = await client.post("/api/v1/service/update") assert response.ok - update_mock.assert_called_once_with() + update_mock.assert_called_once_with("username") diff --git a/tests/ahriman/web/views/test_views_base.py b/tests/ahriman/web/views/test_views_base.py index 7d1c0643..b670e346 100644 --- a/tests/ahriman/web/views/test_views_base.py +++ b/tests/ahriman/web/views/test_views_base.py @@ -2,6 +2,8 @@ import pytest from multidict import MultiDict from aiohttp.test_utils import TestClient +from pytest_mock import MockerFixture +from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess from ahriman.web.views.base import BaseView @@ -146,3 +148,22 @@ async def test_head_not_allowed(client: TestClient) -> None: """ response = await client.head("/api/v1/service/add") assert response.status == 405 + + +async def test_username(base: BaseView, mocker: MockerFixture) -> None: + """ + must return identity of logged-in user + """ + policy = AsyncMock() + policy.identify.return_value = "identity" + mocker.patch("aiohttp.web.Application.get", return_value=policy) + + assert await base.username() == "identity" + policy.identify.assert_called_once_with(base.request) + + +async def test_username_no_auth(base: BaseView) -> None: + """ + must return None in case if auth is disabled + """ + assert await base.username() is None