diff --git a/data/helpers.d/user b/data/helpers.d/user index aeac3a9c5d..16eaeac892 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -73,6 +73,20 @@ ynh_system_user_exists() { getent passwd "$username" &>/dev/null } +# Check if the app user exists on LDAP +# +# Note that the username is : $YNH_APP_INSTANCE_NAME +# +# usage: ynh_ldap_user_exists +# | exit: Return 1 if the user doesn't exist, 0 otherwise +# +# Requires YunoHost version 3.8.0 or higher. +ynh_ldap_user_exists() { + # Declare an array to define the options of this helper. + local username=$YNH_APP_INSTANCE_NAME + test -n "$(yunohost tools shell -c "from yunohost.user import user_list; print(user_list(fields=['uid'], app_user=True)['users'].get('$username', ''))")" +} + # Check if a group exists on the system # # usage: ynh_system_group_exists --group=group @@ -135,6 +149,64 @@ ynh_system_user_create () { fi } + +# Create a LDAP user for app +# +# Note that the username is : $YNH_APP_INSTANCE_NAME +# This will create a user and a group in LDAP in the LDAP. +# The user will be in: ou=users,ou=apps,dc=yunohost,dc=org +# The group will be in: ou=groups,ou=apps,dc=yunohost,dc=org +# +# examples: +# # Create a nextcloud user with no home directory, /usr/sbin/nologin login shell (hence no login capability) and nextcloud@main_domain.tld as email +# ynh_ldap_user_create --password="The-Best_password" --home_dir /var/www/nextcloud +# # Create a discourse user using /var/www/discourse as home directory and the default login shell +# ynh_ldap_user_create --password="RAND0MP4sSw0RO" --home_dir=/var/www/discourse --mail forum@app_domain.tld --use_shell +# +# usage: ynh_ldap_user_create --username=user_name --password=password --home_dir=home_dir [--mail=app_email] [--use_shell] +# | arg: -p, --password - Password of the user. Used for the authentication in LDAP, sending and receiving email. +# | arg: -h, --home_dir= - Path of the home dir for the user. Usually the final path of the app. +# | arg: -m, --mail - Email of this user. By default it's username@yunohost_main_domain +# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell +# +# Note that with this helper the app will have a mailbox accessible with SMTP and IMAP. +# the identifier are: +# user: $YNH_APP_INSTANCE_NAME +# password: password defined when the helper is called. +# +# Requires YunoHost version 3.8.0 or higher. +ynh_ldap_user_create () { + # Declare an array to define the options of this helper. + local legacy_args=phms + local -A args_array=([p]=password= [h]=home_dir= [m]=mail [s]=use_shell ) + local password + local home_dir + local mail + local use_shell + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + local username=$YNH_APP_INSTANCE_NAME + mail="${mail:-}" + use_shell="${use_shell:-0}" + + if ! ynh_ldap_user_exists # Check if the user exists in LDAP + then # If the user doesn't exist + if [ -n $mail ] + then + mail="$username"@$(cat /etc/yunohost/current_host) + fi + if [ $use_shell -eq 1 ] + then # If we want a shell for the user + local shell="/bin/bash" + else + local shell="/usr/sbin/nologin" + fi + + # Note that we can't use the CLI because we need to specify that it's a app_user and not a normal user. + yunohost tools shell -c "from yunohost.user import user_create; user_create('$username', firstname='$username', lastname='App', mail='$mail', password='$password', home_directory='$home_dir', shell='$shell', app_user=True)" + fi +} + # Delete a system user # # usage: ynh_system_user_delete --username=user_name @@ -163,3 +235,25 @@ ynh_system_user_delete () { delgroup $username fi } + +# Delete the LDAP user +# +# Note that the username is : $YNH_APP_INSTANCE_NAME +# +# Note that the LDAP app user is automatically deleted by the core when the app is removed +# +# usage: ynh_LDAP_user_delete +# +# Requires YunoHost version 3.8.0 or higher. +ynh_ldap_user_delete () { + # Declare an array to define the options of this helper. + local username=$YNH_APP_INSTANCE_NAME + + # Check if the user exists on the system + if ynh_ldap_user_exists + then + yunohost tools shell -c "from yunohost.user import user_delete; user_delete('$username', app_user=True)" + else + ynh_print_warn --message="The user $username was not found" + fi +} diff --git a/data/other/ldap_scheme.yml b/data/other/ldap_scheme.yml index 660d6fbb50..7082665547 100644 --- a/data/other/ldap_scheme.yml +++ b/data/other/ldap_scheme.yml @@ -64,6 +64,17 @@ children: - posixGroup - groupOfNamesYnh + ou=users,ou=apps: + ou: users + objectClass: + - organizationalUnit + - top + ou=groups,ou=apps: + ou: users + objectClass: + - organizationalUnit + - top + depends_children: cn=mail.main,ou=permission: cn: mail.main diff --git a/data/templates/dovecot/dovecot-ldap.conf b/data/templates/dovecot/dovecot-ldap.conf index 3a80ba47f2..acb240a079 100644 --- a/data/templates/dovecot/dovecot-ldap.conf +++ b/data/templates/dovecot/dovecot-ldap.conf @@ -1,9 +1,8 @@ hosts = 127.0.0.1 auth_bind = yes ldap_version = 3 -base = ou=users,dc=yunohost,dc=org +base = dc=yunohost,dc=org user_attrs = uidNumber=500,gidNumber=8,mailuserquota=quota_rule=*:bytes=%$ -user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) -pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps))) +pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps))) default_pass_scheme = SSHA - diff --git a/data/templates/postfix/plain/ldap-accounts.cf b/data/templates/postfix/plain/ldap-accounts.cf index 75f38cf580..33675d6473 100644 --- a/data/templates/postfix/plain/ldap-accounts.cf +++ b/data/templates/postfix/plain/ldap-accounts.cf @@ -1,5 +1,5 @@ server_host = localhost server_port = 389 search_base = dc=yunohost,dc=org -query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +query_filter = (&(objectClass=mailAccount)(mail=%s)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps))) result_attribute = uid diff --git a/data/templates/postfix/plain/ldap-aliases.cf b/data/templates/postfix/plain/ldap-aliases.cf index 46563ae223..0f1abb77ee 100644 --- a/data/templates/postfix/plain/ldap-aliases.cf +++ b/data/templates/postfix/plain/ldap-aliases.cf @@ -1,5 +1,5 @@ server_host = localhost server_port = 389 search_base = dc=yunohost,dc=org -query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +query_filter = (&(objectClass=mailAccount)(mail=%s)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps))) result_attribute = maildrop diff --git a/locales/en.json b/locales/en.json index 25712e8cd4..43d2366447 100644 --- a/locales/en.json +++ b/locales/en.json @@ -428,6 +428,7 @@ "migration_description_0012_postgresql_password_to_md5_authentication": "Force PostgreSQL authentication to use MD5 for local connections", "migration_description_0013_futureproof_apps_catalog_system": "Migrate to the new future-proof apps catalog system", "migration_description_0014_remove_app_status_json": "Remove legacy status.json app files", + "migration_description_0015_update_ldap_for_apps_users": "Update LDAP structure for app user in LDAP", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists…", "migration_0003_main_upgrade": "Starting main upgrade…", @@ -465,6 +466,8 @@ "migration_0011_update_LDAP_database": "Updating LDAP database…", "migration_0011_update_LDAP_schema": "Updating LDAP schema…", "migration_0011_failed_to_remove_stale_object": "Could not remove stale object {dn}: {error}", + "migration_0015_update_LDAP_database": "Updating LDAP database for app's user", + "migration_0015_done": "Migration completed. App can have his own user.", "migrations_already_ran": "Those migrations are already done: {ids}", "migrations_cant_reach_migration_file": "Could not access migrations files at the path '%s'", "migrations_dependencies_not_satisfied": "Run these migrations: '{dependencies_id}', before migration {id}.", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e2df6ba78c..c4593ef62e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -626,6 +626,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.log import OperationLogger + from yunohost.user import user_list, user_delete from yunohost.permission import user_permission_list, permission_create, permission_url, permission_delete, permission_sync_to_user, user_permission_update # Fetch or extract sources @@ -860,6 +861,10 @@ def confirm_install(confirm): if permission_name.startswith(app_instance_name+"."): permission_delete(permission_name, force=True, sync_perm=False) + # Remove app user in LDAP + if app_instance_name in user_list(fields=['uid'], app_user=True)['users']: + user_delete(app_instance_name, app_user=True) + if remove_retcode != 0: msg = m18n.n('app_not_properly_removed', app=app_instance_name) @@ -992,6 +997,7 @@ def app_remove(operation_logger, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback + from yunohost.user import user_list, user_delete from yunohost.permission import user_permission_list, permission_delete, permission_sync_to_user if not _is_installed(app): raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) @@ -1060,6 +1066,10 @@ def app_remove(operation_logger, app): if permission_name.startswith(app+"."): permission_delete(permission_name, force=True, sync_perm=False) + # Remove app user in LDAP + if app in user_list(fields=['uid'], app_user=True)['users']: + user_delete(app, app_user=True) + permission_sync_to_user() _assert_system_is_sane_for_app(manifest, "post") diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index d059170e9f..a9acd52991 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1182,6 +1182,7 @@ def _restore_system(self): if system_targets == []: return + from yunohost.tools import _get_migration_by_name from yunohost.user import user_group_list from yunohost.permission import permission_create, permission_delete, user_permission_update, user_permission_list, permission_sync_to_user @@ -1230,7 +1231,6 @@ def _restore_system(self): # # Legacy code if not "all_users" in user_group_list()["groups"].keys(): - from yunohost.tools import _get_migration_by_name setup_group_permission = _get_migration_by_name("setup_group_permission") # Update LDAP schema restart slapd logger.info(m18n.n("migration_0011_update_LDAP_schema")) @@ -1247,6 +1247,11 @@ def _restore_system(self): if _is_installed(app_name): permission_create(permission_name, url=permission_infos["url"], allowed=permission_infos["allowed"], sync_perm=False) + # Run this migration in all case. This will clean all app user while the backup is made + # and this will update the LDAP tree for app user in LDAP + cleanup_ldap_app_users = _get_migration_by_name("update_ldap_for_apps_users") + cleanup_ldap_app_users.run() + permission_sync_to_user() diff --git a/src/yunohost/data_migrations/0015_update_ldap_for_apps_users.py b/src/yunohost/data_migrations/0015_update_ldap_for_apps_users.py new file mode 100644 index 0000000000..fe2bd7af50 --- /dev/null +++ b/src/yunohost/data_migrations/0015_update_ldap_for_apps_users.py @@ -0,0 +1,67 @@ +import time +import os + +from moulinette import m18n +from yunohost.utils.error import YunohostError +from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_yaml + +from yunohost.tools import Migration + +logger = getActionLogger('yunohost.migration') + +class MyMigration(Migration): + """ + Update the LDAP DB to be able to store apps' user in LDAP + """ + + required = True + + def remove_if_exists(self, target): + + from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() + + try: + objects = ldap.search(target + ",dc=yunohost,dc=org") + # ldap search will raise an exception if no corresponding object is found >.> ... + except Exception as e: + logger.debug("%s does not exist, no need to delete it" % target) + return + + objects.reverse() + for o in objects: + for dn in o["dn"]: + dn = dn.replace(",dc=yunohost,dc=org", "") + logger.debug("Deleting old object %s ..." % dn) + try: + ldap.remove(dn) + except Exception as e: + raise YunohostError("migration_0011_failed_to_remove_stale_object", dn=dn, error=e) + + + def run(self): + + logger.info(m18n.n("migration_0015_update_LDAP_database")) + + from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() + + ldap_map = read_yaml('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') + + try: + self.remove_if_exists('ou=apps') + + attr_dict = ldap_map['parents']['ou=apps'] + ldap.add('ou=apps', attr_dict) + + attr_dict = ldap_map['children']['ou=users,ou=apps'] + ldap.add('ou=users,ou=apps', attr_dict) + + attr_dict = ldap_map['children']['ou=groups,ou=apps'] + ldap.add('ou=groups,ou=apps', attr_dict) + + except Exception as e: + raise YunohostError("migration_0011_LDAP_update_failed", error=e) + + logger.info(m18n.n("migration_0015_done")) diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py index f1eae9c4ea..3194968efc 100644 --- a/src/yunohost/tests/test_user-group.py +++ b/src/yunohost/tests/test_user-group.py @@ -5,6 +5,7 @@ from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ user_group_list, user_group_create, user_group_delete, user_group_update from yunohost.domain import _get_maindomain +from yunohost.app import app_install, app_remove from yunohost.tests.test_permission import check_LDAP_db_integrity # Get main domain @@ -19,6 +20,13 @@ def clean_user_groups(): if g not in ["all_users", "visitors"]: user_group_delete(g) + for u in user_list(app_user=True)['users']: + user_delete(u, app_user=True) + + for g in user_group_list(app_group=True)['groups']: + if g not in ["all_users", "visitors"]: + user_group_delete(g, app_group=True) + def setup_function(function): clean_user_groups() @@ -29,6 +37,7 @@ def setup_function(function): user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh") user_create("bob", "Bob", "Snow", "bob@" + maindomain, "test123Ynh") user_create("jack", "Jack", "Black", "jack@" + maindomain, "test123Ynh") + user_create("wordpress", "wordpress", "app", "wordpress@" + maindomain, "test123Ynh", app_user=True) user_group_create("dev") user_group_create("apps") @@ -38,6 +47,10 @@ def setup_function(function): def teardown_function(function): clean_user_groups() + try: + app_remove("ldap_user_app") + except: + pass @pytest.fixture(autouse=True) @@ -53,19 +66,35 @@ def check_LDAP_db_integrity_call(): def test_list_users(): res = user_list()['users'] + res_app = user_list(app_user=True)['users'] assert "alice" in res assert "bob" in res assert "jack" in res + assert "wordpress" not in res + + assert "alice" not in res_app + assert "bob" not in res_app + assert "jack" not in res_app + assert "wordpress" in res_app def test_list_groups(): res = user_group_list()['groups'] + res_app = user_group_list(app_group=True)['groups'] assert "all_users" in res assert "alice" in res assert "bob" in res assert "jack" in res + assert "wordpress" not in res + + assert "all_users" not in res_app + assert "alice" not in res_app + assert "bob" not in res_app + assert "jack" not in res_app + assert "wordpress" in res_app + for u in ["alice", "bob", "jack"]: assert u in res assert u in res[u]['members'] @@ -118,6 +147,18 @@ def test_del_group(mocker): group_res = user_group_list()['groups'] assert "dev" not in group_res + +def test_create_app_user(mocker): + with message(mocker, "user_created"): + user_create("discourse", "discourse", "app", "discourse@" + maindomain, "test123Ynh", app_user=True) + + group_res = user_group_list(app_group=False)['groups'] + group_res_app = user_group_list(app_group=True)['groups'] + assert "discourse" in user_list(app_user=True)['users'] + assert "discourse" in group_res_app + assert "discourse" in group_res_app['discourse']['members'] + assert "discourse" not in group_res['all_users']['members'] + # # Error on create / remove function # @@ -248,3 +289,17 @@ def test_update_group_add_user_that_doesnt_exist(mocker): user_group_update("dev", add=["doesnt_exist"]) assert "doesnt_exist" not in user_group_list()["groups"]["dev"]["members"] + + +# +# Test app with user in LDAP +# + +def test_install_app_ldap_user(): + app_install("./tests/apps/ldap_user_app_ynh", + args="domain=" + maindomain, force=True) + + # App is configured as public by default using the legacy unprotected_uri mechanics + # It should automatically be migrated during the install + res = user_list(app_user=True)['users'] + assert "ldap_user_app" in res diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 282ec84079..5d6eb2164f 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -45,7 +45,7 @@ logger = getActionLogger('yunohost.user') -def user_list(fields=None): +def user_list(fields=None, app_user=False): """ List users @@ -70,6 +70,7 @@ def user_list(fields=None): attrs = ['uid'] users = {} + ldap_user_path = 'ou=users,ou=apps' if app_user else 'ou=users' if fields: keys = user_attrs.keys() @@ -82,7 +83,7 @@ def user_list(fields=None): attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] ldap = _get_ldap_interface() - result = ldap.search('ou=users,dc=yunohost,dc=org', + result = ldap.search('%s,dc=yunohost,dc=org' % ldap_user_path, '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', attrs) @@ -106,17 +107,21 @@ def user_list(fields=None): @is_unit_operation([('username', 'user')]) def user_create(operation_logger, username, firstname, lastname, mail, password, - mailbox_quota="0"): + home_directory=None, shell='/bin/false', + mailbox_quota="0", app_user=False): """ Create user Keyword argument: firstname lastname - username -- Must be unique - mail -- Main mail address must be unique + username -- Must be unique + mail -- Main mail address must be unique password - mailbox_quota -- Mailbox size quota + home_directory -- Home directory of user. Default is /home/username + shell -- Shell of the user. Default is /bin/false + mailbox_quota -- Mailbox size quota + app_user -- Define if it is a user dedicated to an app """ from yunohost.domain import domain_list, _get_maindomain @@ -189,16 +194,19 @@ def user_create(operation_logger, username, firstname, lastname, mail, password, 'userPassword': _hash_user_password(password), 'gidNumber': uid, 'uidNumber': uid, - 'homeDirectory': '/home/' + username, - 'loginShell': '/bin/false' + 'homeDirectory': '/home/' + username if home_directory is None else home_directory, + 'loginShell': shell } # If it is the first user, add some aliases - if not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): + if not app_user and not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): attr_dict['mail'] = [attr_dict['mail']] + aliases + # Define the place in LDAP where we put the user + ldap_user_path = 'ou=users,ou=apps' if app_user else 'ou=users' + try: - ldap.add('uid=%s,ou=users' % username, attr_dict) + ldap.add('uid=%s,%s' % (username, ldap_user_path), attr_dict) except Exception as e: raise YunohostError('user_creation_failed', user=username, error=e) @@ -206,65 +214,72 @@ def user_create(operation_logger, username, firstname, lastname, mail, password, subprocess.call(['nscd', '-i', 'passwd']) subprocess.call(['nscd', '-i', 'group']) - try: - # Attempt to create user home folder - subprocess.check_call( - ['su', '-', username, '-c', "''"]) - except subprocess.CalledProcessError: - if not os.path.isdir('/home/{0}'.format(username)): - logger.warning(m18n.n('user_home_creation_failed'), - exc_info=1) + if not app_user: + try: + # Attempt to create user home folder + subprocess.check_call( + ['su', '-', username, '-c', "''"]) + except subprocess.CalledProcessError: + if not os.path.isdir('/home/{0}'.format(username)): + logger.warning(m18n.n('user_home_creation_failed'), + 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) + user_group_create(groupname=username, gid=uid, primary_group=True, app_group=app_user, sync_perm=False) + if not app_user: + user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) - hook_callback('post_user_create', - args=[username, mail, password, firstname, lastname]) + if not app_user: + hook_callback('post_user_create', + args=[username, mail, password, firstname, lastname]) return {'fullname': fullname, 'username': username, 'mail': mail} @is_unit_operation([('username', 'user')]) -def user_delete(operation_logger, username, purge=False): +def user_delete(operation_logger, username, purge=False, app_user=False): """ Delete user Keyword argument: username -- Username to delete purge + app_user -- Define if it is a user dedicated to an app """ from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface from yunohost.permission import permission_sync_to_user - if username not in user_list()["users"]: + if username not in user_list(app_user=app_user)["users"]: raise YunohostError('user_unknown', user=username) + ldap_user_path = 'ou=users,ou=apps' if app_user else 'ou=users' + operation_logger.start() - user_group_update("all_users", remove=username, force=True, sync_perm=False) - for group, infos in user_group_list()["groups"].items(): - if group == "all_users": - continue - # If the user is in this group (and it's not the primary group), - # remove the member from the group - if username != group and username in infos["members"]: - user_group_update(group, remove=username, sync_perm=False) + if not app_user: + user_group_update("all_users", remove=username, force=True, sync_perm=False) + for group, infos in user_group_list()["groups"].items(): + if group == "all_users": + continue + # If the user is in this group (and it's not the primary group), + # remove the member from the group + if username != group and username in infos["members"]: + user_group_update(group, remove=username, sync_perm=False) # Delete primary group if it exists (why wouldnt it exists ? because some # epic bug happened somewhere else and only a partial removal was # performed...) - if username in user_group_list()['groups'].keys(): - user_group_delete(username, force=True, sync_perm=True) + if username in user_group_list(app_group=app_user)['groups'].keys(): + user_group_delete(username, force=True, app_group=app_user, sync_perm=True) ldap = _get_ldap_interface() try: - ldap.remove('uid=%s,ou=users' % username) + ldap.remove('uid=%s,%s' % (username, ldap_user_path)) except Exception as e: raise YunohostError('user_deletion_failed', user=username, error=e) @@ -272,10 +287,12 @@ def user_delete(operation_logger, username, purge=False): subprocess.call(['nscd', '-i', 'passwd']) if purge: - subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) + if app_user: + subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)]) - hook_callback('post_user_delete', args=[username, purge]) + if not app_user: + hook_callback('post_user_delete', args=[username, purge]) logger.success(m18n.n('user_deleted')) @@ -410,12 +427,13 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= return user_info(username) -def user_info(username): +def user_info(username, app_user=False): """ Get user informations Keyword argument: username -- Username or mail to get informations + app_user -- Define if it is a user dedicated to an app """ from yunohost.utils.ldap import _get_ldap_interface @@ -425,13 +443,14 @@ def user_info(username): user_attrs = [ 'cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn', 'mailuserquota' ] + ldap_user_path = 'ou=users,ou=apps' if app_user else 'ou=users' if len(username.split('@')) == 2: filter = 'mail=' + username else: filter = 'uid=' + username - result = ldap.search('ou=users,dc=yunohost,dc=org', filter, user_attrs) + result = ldap.search('%s,dc=yunohost,dc=org' % ldap_user_path, filter, user_attrs) if result: user = result[0] @@ -497,7 +516,7 @@ def user_info(username): # # Group subcategory # -def user_group_list(short=False, full=False, include_primary_groups=True): +def user_group_list(short=False, full=False, include_primary_groups=True, app_group=False): """ List users @@ -508,13 +527,16 @@ def user_group_list(short=False, full=False, include_primary_groups=True): This option is set to false by default in the action map because we don't want to have these displayed when the user runs `yunohost user group list`, but internally we do want to list them when called from other functions + app_group -- Define if it is a group dedicated to an app + """ # Fetch relevant informations from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract ldap = _get_ldap_interface() - groups_infos = ldap.search('ou=groups,dc=yunohost,dc=org', + ldap_group_path = 'ou=groups,ou=apps' if app_group else 'ou=groups' + groups_infos = ldap.search('%s,dc=yunohost,dc=org' % ldap_group_path, '(objectclass=groupOfNamesYnh)', ["cn", "member", "permission"]) @@ -543,13 +565,13 @@ def user_group_list(short=False, full=False, include_primary_groups=True): @is_unit_operation([('groupname', 'group')]) -def user_group_create(operation_logger, groupname, gid=None, primary_group=False, sync_perm=True): +def user_group_create(operation_logger, groupname, gid=None, primary_group=False, app_group=False, sync_perm=True): """ Create group Keyword argument: groupname -- Must be unique - + app_group -- Define if it is a group dedicated to an app """ from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface @@ -586,16 +608,18 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False 'cn': groupname, 'gidNumber': gid, } + ldap_user_path = 'ou=users,ou=apps' if app_group else 'ou=users' + ldap_group_path = 'ou=groups,ou=apps' if app_group else 'ou=groups' # Here we handle the creation of a primary group # We want to initialize this group to contain the corresponding user # (then we won't be able to add/remove any user in this group) if primary_group: - attr_dict["member"] = ["uid=" + groupname + ",ou=users,dc=yunohost,dc=org"] + attr_dict["member"] = ["uid=%s,%s,dc=yunohost,dc=org" % (groupname, ldap_user_path)] operation_logger.start() try: - ldap.add('cn=%s,ou=groups' % groupname, attr_dict) + ldap.add('cn=%s,%s' % (groupname, ldap_group_path), attr_dict) except Exception as e: raise YunohostError('group_creation_failed', group=groupname, error=e) @@ -611,18 +635,18 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False @is_unit_operation([('groupname', 'group')]) -def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): +def user_group_delete(operation_logger, groupname, force=False, app_group=False, sync_perm=True): """ Delete user Keyword argument: groupname -- Groupname to delete - + app_group -- Define if it is a group dedicated to an app """ from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface - existing_groups = user_group_list()['groups'].keys() + existing_groups = user_group_list(app_group=app_group)['groups'].keys() if groupname not in existing_groups: raise YunohostError('group_unknown', group=groupname) @@ -635,10 +659,12 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): if groupname in undeletable_groups and not force: raise YunohostError('group_cannot_be_deleted', group=groupname) + ldap_group_path = 'ou=groups,ou=apps' if app_group else 'ou=groups' + operation_logger.start() ldap = _get_ldap_interface() try: - ldap.remove('cn=%s,ou=groups' % groupname) + ldap.remove('cn=%s,%s' % (groupname, ldap_group_path)) except Exception as e: raise YunohostError('group_deletion_failed', group=groupname, error=e)