diff --git a/actions/ldap b/actions/ldap deleted file mode 100755 index b6c2ae69b..000000000 --- a/actions/ldap +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash -# -# This file is part of Plinth. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -# Store anything available from stdin. -# This is used to receive passwords from Plinth. -if read -t 0; then - IFS= read -r input -fi - -set -e # Exit on failure - - -create_user() -{ - username="$1" - password="$2" - - # All users shall have 'users' (a group in /etc/group) as primary group. - ldapadduser $username users > /dev/null - - set_user_password $username $password - - flush_cache -} - - -delete_user() -{ - username="$1" - - groups=$(get_user_groups $username) - - ldapdeleteuser $username - - while read -r group; do - ldapdeleteuserfromgroup $username $group > /dev/null || true - done <<< "$groups" - - flush_cache -} - - -rename_user() -{ - old_username="$1" - new_username="$2" - - groups=$(get_user_groups $old_username) - - ldaprenameuser $old_username $new_username - - while read -r group; do - ldapdeleteuserfromgroup $old_username $group > /dev/null || true - ldapaddusertogroup $new_username $group > /dev/null || true - done <<< "$groups" - - flush_cache -} - - -set_user_password() -{ - username="$1" - password=$(slappasswd -s "$2") - - ldapsetpasswd "$username" "$password" -} - - -get_user_groups() -{ - # Return only supplimentary groups and don't include the 'users' - # primary group. - username="$1" - - ldapid $username | cut -f 3 -d ' ' | cut -d = -f 2 | sed 's+,+\n+g' | sed "s+.*(\(.*\))+\1+" | grep -v users || true -} - - -add_group() -{ - groupname="$1" - - ldapsearch -Q -L -L -L -Y EXTERNAL -H ldapi:/// -s base -b "cn=${groupname},dc=thisbox" || ldapaddgroup "${groupname}" > /dev/null 2>&1 -} - - -remove_group() -{ - groupname="$1" - - ldapsearch -Q -L -L -L -Y EXTERNAL -H ldapi:/// -s base -b "cn=${groupname},dc=thisbox" && ldapdeletegroup "${groupname}" > /dev/null 2>&1 -} - - -add_user_to_group() -{ - username="$1" - groupname="$2" - - # Try to create group and ignore failure if group already exists - add_group "${groupname}" - - ldapaddusertogroup $username $groupname > /dev/null - - flush_cache -} - - -remove_user_from_group() -{ - username="$1" - groupname="$2" - - ldapdeleteuserfromgroup $username $groupname > /dev/null - - flush_cache -} - - -flush_cache() -{ - # Flush nscd cache - nscd --invalidate=passwd - nscd --invalidate=group -} - - -command=$1 -shift -case $command in - create-user) - create_user "$1" "$input" - ;; - delete-user) - delete_user "$@" - ;; - rename-user) - rename_user "$@" - ;; - set-user-password) - set_user_password "$1" "$input" - ;; - get-user-groups) - get_user_groups "$@" - ;; - add-user-to-group) - add_user_to_group "$@" - ;; - remove-user-from-group) - remove_user_from_group "$@" - ;; - add-group) - add_group "$@" - ;; - remove-group) - remove_group "$@" - ;; - *) - echo "Invalid sub-command" - exit -1 - ;; -esac diff --git a/actions/users b/actions/users index de63ba530..97c361235 100755 --- a/actions/users +++ b/actions/users @@ -16,15 +16,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ Configuration helper for the LDAP user directory """ import argparse -import subprocess - import augeas +import re +import subprocess +import sys + from plinth import action_utils ACCESS_CONF = '/etc/security/access.conf' @@ -38,6 +39,51 @@ def parse_arguments(): subparsers.add_parser('setup', help='Setup LDAP') + subparser = subparsers.add_parser( + 'create-user', help='Create an LDAP user') + subparser.add_argument('username', help='Name of the LDAP user to create') + + subparser = subparsers.add_parser( + 'remove-user', help='Delete an LDAP user') + subparser.add_argument('username', help='Name of the LDAP user to delete') + + subparser = subparsers.add_parser( + 'rename-user', help='Rename an LDAP user') + subparser.add_argument('oldusername', help='Old name of the LDAP user') + subparser.add_argument('newusername', help='New name of the LDAP user') + + subparser = subparsers.add_parser( + 'set-user-password', help='Set the password of an LDAP user') + subparser.add_argument( + 'username', help='Name of the LDAP user to set the password for') + + subparser = subparsers.add_parser( + 'create-group', help='Create an LDAP group') + subparser.add_argument( + 'groupname', help='Name of the LDAP group to create') + + subparser = subparsers.add_parser( + 'remove-group', help='Delete an LDAP group') + subparser.add_argument( + 'groupname', help='Name of the LDAP group to delete') + + subparser = subparsers.add_parser( + 'get-user-groups', help='Get all the LDAP groups for an LDAP user') + subparser.add_argument( + 'username', help='LDAP user to retrieve the groups for') + + subparser = subparsers.add_parser( + 'add-user-to-group', help='Add an LDAP user to an LDAP group') + subparser.add_argument('username', help='LDAP user to add to group') + subparser.add_argument('groupname', help='LDAP group to add the user to') + + subparser = subparsers.add_parser( + 'remove-user-from-group', + help='Remove an LDAP user from an LDAP group') + subparser.add_argument('username', help='LDAP user to remove from group') + subparser.add_argument( + 'groupname', help='LDAP group to remove the user from') + subparsers.required = True return parser.parse_args() @@ -57,10 +103,12 @@ def subcommand_setup(_): def configure_slapd(): """Configure LDAP authentication.""" action_utils.dpkg_reconfigure('slapd', {'domain': 'thisbox'}) - action_utils.dpkg_reconfigure('nslcd', {'ldap-uris': 'ldapi:///', - 'ldap-base': 'dc=thisbox', - 'ldap-auth-type': 'SASL', - 'ldap-sasl-mech': 'EXTERNAL'}) + action_utils.dpkg_reconfigure('nslcd', { + 'ldap-uris': 'ldapi:///', + 'ldap-base': 'dc=thisbox', + 'ldap-auth-type': 'SASL', + 'ldap-sasl-mech': 'EXTERNAL' + }) action_utils.dpkg_reconfigure('libnss-ldapd', {'nsswitch': 'group, passwd, shadow'}) action_utils.service_restart('nscd') @@ -82,9 +130,12 @@ def create_organizational_unit(unit): distinguished_name = 'ou={unit},dc=thisbox'.format(unit=unit) try: subprocess.run( - ['ldapsearch', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', - 'base', '-b', distinguished_name, '(objectclass=*)'], - stdout=subprocess.DEVNULL, check=True) + [ + 'ldapsearch', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', + 'base', '-b', distinguished_name, '(objectclass=*)' + ], + stdout=subprocess.DEVNULL, + check=True) return # Already exists except subprocess.CalledProcessError: input = ''' @@ -92,18 +143,23 @@ dn: ou={unit},dc=thisbox objectClass: top objectClass: organizationalUnit ou: {unit}'''.format(unit=unit) - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - input=input.encode(), stdout=subprocess.DEVNULL, - check=True) + subprocess.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], + input=input.encode(), + stdout=subprocess.DEVNULL, + check=True) def setup_admin(): """Remove LDAP admin password and Allow root to modify the users.""" process = subprocess.run( - ['ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H', - 'ldapi:///', '-s', 'base', '-b', 'olcDatabase={1}mdb,cn=config', - '(objectclass=*)', 'olcRootDN', 'olcRootPW'], - check=True, stdout=subprocess.PIPE) + [ + 'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H', + 'ldapi:///', '-s', 'base', '-b', 'olcDatabase={1}mdb,cn=config', + '(objectclass=*)', 'olcRootDN', 'olcRootPW' + ], + check=True, + stdout=subprocess.PIPE) ldap_object = {} for line in process.stdout.decode().splitlines(): if line: @@ -113,7 +169,9 @@ def setup_admin(): if 'olcRootPW' in ldap_object: subprocess.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, + stdout=subprocess.DEVNULL, + input=b''' dn: olcDatabase={1}mdb,cn=config changetype: modify delete: olcRootPW''') @@ -122,7 +180,9 @@ delete: olcRootPW''') if ldap_object['olcRootDN'] != root_dn: subprocess.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, + stdout=subprocess.DEVNULL, + input=b''' dn: olcDatabase={1}mdb,cn=config changetype: modify replace: olcRootDN @@ -132,8 +192,8 @@ olcRootDN: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth def configure_ldapscripts(): """Set the configuration used by ldapscripts for later user management.""" - aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + - augeas.Augeas.NO_MODL_AUTOLOAD) + aug = augeas.Augeas( + flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns') aug.set('/augeas/load/Shellvars/incl[last() + 1]', LDAPSCRIPTS_CONF) aug.load() @@ -149,12 +209,155 @@ def configure_ldapscripts(): aug.save() +def read_password(): + """Read the password from stdin.""" + return ''.join(sys.stdin) + + +def subcommand_create_user(arguments): + """Create an LDAP user, set password and flush cache.""" + _run(['ldapadduser', arguments.username, 'users']) + set_user_password(arguments.username, read_password()) + flush_cache() + + +def subcommand_remove_user(arguments): + """Remove an LDAP user.""" + username = arguments.username + groups = get_user_groups(username) + + for group in groups: + remove_user_from_group(username, group) + + _run(['ldapdeleteuser', username]) + flush_cache() + + +def subcommand_rename_user(arguments): + """Rename an LDAP user.""" + old_username = arguments.oldusername + new_username = arguments.newusername + groups = get_user_groups(old_username) + + for group in groups: + remove_user_from_group(old_username, group) + + _run(['ldaprenameuser', old_username, new_username]) + + for group in groups: + add_user_to_group(new_username, group) + + flush_cache() + + +def set_user_password(username, password): + """Set a user's password.""" + process = _run( + ['slappasswd', '-s', password], stdout=subprocess.PIPE) + password = process.stdout.decode().strip() + _run(['ldapsetpasswd', username, password]) + + +def subcommand_set_user_password(arguments): + """Set a user's password.""" + set_user_password(arguments.username, read_password()) + + +def get_user_groups(username): + """Returns only the supplementary groups of the given user and doesn't include + the 'users' primary group""" + process = _run( + ['ldapid', username], stdout=subprocess.PIPE, check=False) + output = process.stdout.decode('utf-8').strip() + if output: + groups_part = output.split(' ')[2] + groups = groups_part.split('=')[1] + group_names = [user.strip('()') + for user in re.findall('\(.*?\)', groups)] + group_names.remove('users') + return group_names + return [] + + +def subcommand_get_user_groups(arguments): + """Return list of a given user's groups.""" + groups = [group + for group in get_user_groups(arguments.username)] + if groups: + print(*groups, sep='\n') + + +def group_exists(groupname): + """Return whether a group already exits.""" + process = _run(['ldapgid', groupname], check=False) + return process.returncode == 0 + + +def create_group(groupname): + """Add an LDAP group.""" + if not group_exists(groupname): + _run(['ldapaddgroup', groupname]) + + +def subcommand_create_group(arguments): + """Add an LDAP group.""" + create_group(arguments.groupname) + flush_cache() + + +def subcommand_remove_group(arguments): + """Remove an LDAP group.""" + if group_exists(arguments.groupname): + _run(['ldapdeletegroup', arguments.groupname]) + + flush_cache() + + +def add_user_to_group(username, groupname): + """Add an LDAP user to an LDAP group.""" + create_group(groupname) + _run(['ldapaddusertogroup', username, groupname]) + + +def subcommand_add_user_to_group(arguments): + """Add an LDAP user to an LDAP group.""" + add_user_to_group(arguments.username, arguments.groupname) + flush_cache() + + +def remove_user_from_group(username, groupname): + """Remove an LDAP user from an LDAP group.""" + _run( + ['ldapdeleteuserfromgroup', username, groupname]) + + +def subcommand_remove_user_from_group(arguments): + """Remove an LDAP user from an LDAP group.""" + remove_user_from_group(arguments.username, arguments.groupname) + flush_cache() + + +def flush_cache(): + """Flush nscd cache.""" + _run(['nscd', '--invalidate=passwd']) + _run(['nscd', '--invalidate=group']) + + +def _run(arguments, **kwargs): + """Run a command. Check return code and suppress output by default.""" + kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL) + kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL) + kwargs['check'] = kwargs.get('check', True) + return subprocess.run(arguments, **kwargs) + + def main(): """Parse arguments and perform all duties""" arguments = parse_arguments() subcommand = arguments.subcommand.replace('-', '_') subcommand_method = globals()['subcommand_' + subcommand] + subcommand_method(arguments) diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py index 30e2212dc..ad5d9715d 100644 --- a/plinth/modules/users/__init__.py +++ b/plinth/modules/users/__init__.py @@ -87,8 +87,8 @@ def _diagnose_ldap_entry(search_item): def add_group(group): - actions.superuser_run("ldap", options=["add-group", group]) + actions.superuser_run("users", options=["add-group", group]) def remove_group(group): - actions.superuser_run("ldap", options=["remove-group", group]) + actions.superuser_run("users", options=["remove-group", group]) diff --git a/plinth/modules/users/forms.py b/plinth/modules/users/forms.py index 936090944..c877f1a09 100644 --- a/plinth/modules/users/forms.py +++ b/plinth/modules/users/forms.py @@ -102,7 +102,7 @@ class CreateUserForm(ValidNewUsernameCheckMixin, UserCreationForm): if commit: try: actions.superuser_run( - 'ldap', + 'users', ['create-user', user.get_username()], input=self.cleaned_data['password1'].encode()) except ActionError: @@ -112,7 +112,7 @@ class CreateUserForm(ValidNewUsernameCheckMixin, UserCreationForm): for group in self.cleaned_data['groups']: try: actions.superuser_run( - 'ldap', + 'users', ['add-user-to-group', user.get_username(), group]) except ActionError: messages.error( @@ -167,14 +167,14 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, forms.ModelForm): if commit: output = actions.superuser_run( - 'ldap', ['get-user-groups', self.username]) + 'users', ['get-user-groups', self.username]) old_groups = output.strip().split('\n') old_groups = [group for group in old_groups if group] if self.username != user.get_username(): try: actions.superuser_run( - 'ldap', + 'users', ['rename-user', self.username, user.get_username()]) except ActionError: messages.error(self.request, @@ -185,7 +185,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, forms.ModelForm): if old_group not in new_groups: try: actions.superuser_run( - 'ldap', + 'users', ['remove-user-from-group', user.get_username(), old_group]) except ActionError: @@ -196,7 +196,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, forms.ModelForm): if new_group not in old_groups: try: actions.superuser_run( - 'ldap', + 'users', ['add-user-to-group', user.get_username(), new_group]) except ActionError: @@ -227,7 +227,7 @@ class UserChangePasswordForm(SetPasswordForm): if commit: try: actions.superuser_run( - 'ldap', + 'users', ['set-user-password', user.get_username()], input=self.cleaned_data['new_password1'].encode()) except ActionError: @@ -253,7 +253,7 @@ class FirstBootForm(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm): try: actions.superuser_run( - 'ldap', + 'users', ['create-user', user.get_username()], input=self.cleaned_data['password1'].encode()) except ActionError: @@ -262,7 +262,7 @@ class FirstBootForm(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm): try: actions.superuser_run( - 'ldap', + 'users', ['add-user-to-group', user.get_username(), 'admin']) except ActionError: messages.error(self.request, diff --git a/plinth/modules/users/tests/test_actions.py b/plinth/modules/users/tests/test_actions.py new file mode 100644 index 000000000..e75f1f92f --- /dev/null +++ b/plinth/modules/users/tests/test_actions.py @@ -0,0 +1,293 @@ +#!/usr/bin/python3 +# +# This file is part of Plinth. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Test module to exercise user actions. + +it is recommended to run this module with root privileges in a virtual machine. +""" + +import os +import random +import string +import subprocess +import unittest + +from plinth import action_utils + +euid = os.geteuid() + + +def random_string(length=8): + """Return a random string created from lower case ascii.""" + return ''.join( + [random.choice(string.ascii_lowercase) for _ in range(length)]) + + +def is_exit_zero(args): + """Return whether a command gave exit code zero""" + process = subprocess.run( + args, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False) + return process.returncode == 0 + + +def get_password_hash(username): + """Query and return the password hash of the given LDAP username""" + query = [ + 'ldapsearch', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H', 'ldapi:///', + '-b', 'ou=users,dc=thisbox', '-Q', '(cn={})'.format(username), + 'userPassword' + ] + process = subprocess.run( + query, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True) + return process.stdout.decode().strip().split()[-1] + + +def try_login_to_ssh(username, password, returncode=0): + """Return whether the sshpass returncode matches when trying to + login to ssh using the given username and password""" + if not action_utils.service_is_running('ssh'): + return True + + command = [ + 'sshpass', '-p', password, 'ssh', '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'StrictHostKeyChecking=no', '-o', 'VerifyHostKeyDNS=no', + username + '@127.0.0.1', '/bin/true' + ] + process = subprocess.run( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False) + return process.returncode == returncode + + +class TestActions(unittest.TestCase): + """Test user related actions.""" + + def setUp(self): + """Setup each .""" + current_directory = os.path.dirname(__file__) + self.action_file = os.path.join(current_directory, '..', '..', '..', + '..', 'actions', 'users') + self.users = set() + self.groups = set() + + def tearDown(self): + for user in self.users: + try: + self.delete_user(user) + except Exception: + pass + for group in self.groups: + self.delete_group(group) + + def call_action(self, arguments, **kwargs): + """Call the action script.""" + kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL) + kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL) + kwargs['check'] = kwargs.get('check', True) + return subprocess.run([self.action_file] + arguments, **kwargs) + + def create_user(self, username=None, groups=None): + """Call the action script for creating a new user.""" + username = username or random_string() + password = random_string() + + self.call_action(['create-user', username], input=password.encode()) + + if groups: + for group in groups: + self.call_action(['add-user-to-group', username, group]) + self.groups.add(group) + + self.users.add(username) + return username, password + + def delete_user(self, username): + """Utility to delete an LDAP user""" + self.call_action(['remove-user', username]) + + def rename_user(self, old_username, new_username=None): + """Rename a user.""" + new_username = new_username or random_string() + self.call_action(['rename-user', old_username, new_username]) + self.users.remove(old_username) + self.users.add(new_username) + return new_username + + def get_user_groups(self, username): + """Return the list of groups for a user.""" + process = self.call_action( + ['get-user-groups', username], stdout=subprocess.PIPE) + return process.stdout.split() + + def create_group(self, groupname=None): + groupname = groupname or random_string() + self.call_action(['create-group', groupname]) + self.groups.add(groupname) + return groupname + + def delete_group(self, groupname): + self.call_action(['remove-group', groupname]) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_create_user(self): + """Test whether creating a new user works.""" + username, password = self.create_user(groups=[random_string()]) + # assert_can_login_to_console(username, password) + self.assertTrue(try_login_to_ssh(username, password)) + with self.assertRaises(subprocess.CalledProcessError): + self.create_user(username) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_change_user_password(self): + username, old_password = self.create_user() + old_password_hash = get_password_hash(username) + new_password = 'pass $123' + self.call_action( + ['set-user-password', username], input=new_password.encode()) + new_password_hash = get_password_hash(username) + self.assertNotEqual(old_password_hash, new_password_hash) + + # User can login to ssh using new password but not the old password. + # sshpass gives a return code of 5 if the password is incorrect. + self.assertTrue(try_login_to_ssh(username, old_password, returncode=5)) + self.assertTrue(try_login_to_ssh(username, new_password)) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_set_password_for_non_existent_user(self): + non_existent_user = random_string() + fake_password = random_string().encode() + with self.assertRaises(subprocess.CalledProcessError): + self.call_action( + ['set-user-password', non_existent_user], input=fake_password) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_rename_user(self): + """Test whether renaming a user works.""" + old_username, password = self.create_user(groups=[random_string()]) + old_groups = self.get_user_groups(old_username) + + new_username = self.rename_user(old_username) + self.assertTrue(try_login_to_ssh(new_username, password)) + self.assertTrue(try_login_to_ssh(old_username, password, returncode=5)) + + new_groups = self.get_user_groups(new_username) + old_users_groups = self.get_user_groups(old_username) + self.assertFalse(len(old_users_groups)) + self.assertEqual(old_groups, new_groups) + + self.assertFalse(self.get_user_groups(old_username)) # is empty + + with self.assertRaises(subprocess.CalledProcessError): + self.rename_user(old_username) + + # Renaming a non-existent user fails + random_username = random_string() + with self.assertRaises(subprocess.CalledProcessError): + self.rename_user(random_username, new_username=random_string()) + + # Renaming to an existing user fails + existing_user, _ = self.create_user() + with self.assertRaises(subprocess.CalledProcessError): + self.rename_user(existing_user, new_username=new_username) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_delete_user(self): + """Test to check whether LDAP users can be deleted""" + username, password = self.create_user(groups=[random_string()]) + groups_before = self.get_user_groups(username) + self.assertTrue(groups_before) + self.delete_user(username) + groups_after = self.get_user_groups(username) + self.assertFalse(groups_after) # User gets removed from all groups + + # User account cannot be found after deletion + self.assertFalse(is_exit_zero(['ldapid', username])) + + # Deleted user cannot login to ssh + self.assertTrue(try_login_to_ssh(username, password, returncode=5)) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_delete_non_existent_user(self): + """Deleting a non-existent user should fail.""" + non_existent_user = random_string() + with self.assertRaises(subprocess.CalledProcessError): + self.call_action(['delete-user', non_existent_user]) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_groups(self): + """Test to check that LDAP groups can be deleted""" + groupname = random_string() + + self.create_group(groupname) + self.assertTrue(is_exit_zero(['ldapgid', groupname])) + + # create-group is idempotent + self.assertTrue( + is_exit_zero([self.action_file, 'create-group', groupname])) + + self.delete_group(groupname) + self.assertFalse(is_exit_zero(['ldapgid', groupname])) + + # delete-group is idempotent + self.assertTrue( + is_exit_zero([self.action_file, 'remove-group', groupname])) + + @unittest.skipUnless(euid == 0, 'Needs to be root') + def test_user_group_interactions(self): + group1 = random_string() + user1, _ = self.create_user(groups=[group1]) + + # add-user-to-group is not idempotent + with self.assertRaises(subprocess.CalledProcessError): + self.call_action(['add-user-to-group', user1, group1]) + + # The same user can be added to other new groups + group2 = random_string() + self.create_group(group2) + self.call_action(['add-user-to-group', user1, group2]) + + # Adding a user to a non-existent group creates the group + group3 = random_string() + self.call_action(['add-user-to-group', user1, group3]) + self.groups.add(group3) + + # The expected groups got created and the user is part of them. + expected_groups = [group1, group2, group3] + actual_groups = [g.decode() for g in self.get_user_groups(user1)] + self.assertEqual(expected_groups, actual_groups) + + # Remove user from group + group_to_remove_from = random.choice(expected_groups) + self.call_action( + ['remove-user-from-group', user1, group_to_remove_from]) + + # User is no longer in the group that they're removed from + expected_groups.remove(group_to_remove_from) + actual_groups = [g.decode() for g in self.get_user_groups(user1)] + self.assertEqual(expected_groups, actual_groups) + + # User cannot be removed from a group that they're not part of + random_group = random_string() + self.create_group(random_group) + with self.assertRaises(subprocess.CalledProcessError): + self.call_action(['remove-user-from-group', user1, random_group]) diff --git a/plinth/modules/users/views.py b/plinth/modules/users/views.py index 3f0f461eb..bd9bf1644 100644 --- a/plinth/modules/users/views.py +++ b/plinth/modules/users/views.py @@ -138,7 +138,7 @@ class UserDelete(ContextMixin, DeleteView): messages.success(self.request, message) try: - actions.superuser_run('ldap', ['delete-user', self.kwargs['slug']]) + actions.superuser_run('users', ['remove-user', self.kwargs['slug']]) except ActionError: messages.error(self.request, _('Deleting LDAP user failed.'))