generate-version.sh | 3 +- meson.build | 5 + meson_options.txt | 1 + po/cs.po | 53 ++- po/ru.po | 53 ++- src/daemon.c | 41 ++- src/libaccountsservice/act-user-manager.c | 14 +- src/user-classify.c | 2 + subprojects/mocklibc.wrap | 2 + .../packagefiles/mocklibc-print-indent.diff | 13 + tests/dbusmock/accounts_service.py | 393 --------------------- tests/mock_services/accounts_service.py | 393 +++++++++++++++++++++ tests/test-libaccountsservice.py | 4 +- 13 files changed, 531 insertions(+), 446 deletions(-) diff --git a/generate-version.sh b/generate-version.sh index 3f88bff..8375c86 100755 --- a/generate-version.sh +++ b/generate-version.sh @@ -4,11 +4,10 @@ exec 3>&2 2> /dev/null SRCDIR=$(dirname "$0") cd "$SRCDIR" CWD=$(realpath "$PWD") -TOPLEVEL_WORKING_DIR=$(realpath "$(git rev-parse --show-toplevel)") exec 2>&3 # If it's not from a git checkout, assume it's from a tarball -if [ "$TOPLEVEL_WORKING_DIR" != "$CWD" ]; then +if ! git rev-parse --is-inside-git-dir > /dev/null 2>&1; then VERSION_FROM_DIR_NAME=$(basename "$CWD" | sed -n 's/^accountsservice-\([^-]*\)$/\1/p') if [ -n "$VERSION_FROM_DIR_NAME" ]; then diff --git a/meson.build b/meson.build index 4a509e7..04455f6 100644 --- a/meson.build +++ b/meson.build @@ -198,6 +198,10 @@ if admin_group == '' endif endif +default_user_groups = ','.join(get_option('default_user_groups')) + +config_h.set_quoted('DEFAULT_USER_GROUPS', default_user_groups) + extra_admin_groups = ','.join(get_option('extra_admin_groups')) config_h.set_quoted('ADMIN_GROUP', admin_group) @@ -247,6 +251,7 @@ meson.add_install_script( output = '\n' + meson.project_name() + ' was configured with the following options:\n' output += '** DocBook documentation build: ' + enable_docbook.to_string() + '\n' output += '** Administrator group: ' + admin_group + '\n' +output += '** Default user groups: ' + default_user_groups + '\n' output += '** Extra administrator groups: ' + extra_admin_groups + '\n' output += '** GDM configuration: ' + gdm_conf_file + '\n' output += '** LightDM configuration: ' + lightdm_conf_file diff --git a/meson_options.txt b/meson_options.txt index b34a0fa..47cc9c9 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,6 +4,7 @@ option('lightdmconffile', type: 'string', value: '/etc/lightdm/lightdm.conf', de option('admin_group', type: 'string', value: '', description: 'Set group for administrative accounts') option('extra_admin_groups', type: 'array', value: [], description: 'Comma-separated list of extra groups that administrator users are part of') +option('default_user_groups', type: 'array', value: [], description: 'Comma-separated list of groups that all users are part of by default') option('minimum_uid', type: 'integer', value: 1000, description: 'Set minimum uid for human users') option('elogind', type: 'boolean', value: false, description: 'Use elogind') diff --git a/po/cs.po b/po/cs.po index 7cdc2b4..c948c99 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,62 +1,77 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # Marek Černocký , 2011 # Ondrej Novak , 2011 +# Daniel Rusek , 2023 msgid "" msgstr "" "Project-Id-Version: accounts service\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-01-17 09:48-0500\n" -"PO-Revision-Date: 2019-02-22 14:19+0000\n" -"Last-Translator: halfline \n" -"Language-Team: Czech (http://www.transifex.com/freedesktop/accountsservice/language/cs/)\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/accountsservice/" +"accountsservice/issues\n" +"POT-Creation-Date: 2022-07-31 12:33+0000\n" +"PO-Revision-Date: 2023-05-31 15:16+0200\n" +"Last-Translator: Daniel Rusek \n" +"Language-Team: Czech (http://www.transifex.com/freedesktop/accountsservice/" +"language/cs/)\n" +"Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: cs\n" -"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n " +"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" +"X-Generator: Poedit 3.3.1\n" -#: ../data/org.freedesktop.accounts.policy.in.h:1 +#: data/org.freedesktop.accounts.policy.in:11 msgid "Change your own user data" msgstr "Změnit své vlastní údaje" -#: ../data/org.freedesktop.accounts.policy.in.h:2 +#: data/org.freedesktop.accounts.policy.in:12 msgid "Authentication is required to change your own user data" msgstr "Pro změnu svých vlastních údajů je vyžadována autentizace" -#: ../data/org.freedesktop.accounts.policy.in.h:3 +#: data/org.freedesktop.accounts.policy.in:21 +msgid "Change your own user password" +msgstr "Změnit své vlastní heslo" + +#: data/org.freedesktop.accounts.policy.in:22 +msgid "Authentication is required to change your own user password" +msgstr "Pro změnu svého vlastního hesla je vyžadována autentizace" + +#: data/org.freedesktop.accounts.policy.in:31 msgid "Manage user accounts" msgstr "Spravovat uživatelské účty" -#: ../data/org.freedesktop.accounts.policy.in.h:4 +#: data/org.freedesktop.accounts.policy.in:32 msgid "Authentication is required to change user data" msgstr "Pro změnu údajů o uživateli je vyžadována autentizace" -#: ../data/org.freedesktop.accounts.policy.in.h:5 +#: data/org.freedesktop.accounts.policy.in:41 msgid "Change the login screen configuration" msgstr "Změnit nastavení přihlašovací obrazovky" -#: ../data/org.freedesktop.accounts.policy.in.h:6 +#: data/org.freedesktop.accounts.policy.in:42 msgid "Authentication is required to change the login screen configuration" msgstr "Pro změnu nastavení přihlašovací obrazovky je vyžadována autentizace" -#: ../src/main.c:127 +#: src/main.c:225 msgid "Output version information and exit" msgstr "Zobrazit informace o verzi a ukončit program" -#: ../src/main.c:128 +#: src/main.c:226 msgid "Replace existing instance" msgstr "Nahradit současnou instanci" -#: ../src/main.c:129 +#: src/main.c:227 msgid "Enable debugging code" msgstr "Povolit ladicí kód" -#: ../src/main.c:149 +#: src/main.c:246 msgid "" "Provides D-Bus interfaces for querying and manipulating\n" "user account information." -msgstr "Poskytuje rozhraní D-Bus pro zobrazování a změny\ninformací o uživatelských účtech" +msgstr "" +"Poskytuje rozhraní D-Bus pro zobrazování a změny\n" +"informací o uživatelských účtech" diff --git a/po/ru.po b/po/ru.po index 58d173a..5040794 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,61 +1,76 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # Yuri Kozlov , 2011 msgid "" msgstr "" "Project-Id-Version: accounts service\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-01-17 09:48-0500\n" -"PO-Revision-Date: 2019-02-22 14:19+0000\n" -"Last-Translator: halfline \n" -"Language-Team: Russian (http://www.transifex.com/freedesktop/accountsservice/language/ru/)\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/accountsservice/" +"accountsservice/issues\n" +"POT-Creation-Date: 2022-07-31 12:33+0000\n" +"PO-Revision-Date: 2023-04-08 17:12+0300\n" +"Last-Translator: Aleksandr Melman \n" +"Language-Team: Russian (http://www.transifex.com/freedesktop/accountsservice/" +"language/ru/)\n" +"Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " +"(n%100>=11 && n%100<=14)? 2 : 3);\n" +"X-Generator: Poedit 3.2.2\n" -#: ../data/org.freedesktop.accounts.policy.in.h:1 +#: data/org.freedesktop.accounts.policy.in:11 msgid "Change your own user data" msgstr "Изменить личные пользовательские данные" -#: ../data/org.freedesktop.accounts.policy.in.h:2 +#: data/org.freedesktop.accounts.policy.in:12 msgid "Authentication is required to change your own user data" msgstr "Для изменения личных пользовательских данных требуется аутентификация" -#: ../data/org.freedesktop.accounts.policy.in.h:3 +#: data/org.freedesktop.accounts.policy.in:21 +msgid "Change your own user password" +msgstr "Изменить личный пароль пользователя" + +#: data/org.freedesktop.accounts.policy.in:22 +msgid "Authentication is required to change your own user password" +msgstr "Для изменения личного пароля пользователя требуется аутентификация" + +#: data/org.freedesktop.accounts.policy.in:31 msgid "Manage user accounts" msgstr "Управление учётными записями пользователей" -#: ../data/org.freedesktop.accounts.policy.in.h:4 +#: data/org.freedesktop.accounts.policy.in:32 msgid "Authentication is required to change user data" msgstr "Для изменения пользовательских данных требуется аутентификация" -#: ../data/org.freedesktop.accounts.policy.in.h:5 +#: data/org.freedesktop.accounts.policy.in:41 msgid "Change the login screen configuration" msgstr "Изменить настройки экрана входа в систему" -#: ../data/org.freedesktop.accounts.policy.in.h:6 +#: data/org.freedesktop.accounts.policy.in:42 msgid "Authentication is required to change the login screen configuration" msgstr "Для изменения настроек экрана входа в систему требуется аутентификация" -#: ../src/main.c:127 +#: src/main.c:225 msgid "Output version information and exit" msgstr "Вывод информации о версии и выход из программы" -#: ../src/main.c:128 +#: src/main.c:226 msgid "Replace existing instance" msgstr "Заменить существующий экземпляр" -#: ../src/main.c:129 +#: src/main.c:227 msgid "Enable debugging code" msgstr "Включить отладочный код" -#: ../src/main.c:149 +#: src/main.c:246 msgid "" "Provides D-Bus interfaces for querying and manipulating\n" "user account information." -msgstr "Предоставляет интерфейс D-Bus для опроса и изменения\nинформации об учётных данных пользователей." +msgstr "" +"Предоставляет интерфейс D-Bus для опроса и изменения\n" +"информации об учётных данных пользователей." diff --git a/src/daemon.c b/src/daemon.c index aa9d050..6e36795 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -1311,6 +1311,7 @@ daemon_create_user_authorized_cb (Daemon *daemon, g_autoptr (GError) error = NULL; const gchar *argv[9]; g_autofree gchar *admin_groups = NULL; + g_autofree gchar *user_groups = NULL; if (getpwnam (cd->user_name) != NULL) { throw_error (context, ERROR_USER_EXISTS, "A user with name '%s' already exists", cd->user_name); @@ -1323,11 +1324,32 @@ daemon_create_user_authorized_cb (Daemon *daemon, argv[1] = "-m"; argv[2] = "-c"; argv[3] = cd->real_name; + + /* Build default user groups */ + g_auto (GStrv) user_groups_array = NULL; + g_autoptr (GStrvBuilder) user_groups_builder = g_strv_builder_new (); + + if (DEFAULT_USER_GROUPS != NULL && DEFAULT_USER_GROUPS[0] != '\0') { + g_auto (GStrv) default_user_groups = NULL; + default_user_groups = g_strsplit (DEFAULT_USER_GROUPS, ",", 0); + + for (gsize i = 0; default_user_groups[i] != NULL; i++) { + if (getgrnam (default_user_groups[i]) != NULL) + g_strv_builder_add (user_groups_builder, default_user_groups[i]); + else + g_warning ("Group %s doesn’t exist: not adding the user to it", default_user_groups[i]); + } + } + user_groups_array = g_strv_builder_end (user_groups_builder); + user_groups = g_strjoinv (",", user_groups_array); + if (cd->account_type == ACCOUNT_TYPE_ADMINISTRATOR) { g_auto (GStrv) admin_groups_array = NULL; g_autoptr (GStrvBuilder) admin_groups_builder = g_strv_builder_new (); g_strv_builder_add (admin_groups_builder, ADMIN_GROUP); + /* Add default user groups to admin groups */ + g_strv_builder_add (admin_groups_builder, user_groups); if (EXTRA_ADMIN_GROUPS != NULL && EXTRA_ADMIN_GROUPS[0] != '\0') { g_auto (GStrv) extra_admin_groups = NULL; @@ -1340,6 +1362,7 @@ daemon_create_user_authorized_cb (Daemon *daemon, g_warning ("Extra admin group %s doesn’t exist: not adding the user to it", extra_admin_groups[i]); } } + admin_groups_array = g_strv_builder_end (admin_groups_builder); admin_groups = g_strjoinv (",", admin_groups_array); @@ -1349,9 +1372,11 @@ daemon_create_user_authorized_cb (Daemon *daemon, argv[7] = cd->user_name; argv[8] = NULL; } else if (cd->account_type == ACCOUNT_TYPE_STANDARD) { - argv[4] = "--"; - argv[5] = cd->user_name; - argv[6] = NULL; + argv[4] = "-G"; + argv[5] = user_groups; + argv[6] = "--"; + argv[7] = cd->user_name; + argv[8] = NULL; } else { throw_error (context, ERROR_FAILED, "Don't know how to add user of type %d", cd->account_type); return; @@ -1803,9 +1828,8 @@ load_autologin (Daemon *daemon, else if (dm_type == DISPLAY_MANAGER_TYPE_GDM) return load_autologin_gdm (daemon, name, enabled, error); - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _ ("Unsupported Display Manager")); - - return FALSE; + /* Default to GDM for backward compatibility */ + return load_autologin_gdm (daemon, name, enabled, error); } static gboolean @@ -1824,7 +1848,7 @@ save_autologin_gdm (Daemon *daemon, PATH_GDM_CUSTOM, G_KEY_FILE_KEEP_COMMENTS, &local_error)) { - /* It's OK for custom.conf to not exist, we will make it */ + /* It's OK if the GDM config file doesn't exist, since we will make it, if necessary */ if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; @@ -1885,7 +1909,8 @@ save_autologin (Daemon *daemon, else if (dm_type == DISPLAY_MANAGER_TYPE_GDM) return save_autologin_gdm (daemon, name, enabled, error); - return FALSE; + /* Default to GDM for backward compatibility */ + return save_autologin_gdm (daemon, name, enabled, error); } gboolean diff --git a/src/libaccountsservice/act-user-manager.c b/src/libaccountsservice/act-user-manager.c index 61b4da5..7afe713 100644 --- a/src/libaccountsservice/act-user-manager.c +++ b/src/libaccountsservice/act-user-manager.c @@ -153,6 +153,7 @@ typedef struct }; char *object_path; char *description; + gulong ready_id; } ActUserManagerFetchUserRequest; typedef struct @@ -717,6 +718,8 @@ on_user_destroyed (ActUserManager *manager, ActUserManagerPrivate *priv = act_user_manager_get_instance_private (manager); GSList *node; + priv->new_users = g_slist_remove (priv->new_users, destroyed_user); + node = priv->fetch_user_requests; while (node != NULL) { ActUserManagerFetchUserRequest *request; @@ -1754,6 +1757,7 @@ load_new_session_incrementally (ActUserManagerNewSession *new_session) break; case ACT_USER_MANAGER_NEW_SESSION_STATE_LOADED: break; + case ACT_USER_MANAGER_NEW_SESSION_STATE_UNLOADED: default: g_assert_not_reached (); } @@ -1781,6 +1785,7 @@ free_fetch_user_request (ActUserManagerFetchUserRequest *request) g_cancellable_cancel (request->cancellable); g_object_unref (request->cancellable); + g_clear_signal_handler (&request->ready_id, manager); g_debug ("ActUserManager: unrefing manager owned by fetch user request"); g_object_unref (manager); @@ -1823,7 +1828,7 @@ on_user_manager_maybe_ready_for_request (ActUserManager *manager g_debug ("ActUserManager: user manager now loaded, proceeding with fetch user request for %s", request->description); - g_signal_handlers_disconnect_by_func (manager, on_user_manager_maybe_ready_for_request, request); + g_clear_signal_handler (&request->ready_id, manager); request->state++; fetch_user_incrementally (request); @@ -1845,8 +1850,10 @@ fetch_user_incrementally (ActUserManagerFetchUserRequest *request) } else { g_debug ("ActUserManager: waiting for user manager to load before finding %s", request->description); - g_signal_connect (manager, "notify::is-loaded", - G_CALLBACK (on_user_manager_maybe_ready_for_request), request); + g_assert (request->ready_id == 0); + request->ready_id = + g_signal_connect (manager, "notify::is-loaded", + G_CALLBACK (on_user_manager_maybe_ready_for_request), request); } break; @@ -2270,6 +2277,7 @@ load_seat_incrementally (ActUserManager *manager) case ACT_USER_MANAGER_SEAT_STATE_LOADED: g_debug ("ActUserManager: Seat loading sequence complete"); break; + case ACT_USER_MANAGER_NEW_SESSION_STATE_UNLOADED: default: g_assert_not_reached (); } diff --git a/src/user-classify.c b/src/user-classify.c index 2a38a77..ef8b864 100644 --- a/src/user-classify.c +++ b/src/user-classify.c @@ -108,6 +108,8 @@ is_invalid_shell (const char *shell) return TRUE; } else if (g_strcmp0 (basename, "false") == 0) { return TRUE; + } else if (g_strcmp0 (basename, "null") == 0) { + return TRUE; } return ret; diff --git a/subprojects/mocklibc.wrap b/subprojects/mocklibc.wrap index af82298..539ee83 100644 --- a/subprojects/mocklibc.wrap +++ b/subprojects/mocklibc.wrap @@ -8,3 +8,5 @@ source_hash = b2236a6af1028414783e9734a46ea051916ec226479d6a55a3bb823bff68f120 patch_url = https://wrapdb.mesonbuild.com/v1/projects/mocklibc/1.0/2/get_zip patch_filename = mocklibc-1.0-2-wrap.zip patch_hash = 0280f96a2eeb3c023e5acf4e00cef03d362868218d4a85347ea45137c0ef6c56 + +diff_files = mocklibc-print-indent.diff diff --git a/subprojects/packagefiles/mocklibc-print-indent.diff b/subprojects/packagefiles/mocklibc-print-indent.diff new file mode 100644 index 0000000..4aaed40 --- /dev/null +++ b/subprojects/packagefiles/mocklibc-print-indent.diff @@ -0,0 +1,13 @@ +diff -up mocklibc-1.0/src/netgroup-debug.c.print-indent mocklibc-1.0/src/netgroup-debug.c +--- mocklibc-1.0/src/netgroup-debug.c.print-indent 2023-04-11 10:20:53.717381559 -0400 ++++ mocklibc-1.0/src/netgroup-debug.c 2023-04-11 10:21:02.296270333 -0400 +@@ -21,6 +21,9 @@ + #include + #include + ++void print_indent (FILE *stream, ++ unsigned int indent); ++ + void netgroup_debug_print_entry(struct entry *entry, FILE *stream, unsigned int indent) { + print_indent(stream, indent); + diff --git a/tests/dbusmock/accounts_service.py b/tests/dbusmock/accounts_service.py deleted file mode 100644 index 6f36efe..0000000 --- a/tests/dbusmock/accounts_service.py +++ /dev/null @@ -1,393 +0,0 @@ -'''Accounts Service D-Bus mock template''' - -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 3 of the License, or (at your option) any -# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text -# of the license. - -__author__ = 'Marco Trevisan' -__email__ = 'marco.trevisan@canonical.com' -__copyright__ = '(c) 2021 Canonical Ltd.' -__license__ = 'LGPL 3+' - -import sys -import time - -import dbus -from dbusmock import MOCK_IFACE, mockobject - -BUS_NAME = 'org.freedesktop.Accounts' -MAIN_OBJ = '/org/freedesktop/Accounts' -MAIN_IFACE = 'org.freedesktop.Accounts' -USER_IFACE = MAIN_IFACE + '.User' -SYSTEM_BUS = True - -DEFAULT_USER_PASSWORD = 'Pa$$wo0rd' - - -def get_user_path(uid): - return '/org/freedesktop/Accounts/User{}'.format(uid) - - -def load(mock, parameters=None): - parameters = parameters if parameters else {} - mock.mock_users = {} - mock.cached_users = [] - mock.automatic_login_users = set() - mock.users_auto_uids = 2000 - - mock.AddProperties(MAIN_IFACE, mock.GetAll(MAIN_IFACE)) - - for uid, name in parameters.get('users', {}).items(): - mock.AddUser(uid, name, DEFAULT_USER_PASSWORD, {}, {}) - -def emit_properties_changed(mock, interface=MAIN_IFACE, properties=None): - if properties is None: - properties = mock.GetAll(interface) - elif isinstance(properties, str): - properties = [properties] - - if isinstance(properties, (list, set)): - properties = {p: mock.Get(interface, p) for p in properties} - elif not isinstance(properties, dict): - raise TypeError('Unsupported properties type') - - mock.EmitSignal(dbus.PROPERTIES_IFACE, 'PropertiesChanged', 'sa{sv}as', ( - interface, properties, [])) - - -@dbus.service.method(MOCK_IFACE, in_signature='xssa{sv}a{sv}', - out_signature='o') -def AddUser(self, uid, username, password=DEFAULT_USER_PASSWORD, - overrides=None, password_policy_overrides=None): - '''Add user via uid and username and optionally overriding properties - - Returns the new object path. - ''' - path = get_user_path(uid) - default_props = { - 'Uid': dbus.UInt64(uid), - 'UserName': username, - 'RealName': username[0].upper() + username[1:] + ' Fake', - 'AccountType': dbus.Int32(1), - 'AutomaticLogin': False, - 'BackgroundFile': '', - 'Email': '{}@python-dbusmock.org'.format(username), - 'FormatsLocale': 'C', - 'HomeDirectory': '/nonexisting/mock-home/{}'.format(username), - 'IconFile': '', - 'InputSources': dbus.Array([], signature='a{ss}'), - 'Language': 'C', - 'Languages': dbus.Array([], signature='s'), - 'LocalAccount': True, - 'Location': '', - 'Locked': False, - 'LoginFrequency': dbus.UInt64(0), - 'LoginHistory': dbus.Array([], signature='(xxa{sv})'), - 'LoginTime': dbus.Int64(0), - 'PasswordHint': 'Remember it, come on!', - 'PasswordMode': 0, - 'Session': 'mock-session', - 'SessionType': 'wayland', - 'Shell': '/usr/bin/zsh', - 'SystemAccount': False, - 'XHasMessages': False, - 'XKeyboardLayouts': dbus.Array([], signature='s'), - 'XSession': 'mock-xsession', - } - default_props.update(overrides if overrides else {}) - self.AddObject(path, USER_IFACE, default_props, []) - - had_users = len(self.mock_users) != 0 - had_multiple_users = len(self.mock_users) > 1 - user = mockobject.objects[path] - user.password = password - user.properties = default_props - user.pwd_expiration_policy = { - 'expiration_time': sys.maxsize, - 'last_change_time': int(time.time()), - 'min_days_between_changes': 0, - 'max_days_between_changes': 0, - 'days_to_warn': 0, - 'days_after_expiration_until_lock': 0, - } - user.pwd_expiration_policy.update( - password_policy_overrides if password_policy_overrides else {}) - self.mock_users[uid] = default_props - - self.EmitSignal(MAIN_IFACE, 'UserAdded', 'o', [path]) - - if not had_users: - emit_properties_changed(self, MAIN_IFACE, 'HasNoUsers') - elif not had_multiple_users and len(self.mock_users) > 1: - emit_properties_changed(self, MAIN_IFACE, 'HasMultipleUsers') - - return path - - -@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='ao') -def ListMockUsers(self): - """ List the mock users that have been created """ - return [get_user_path(uid) for uid in self.mock_users.keys()] - - -@dbus.service.method(MOCK_IFACE, in_signature='s', out_signature='o') -def AddAutoLoginUser(self, username): - """ Enable autologin for an user """ - path = self.FindUserByName(username) - self.automatic_login_users.add(path) - user = mockobject.objects[path] - set_user_property(user, 'AutomaticLogin', True) - emit_properties_changed(self, MAIN_IFACE, 'AutomaticLoginUsers') - return path - - -@dbus.service.method(MOCK_IFACE, in_signature='s', out_signature='o') -def RemoveAutoLoginUser(self, username): - """ Disables autologin for an user """ - path = self.FindUserByName(username) - self.automatic_login_users.remove(path) - user = mockobject.objects[path] - set_user_property(user, 'AutomaticLogin', False) - emit_properties_changed(self, MAIN_IFACE, 'AutomaticLoginUsers') - return path - - -@dbus.service.method(MAIN_IFACE, in_signature='ssi', out_signature='o') -def CreateUser(self, name, fullname, account_type): - """ Creates an user using the default API """ - try: - self.FindUserByName(name) - found = True - except dbus.exceptions.DBusException: - found = False - - if found: - raise dbus.exceptions.DBusException( - 'User {} already exists'.format(name), - name='org.freedesktop.Accounts.Error.Failed') - - self.users_auto_uids += 1 - - return self.AddUser(self.users_auto_uids, name, DEFAULT_USER_PASSWORD, { - 'RealName': fullname, 'AccountType': account_type}, {}) - - -@dbus.service.method(MAIN_IFACE, in_signature='xb') -def DeleteUser(self, uid, _remove_files): - """ Removes a created user """ - path = self.FindUserById(uid) - - had_multiple_users = len(self.mock_users) > 1 - self.RemoveObject(path) - self.mock_users.pop(uid) - self.automatic_login_users.discard(path) - - self.EmitSignal(MAIN_IFACE, 'UserDeleted', 'o', [path]) - - if len(self.mock_users) == 0: - emit_properties_changed(self, MAIN_IFACE, 'HasNoUsers') - elif had_multiple_users and len(self.mock_users) < 2: - emit_properties_changed(self, MAIN_IFACE, 'HasMultipleUsers') - - -@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='o') -def CacheUser(self, username): - """ Cache an user """ - path = self.FindUserByName(username) - self.cached_users.append(path) - return path - - -@dbus.service.method(MAIN_IFACE, in_signature='s') -def UncacheUser(self, username): - """ Removes an user from the cache """ - path = self.FindUserByName(username) - self.cached_users.remove(path) - - -@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='ao') -def ListCachedUsers(self): - """ Lists the cached users """ - return self.cached_users - - -@dbus.service.method(MAIN_IFACE, in_signature='x', out_signature='o') -def FindUserById(_self, uid): - """ Finds an user by its user id """ - user = mockobject.objects.get(get_user_path(uid), None) - if not user: - raise dbus.exceptions.DBusException( - 'No such user exists', - name='org.freedesktop.Accounts.Error.Failed') - return get_user_path(uid) - - -@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='o') -def FindUserByName(self, username): - """ Finds an user form its name """ - try: - [user_id] = [uid for uid, props in self.mock_users.items() - if props['UserName'] == username] - except ValueError as e: - raise dbus.exceptions.DBusException( - 'No such user exists: {}'.format(e), - name='org.freedesktop.Accounts.Error.Failed') - return get_user_path(user_id) - - -@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature='s', - out_signature='a{sv}') -def GetAll(self, interface): - """ Implements the GetAll dbus properties interface method. - - This allows to override the getters using dynamic values. - """ - if interface == MAIN_IFACE: - return { - 'DaemonVersion': 'dbus-mock-0.1', - 'HasNoUsers': len(self.mock_users) == 0, - 'HasMultipleUsers': len(self.mock_users) > 1, - 'AutomaticLoginUsers': dbus.Array(self.automatic_login_users, - signature='o'), - } - if interface == USER_IFACE: - return self.properties - return dbus.Dictionary({}, signature='sv') - - -@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature='ss', - out_signature='v') -def Get(self, interface, prop): - """ Implements the Get dbus properties interface method. - - This allows to override the getters using dynamic values, via GetAll. - """ - return self.GetAll(interface)[prop] - - -def set_user_property(user, property_name, value): - """ Set an user property and emits the relative signals """ - if user.properties[property_name] == value: - return - user.properties[property_name] = value - emit_properties_changed(user, USER_IFACE, property_name) - user.EmitSignal(USER_IFACE, 'Changed', '', []) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetUserName(self, user_name): - set_user_property(self, 'UserName', user_name) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetRealName(self, real_name): - set_user_property(self, 'RealName', real_name) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetEmail(self, email): - set_user_property(self, 'Email', email) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetLanguage(self, language): - set_user_property(self, 'Language', language) - - -@dbus.service.method(USER_IFACE, in_signature='as') -def SetLanguages(self, languages): - set_user_property(self, 'Languages', dbus.Array(languages, signature='s')) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetXSession(self, x_session): - set_user_property(self, 'XSession', x_session) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetSession(self, session): - set_user_property(self, 'Session', session) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetSessionType(self, session_type): - set_user_property(self, 'SessionType', session_type) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetLocation(self, location): - set_user_property(self, 'Location', location) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetHomeDirectory(self, home_directory): - set_user_property(self, 'HomeDirectory', home_directory) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetShell(self, shell): - set_user_property(self, 'Shell', shell) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetIconFile(self, icon_file): - set_user_property(self, 'IconFile', icon_file) - - -@dbus.service.method(USER_IFACE, in_signature='b') -def SetLocked(self, locked): - set_user_property(self, 'Locked', locked) - - -@dbus.service.method(USER_IFACE, in_signature='i') -def SetAccountType(self, account_type): - set_user_property(self, 'AccountType', account_type) - - -@dbus.service.method(USER_IFACE, in_signature='i') -def SetPasswordMode(self, password_mode): - set_user_property(self, 'PasswordMode', password_mode) - - -@dbus.service.method(USER_IFACE, in_signature='s') -def SetPasswordHint(self, hint): - set_user_property(self, 'PasswordHint', hint) - - -@dbus.service.method(USER_IFACE, in_signature='ss') -def SetPassword(self, password, hint): - self.password = password - self.SetPasswordHint(hint) - - -@dbus.service.method(USER_IFACE, in_signature='b') -def SetAutomaticLogin(self, automatic_login): - manager = mockobject.objects[MAIN_OBJ] - if automatic_login: - manager.AddAutoLoginUser(self.properties['UserName']) - else: - manager.RemoveAutoLoginUser(self.properties['UserName']) - - -@dbus.service.method(MOCK_IFACE, in_signature='xxxxxxx') -def SetUserPasswordExpirationPolicy(self, uid, expiration_time, - last_change_time, min_days_between_changes, - max_days_between_changes, days_to_warn, - days_after_expiration_until_lock): - user = mockobject.objects[self.FindUserById(uid)] - user.pwd_expiration_policy = { - 'expiration_time': expiration_time, - 'last_change_time': last_change_time, - 'min_days_between_changes': min_days_between_changes, - 'max_days_between_changes': max_days_between_changes, - 'days_to_warn': days_to_warn, - 'days_after_expiration_until_lock': days_after_expiration_until_lock, - } - user.EmitSignal(USER_IFACE, 'Changed', '', []) - - -@dbus.service.method(USER_IFACE, in_signature='', out_signature='xxxxxx') -def GetPasswordExpirationPolicy(self): - return tuple(self.pwd_expiration_policy.values()) diff --git a/tests/mock_services/accounts_service.py b/tests/mock_services/accounts_service.py new file mode 100644 index 0000000..6f36efe --- /dev/null +++ b/tests/mock_services/accounts_service.py @@ -0,0 +1,393 @@ +'''Accounts Service D-Bus mock template''' + +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 3 of the License, or (at your option) any +# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text +# of the license. + +__author__ = 'Marco Trevisan' +__email__ = 'marco.trevisan@canonical.com' +__copyright__ = '(c) 2021 Canonical Ltd.' +__license__ = 'LGPL 3+' + +import sys +import time + +import dbus +from dbusmock import MOCK_IFACE, mockobject + +BUS_NAME = 'org.freedesktop.Accounts' +MAIN_OBJ = '/org/freedesktop/Accounts' +MAIN_IFACE = 'org.freedesktop.Accounts' +USER_IFACE = MAIN_IFACE + '.User' +SYSTEM_BUS = True + +DEFAULT_USER_PASSWORD = 'Pa$$wo0rd' + + +def get_user_path(uid): + return '/org/freedesktop/Accounts/User{}'.format(uid) + + +def load(mock, parameters=None): + parameters = parameters if parameters else {} + mock.mock_users = {} + mock.cached_users = [] + mock.automatic_login_users = set() + mock.users_auto_uids = 2000 + + mock.AddProperties(MAIN_IFACE, mock.GetAll(MAIN_IFACE)) + + for uid, name in parameters.get('users', {}).items(): + mock.AddUser(uid, name, DEFAULT_USER_PASSWORD, {}, {}) + +def emit_properties_changed(mock, interface=MAIN_IFACE, properties=None): + if properties is None: + properties = mock.GetAll(interface) + elif isinstance(properties, str): + properties = [properties] + + if isinstance(properties, (list, set)): + properties = {p: mock.Get(interface, p) for p in properties} + elif not isinstance(properties, dict): + raise TypeError('Unsupported properties type') + + mock.EmitSignal(dbus.PROPERTIES_IFACE, 'PropertiesChanged', 'sa{sv}as', ( + interface, properties, [])) + + +@dbus.service.method(MOCK_IFACE, in_signature='xssa{sv}a{sv}', + out_signature='o') +def AddUser(self, uid, username, password=DEFAULT_USER_PASSWORD, + overrides=None, password_policy_overrides=None): + '''Add user via uid and username and optionally overriding properties + + Returns the new object path. + ''' + path = get_user_path(uid) + default_props = { + 'Uid': dbus.UInt64(uid), + 'UserName': username, + 'RealName': username[0].upper() + username[1:] + ' Fake', + 'AccountType': dbus.Int32(1), + 'AutomaticLogin': False, + 'BackgroundFile': '', + 'Email': '{}@python-dbusmock.org'.format(username), + 'FormatsLocale': 'C', + 'HomeDirectory': '/nonexisting/mock-home/{}'.format(username), + 'IconFile': '', + 'InputSources': dbus.Array([], signature='a{ss}'), + 'Language': 'C', + 'Languages': dbus.Array([], signature='s'), + 'LocalAccount': True, + 'Location': '', + 'Locked': False, + 'LoginFrequency': dbus.UInt64(0), + 'LoginHistory': dbus.Array([], signature='(xxa{sv})'), + 'LoginTime': dbus.Int64(0), + 'PasswordHint': 'Remember it, come on!', + 'PasswordMode': 0, + 'Session': 'mock-session', + 'SessionType': 'wayland', + 'Shell': '/usr/bin/zsh', + 'SystemAccount': False, + 'XHasMessages': False, + 'XKeyboardLayouts': dbus.Array([], signature='s'), + 'XSession': 'mock-xsession', + } + default_props.update(overrides if overrides else {}) + self.AddObject(path, USER_IFACE, default_props, []) + + had_users = len(self.mock_users) != 0 + had_multiple_users = len(self.mock_users) > 1 + user = mockobject.objects[path] + user.password = password + user.properties = default_props + user.pwd_expiration_policy = { + 'expiration_time': sys.maxsize, + 'last_change_time': int(time.time()), + 'min_days_between_changes': 0, + 'max_days_between_changes': 0, + 'days_to_warn': 0, + 'days_after_expiration_until_lock': 0, + } + user.pwd_expiration_policy.update( + password_policy_overrides if password_policy_overrides else {}) + self.mock_users[uid] = default_props + + self.EmitSignal(MAIN_IFACE, 'UserAdded', 'o', [path]) + + if not had_users: + emit_properties_changed(self, MAIN_IFACE, 'HasNoUsers') + elif not had_multiple_users and len(self.mock_users) > 1: + emit_properties_changed(self, MAIN_IFACE, 'HasMultipleUsers') + + return path + + +@dbus.service.method(MOCK_IFACE, in_signature='', out_signature='ao') +def ListMockUsers(self): + """ List the mock users that have been created """ + return [get_user_path(uid) for uid in self.mock_users.keys()] + + +@dbus.service.method(MOCK_IFACE, in_signature='s', out_signature='o') +def AddAutoLoginUser(self, username): + """ Enable autologin for an user """ + path = self.FindUserByName(username) + self.automatic_login_users.add(path) + user = mockobject.objects[path] + set_user_property(user, 'AutomaticLogin', True) + emit_properties_changed(self, MAIN_IFACE, 'AutomaticLoginUsers') + return path + + +@dbus.service.method(MOCK_IFACE, in_signature='s', out_signature='o') +def RemoveAutoLoginUser(self, username): + """ Disables autologin for an user """ + path = self.FindUserByName(username) + self.automatic_login_users.remove(path) + user = mockobject.objects[path] + set_user_property(user, 'AutomaticLogin', False) + emit_properties_changed(self, MAIN_IFACE, 'AutomaticLoginUsers') + return path + + +@dbus.service.method(MAIN_IFACE, in_signature='ssi', out_signature='o') +def CreateUser(self, name, fullname, account_type): + """ Creates an user using the default API """ + try: + self.FindUserByName(name) + found = True + except dbus.exceptions.DBusException: + found = False + + if found: + raise dbus.exceptions.DBusException( + 'User {} already exists'.format(name), + name='org.freedesktop.Accounts.Error.Failed') + + self.users_auto_uids += 1 + + return self.AddUser(self.users_auto_uids, name, DEFAULT_USER_PASSWORD, { + 'RealName': fullname, 'AccountType': account_type}, {}) + + +@dbus.service.method(MAIN_IFACE, in_signature='xb') +def DeleteUser(self, uid, _remove_files): + """ Removes a created user """ + path = self.FindUserById(uid) + + had_multiple_users = len(self.mock_users) > 1 + self.RemoveObject(path) + self.mock_users.pop(uid) + self.automatic_login_users.discard(path) + + self.EmitSignal(MAIN_IFACE, 'UserDeleted', 'o', [path]) + + if len(self.mock_users) == 0: + emit_properties_changed(self, MAIN_IFACE, 'HasNoUsers') + elif had_multiple_users and len(self.mock_users) < 2: + emit_properties_changed(self, MAIN_IFACE, 'HasMultipleUsers') + + +@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='o') +def CacheUser(self, username): + """ Cache an user """ + path = self.FindUserByName(username) + self.cached_users.append(path) + return path + + +@dbus.service.method(MAIN_IFACE, in_signature='s') +def UncacheUser(self, username): + """ Removes an user from the cache """ + path = self.FindUserByName(username) + self.cached_users.remove(path) + + +@dbus.service.method(MAIN_IFACE, in_signature='', out_signature='ao') +def ListCachedUsers(self): + """ Lists the cached users """ + return self.cached_users + + +@dbus.service.method(MAIN_IFACE, in_signature='x', out_signature='o') +def FindUserById(_self, uid): + """ Finds an user by its user id """ + user = mockobject.objects.get(get_user_path(uid), None) + if not user: + raise dbus.exceptions.DBusException( + 'No such user exists', + name='org.freedesktop.Accounts.Error.Failed') + return get_user_path(uid) + + +@dbus.service.method(MAIN_IFACE, in_signature='s', out_signature='o') +def FindUserByName(self, username): + """ Finds an user form its name """ + try: + [user_id] = [uid for uid, props in self.mock_users.items() + if props['UserName'] == username] + except ValueError as e: + raise dbus.exceptions.DBusException( + 'No such user exists: {}'.format(e), + name='org.freedesktop.Accounts.Error.Failed') + return get_user_path(user_id) + + +@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature='s', + out_signature='a{sv}') +def GetAll(self, interface): + """ Implements the GetAll dbus properties interface method. + + This allows to override the getters using dynamic values. + """ + if interface == MAIN_IFACE: + return { + 'DaemonVersion': 'dbus-mock-0.1', + 'HasNoUsers': len(self.mock_users) == 0, + 'HasMultipleUsers': len(self.mock_users) > 1, + 'AutomaticLoginUsers': dbus.Array(self.automatic_login_users, + signature='o'), + } + if interface == USER_IFACE: + return self.properties + return dbus.Dictionary({}, signature='sv') + + +@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature='ss', + out_signature='v') +def Get(self, interface, prop): + """ Implements the Get dbus properties interface method. + + This allows to override the getters using dynamic values, via GetAll. + """ + return self.GetAll(interface)[prop] + + +def set_user_property(user, property_name, value): + """ Set an user property and emits the relative signals """ + if user.properties[property_name] == value: + return + user.properties[property_name] = value + emit_properties_changed(user, USER_IFACE, property_name) + user.EmitSignal(USER_IFACE, 'Changed', '', []) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetUserName(self, user_name): + set_user_property(self, 'UserName', user_name) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetRealName(self, real_name): + set_user_property(self, 'RealName', real_name) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetEmail(self, email): + set_user_property(self, 'Email', email) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetLanguage(self, language): + set_user_property(self, 'Language', language) + + +@dbus.service.method(USER_IFACE, in_signature='as') +def SetLanguages(self, languages): + set_user_property(self, 'Languages', dbus.Array(languages, signature='s')) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetXSession(self, x_session): + set_user_property(self, 'XSession', x_session) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetSession(self, session): + set_user_property(self, 'Session', session) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetSessionType(self, session_type): + set_user_property(self, 'SessionType', session_type) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetLocation(self, location): + set_user_property(self, 'Location', location) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetHomeDirectory(self, home_directory): + set_user_property(self, 'HomeDirectory', home_directory) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetShell(self, shell): + set_user_property(self, 'Shell', shell) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetIconFile(self, icon_file): + set_user_property(self, 'IconFile', icon_file) + + +@dbus.service.method(USER_IFACE, in_signature='b') +def SetLocked(self, locked): + set_user_property(self, 'Locked', locked) + + +@dbus.service.method(USER_IFACE, in_signature='i') +def SetAccountType(self, account_type): + set_user_property(self, 'AccountType', account_type) + + +@dbus.service.method(USER_IFACE, in_signature='i') +def SetPasswordMode(self, password_mode): + set_user_property(self, 'PasswordMode', password_mode) + + +@dbus.service.method(USER_IFACE, in_signature='s') +def SetPasswordHint(self, hint): + set_user_property(self, 'PasswordHint', hint) + + +@dbus.service.method(USER_IFACE, in_signature='ss') +def SetPassword(self, password, hint): + self.password = password + self.SetPasswordHint(hint) + + +@dbus.service.method(USER_IFACE, in_signature='b') +def SetAutomaticLogin(self, automatic_login): + manager = mockobject.objects[MAIN_OBJ] + if automatic_login: + manager.AddAutoLoginUser(self.properties['UserName']) + else: + manager.RemoveAutoLoginUser(self.properties['UserName']) + + +@dbus.service.method(MOCK_IFACE, in_signature='xxxxxxx') +def SetUserPasswordExpirationPolicy(self, uid, expiration_time, + last_change_time, min_days_between_changes, + max_days_between_changes, days_to_warn, + days_after_expiration_until_lock): + user = mockobject.objects[self.FindUserById(uid)] + user.pwd_expiration_policy = { + 'expiration_time': expiration_time, + 'last_change_time': last_change_time, + 'min_days_between_changes': min_days_between_changes, + 'max_days_between_changes': max_days_between_changes, + 'days_to_warn': days_to_warn, + 'days_after_expiration_until_lock': days_after_expiration_until_lock, + } + user.EmitSignal(USER_IFACE, 'Changed', '', []) + + +@dbus.service.method(USER_IFACE, in_signature='', out_signature='xxxxxx') +def GetPasswordExpirationPolicy(self): + return tuple(self.pwd_expiration_policy.values()) diff --git a/tests/test-libaccountsservice.py b/tests/test-libaccountsservice.py index 723ab51..f0261b1 100644 --- a/tests/test-libaccountsservice.py +++ b/tests/test-libaccountsservice.py @@ -39,7 +39,7 @@ class AccountsServiceTestBase(dbusmock.DBusTestCase): super().setUp() if not hasattr(self, '_mock'): template = os.path.join( - os.path.dirname(__file__), 'dbusmock/accounts_service.py') + os.path.dirname(__file__), 'mock_services/accounts_service.py') (self._mock, self._mock_obj) = self.spawn_server_template( template, {}, stdout=subprocess.PIPE) self._manager = AccountsService.UserManager.get_default() @@ -88,7 +88,7 @@ class TestAccountsServicePreExistingUser(AccountsServiceTestBase): '''Test mocking AccountsService with pre-existing user''' def setUp(self): template = os.path.join( - os.path.dirname(__file__), 'dbusmock/accounts_service.py') + os.path.dirname(__file__), 'mock_services/accounts_service.py') (self._mock, self._mock_obj) = self.spawn_server_template( template, {'users': { 2001: 'pizza' }}, stdout=subprocess.PIPE) super().setUp()