From 5efd72a9d722aab6a6cb07e47841aa00a77af7c0 Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Fri, 10 May 2024 17:37:46 +0200 Subject: [PATCH] Add users_add bulk operation --- src/tests/test_sso_and_portalapi.py | 11 +++-- src/user.py | 74 +++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/tests/test_sso_and_portalapi.py b/src/tests/test_sso_and_portalapi.py index c2afa6107..344d65814 100644 --- a/src/tests/test_sso_and_portalapi.py +++ b/src/tests/test_sso_and_portalapi.py @@ -7,7 +7,7 @@ import os from .conftest import message, raiseYunohostError, get_test_apps_dir from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list -from yunohost.user import user_create, user_list, user_delete +from yunohost.user import user_create, user_list, user_delete, User, users_add from yunohost.authenticators.ldap_ynhuser import Authenticator, SESSION_FOLDER, short_hash from yunohost.app import app_install, app_remove, app_setting, app_ssowatconf, app_change_url from yunohost.permission import user_permission_list, user_permission_update @@ -44,10 +44,11 @@ def setup_module(module): assert os.system("systemctl is-active yunohost-portal-api >/dev/null") == 0 userlist = user_list()["users"] - if "alice" not in userlist: - user_create("alice", maindomain, dummy_password, fullname="Alice White", admin=True) - if "bob" not in userlist: - user_create("bob", maindomain, dummy_password, fullname="Bob Marley") + users_to_add = [ user for user in [ + User("alice", maindomain, dummy_password, fullname="Alice White", admin=True), + User("bob", maindomain, dummy_password, fullname="Bob Marley"), + ] if user.name not in userlist ] + users_add(users_to_add) domainlist = domain_list()["domains"] domains = [ domain for domain in [ subdomain, secondarydomain ] if domain not in domainlist ] diff --git a/src/user.py b/src/user.py index f8ead156f..c30b550a3 100644 --- a/src/user.py +++ b/src/user.py @@ -24,6 +24,7 @@ import random import subprocess import copy from logging import getLogger +from typing import List, Optional from moulinette import Moulinette, m18n from moulinette.utils.process import check_output @@ -49,6 +50,20 @@ FIELDS_FOR_IMPORT = { ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] +class User: + def __init__( + self, + name: str, + domain: str, + password: str, + fullname: Optional[str] = None, + admin: bool = False, + ): + self.name = name + self.password = password + self.domain = domain + self.fullname = fullname + self.admin = admin def user_list(fields=None): from yunohost.utils.ldap import _get_ldap_interface @@ -131,6 +146,40 @@ def shellexists(shell): """Check if the provided shell exists and is executable.""" return os.path.isfile(shell) and os.access(shell, os.X_OK) +# Used in tests to create many users at once. +# The permissions are synchronized at the end of the entire operation. +@is_unit_operation() +def users_add( + operation_logger, + users: List[User], +): + hooks = [] + for user in users: + # Only force regen_conf on the last iteration + hooks.append(user_create(user.name, user.domain, user.password, fullname=user.fullname, admin=user.admin, do_regen_conf=False)) + + # Invalidate passwd and group to take user and group creation into account + subprocess.call(["nscd", "-i", "passwd"]) + subprocess.call(["nscd", "-i", "group"]) + + # Add new users to all_users group + user_group_update(groupname="all_users", add=[ user.name for user in users ], force=True, sync_perm=False) + + # Do we have new admins? + admins = [ user.name for user in users if user.admin ] + if len(admins) > 0: + user_group_update(groupname="admins", add=admins, sync_perm=False) + + from yunohost.permission import permission_sync_to_user + from yunohost.hook import hook_callback + + # Now we can sync the permissions + permission_sync_to_user() + + for hook in hooks: + # Trigger post_user_create hooks + hook_callback("post_user_create", args=[hook["YNH_USER_USERNAME"], hook["YNH_USER_PASSWORD"]], env=hook) + logger.success(m18n.n("user_created")) @is_unit_operation([("username", "user")]) def user_create( @@ -143,6 +192,7 @@ def user_create( admin=False, from_import=False, loginShell=None, + do_regen_conf=True, ): if not fullname or not fullname.strip(): raise YunohostValidationError( @@ -260,9 +310,6 @@ def user_create( except Exception as e: raise YunohostError("user_creation_failed", user=username, error=e) - # Invalidate passwd and group to take user and group creation into account - subprocess.call(["nscd", "-i", "passwd"]) - subprocess.call(["nscd", "-i", "group"]) try: # Attempt to create user home folder @@ -277,13 +324,8 @@ def user_create( except subprocess.CalledProcessError: logger.warning(f"Failed to protect /home/{username}", exc_info=1) - # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) - user_group_update(groupname="all_users", add=username, force=True, sync_perm=True) - if admin: - user_group_update(groupname="admins", add=username, sync_perm=True) - # Trigger post_user_create hooks env_dict = { "YNH_USER_USERNAME": username, "YNH_USER_MAIL": mail, @@ -292,6 +334,22 @@ def user_create( "YNH_USER_LASTNAME": lastname, } + # If do_regen_conf is False, it means we are in a higher operation that will + # finish synchronizing everything, then run the hooks... so we return early, + # transmitting the env_dict for further hook run. + if not do_regen_conf: + return env_dict + + # Invalidate passwd and group to take user and group creation into account + subprocess.call(["nscd", "-i", "passwd"]) + subprocess.call(["nscd", "-i", "group"]) + + # Create group for user and add to group 'all_users' + user_group_update(groupname="all_users", add=username, force=True, sync_perm=not admin) + if admin: + user_group_update(groupname="admins", add=username, sync_perm=True) + + # Trigger post_user_create hooks hook_callback("post_user_create", args=[username, mail], env=env_dict) # TODO: Send a welcome mail to user