mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
users: Use privileged decorator for actions
Tests: - Functional tests work (failing already) - DONE: Showing front page shortcuts according to user groups works - DONE: Only user who is party of syncthing group is shown syncthing - DONE: Admin users are always shown all the apps - DONE: Syncthing: - Not tested: When upgrading from version 2 or below, renaming group works - DONE: Syncthing is added to freedombox-share group - DONE: Initial setup of users app works - DONE: freedombox-share group is created - DONE: Retriving last admin user works - DONE: Last admin is not allowed to delete account - DONE: Creating a new user works - DONE: Password is set properly (user can login with 'su - user' after) - DONE: Incorrect confirmation password leads to error - DONE: Adding the user to groups works (edit page shows correct list of groups) - DONE: Editing a user works - DONE: User is renamed properly - DONE: Removing user from groups works - DONE: Adding user to new groups works - DONE: Providing incorrect auth password results in error message - DONE: Enabling/disabling account work (confirm with 'su - user'). See #2277. - DONE: Updating user password works - DONE: New password is set (confirm with 'su - user') - DONE: Providing incorrect auth password results in error message - DONE: Initial user account creation works - DONE: User account can be used (confirm with 'su - user') - DONE: User is added to admin group - DONE: Exception while getting SSH keys results in showing empty field - DONE: Removing a user works - DONE: Command provided in a message in users_firstboot.html works for deleting users. - DONE: If an admin users exists when running first wizard, list of admin users is shown. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
e3d0be2885
commit
1dcbfce713
@ -1,15 +1,12 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""Manage application shortcuts on front page."""
|
||||||
Manage application shortcuts on front page.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from plinth import app, cfg
|
from plinth import app, cfg
|
||||||
|
from plinth.modules.users import privileged as users_privileged
|
||||||
from . import actions
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -116,8 +113,7 @@ class Shortcut(app.FollowerComponent):
|
|||||||
return cls._all_shortcuts
|
return cls._all_shortcuts
|
||||||
|
|
||||||
# XXX: Turn this into an API call in users module and cache
|
# XXX: Turn this into an API call in users module and cache
|
||||||
output = actions.superuser_run('users', ['get-user-groups', username])
|
user_groups = set(users_privileged.get_user_groups(username))
|
||||||
user_groups = set(output.strip().split('\n'))
|
|
||||||
|
|
||||||
if 'admin' in user_groups: # Admin has access to all services
|
if 'admin' in user_groups: # Admin has access to all services
|
||||||
return cls._all_shortcuts
|
return cls._all_shortcuts
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from plinth.modules.apache.components import Webserver
|
|||||||
from plinth.modules.backups.components import BackupRestore
|
from plinth.modules.backups.components import BackupRestore
|
||||||
from plinth.modules.firewall.components import Firewall
|
from plinth.modules.firewall.components import Firewall
|
||||||
from plinth.modules.users import add_user_to_share_group
|
from plinth.modules.users import add_user_to_share_group
|
||||||
|
from plinth.modules.users import privileged as users_privileged
|
||||||
from plinth.modules.users.components import UsersAndGroups
|
from plinth.modules.users.components import UsersAndGroups
|
||||||
from plinth.package import Packages
|
from plinth.package import Packages
|
||||||
from plinth.utils import format_lazy
|
from plinth.utils import format_lazy
|
||||||
@ -119,9 +120,7 @@ class SyncthingApp(app_module.App):
|
|||||||
old_groupname = 'syncthing'
|
old_groupname = 'syncthing'
|
||||||
new_groupname = 'syncthing-access'
|
new_groupname = 'syncthing-access'
|
||||||
|
|
||||||
actions.superuser_run(
|
users_privileged.rename_group(old_groupname, new_groupname)
|
||||||
'users',
|
|
||||||
options=['rename-group', old_groupname, new_groupname])
|
|
||||||
|
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
Group.objects.filter(name=old_groupname).update(name=new_groupname)
|
Group.objects.filter(name=old_groupname).update(name=new_groupname)
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""FreedomBox app to manage users."""
|
||||||
FreedomBox app to manage users.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import grp
|
import grp
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -15,6 +13,7 @@ from plinth import cfg, menu
|
|||||||
from plinth.daemon import Daemon
|
from plinth.daemon import Daemon
|
||||||
from plinth.package import Packages
|
from plinth.package import Packages
|
||||||
|
|
||||||
|
from . import privileged
|
||||||
from .components import UsersAndGroups
|
from .components import UsersAndGroups
|
||||||
|
|
||||||
first_boot_steps = [
|
first_boot_steps = [
|
||||||
@ -91,10 +90,10 @@ class UsersApp(app_module.App):
|
|||||||
"""Install and configure the app."""
|
"""Install and configure the app."""
|
||||||
super().setup(old_version)
|
super().setup(old_version)
|
||||||
if not old_version:
|
if not old_version:
|
||||||
actions.superuser_run('users', ['first-setup'])
|
privileged.first_setup()
|
||||||
|
|
||||||
actions.superuser_run('users', ['setup'])
|
privileged.setup()
|
||||||
create_group('freedombox-share')
|
privileged.create_group('freedombox-share')
|
||||||
|
|
||||||
|
|
||||||
def _diagnose_ldap_entry(search_item):
|
def _diagnose_ldap_entry(search_item):
|
||||||
@ -114,21 +113,9 @@ def _diagnose_ldap_entry(search_item):
|
|||||||
return [testname, result]
|
return [testname, result]
|
||||||
|
|
||||||
|
|
||||||
def create_group(group):
|
|
||||||
"""Add an LDAP group."""
|
|
||||||
actions.superuser_run('users', options=['create-group', group])
|
|
||||||
|
|
||||||
|
|
||||||
def remove_group(group):
|
|
||||||
"""Remove an LDAP group."""
|
|
||||||
actions.superuser_run('users', options=['remove-group', group])
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_admin_user():
|
def get_last_admin_user():
|
||||||
"""If there is only one admin user return its name else return None."""
|
"""If there is only one admin user return its name else return None."""
|
||||||
output = actions.superuser_run('users', ['get-group-users', 'admin'])
|
admin_users = privileged.get_group_users('admin')
|
||||||
admin_users = output.strip().split('\n')
|
|
||||||
|
|
||||||
if len(admin_users) == 1 and admin_users[0]:
|
if len(admin_users) == 1 and admin_users[0]:
|
||||||
return admin_users[0]
|
return admin_users[0]
|
||||||
|
|
||||||
@ -142,7 +129,6 @@ def add_user_to_share_group(username, service=None):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
group_members = []
|
group_members = []
|
||||||
if username not in group_members:
|
if username not in group_members:
|
||||||
actions.superuser_run(
|
privileged.add_user_to_group(username, 'freedombox-share')
|
||||||
'users', ['add-user-to-group', username, 'freedombox-share'])
|
|
||||||
if service:
|
if service:
|
||||||
actions.superuser_run('service', ['try-restart', service])
|
actions.superuser_run('service', ['try-restart', service])
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Django forms for user management."""
|
||||||
|
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
@ -16,13 +17,11 @@ from django.utils.translation import gettext_lazy
|
|||||||
|
|
||||||
import plinth.forms
|
import plinth.forms
|
||||||
import plinth.modules.ssh.privileged as ssh_privileged
|
import plinth.modules.ssh.privileged as ssh_privileged
|
||||||
from plinth import actions
|
|
||||||
from plinth.errors import ActionError
|
|
||||||
from plinth.modules import first_boot
|
from plinth.modules import first_boot
|
||||||
from plinth.modules.security import set_restricted_access
|
from plinth.modules.security import set_restricted_access
|
||||||
from plinth.utils import is_user_admin
|
from plinth.utils import is_user_admin
|
||||||
|
|
||||||
from . import get_last_admin_user
|
from . import get_last_admin_user, privileged
|
||||||
from .components import UsersAndGroups
|
from .components import UsersAndGroups
|
||||||
|
|
||||||
|
|
||||||
@ -74,11 +73,13 @@ USERNAME_FIELD = forms.CharField(
|
|||||||
|
|
||||||
class PasswordConfirmForm(forms.Form):
|
class PasswordConfirmForm(forms.Form):
|
||||||
"""Password confirmation form."""
|
"""Password confirmation form."""
|
||||||
|
|
||||||
confirm_password = forms.CharField(
|
confirm_password = forms.CharField(
|
||||||
widget=forms.PasswordInput,
|
widget=forms.PasswordInput,
|
||||||
label=gettext_lazy('Authorization Password'))
|
label=gettext_lazy('Authorization Password'))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize form."""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['confirm_password'].help_text = _(
|
self.fields['confirm_password'].help_text = _(
|
||||||
@ -103,6 +104,7 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
|
|||||||
|
|
||||||
Include options to add user to groups.
|
Include options to add user to groups.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
username = USERNAME_FIELD
|
username = USERNAME_FIELD
|
||||||
groups = forms.MultipleChoiceField(
|
groups = forms.MultipleChoiceField(
|
||||||
choices=UsersAndGroups.get_group_choices,
|
choices=UsersAndGroups.get_group_choices,
|
||||||
@ -120,6 +122,7 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
|
|||||||
|
|
||||||
class Meta(UserCreationForm.Meta):
|
class Meta(UserCreationForm.Meta):
|
||||||
"""Metadata to control automatic form building."""
|
"""Metadata to control automatic form building."""
|
||||||
|
|
||||||
fields = ('username', 'password1', 'password2', 'groups', 'language',
|
fields = ('username', 'password1', 'password2', 'groups', 'language',
|
||||||
'confirm_password')
|
'confirm_password')
|
||||||
|
|
||||||
@ -143,14 +146,11 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
|
|||||||
auth_username = self.request.user.username
|
auth_username = self.request.user.username
|
||||||
confirm_password = self.cleaned_data['confirm_password']
|
confirm_password = self.cleaned_data['confirm_password']
|
||||||
|
|
||||||
process_input = '{0}\n{1}'.format(self.cleaned_data['password1'],
|
|
||||||
confirm_password).encode()
|
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users', [
|
privileged.create_user(user.get_username(),
|
||||||
'create-user',
|
self.cleaned_data['password1'],
|
||||||
user.get_username(), '--auth-user', auth_username
|
auth_username, confirm_password)
|
||||||
], input=process_input)
|
except Exception as error:
|
||||||
except ActionError as error:
|
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_('Creating LDAP user failed: {error}'.format(
|
_('Creating LDAP user failed: {error}'.format(
|
||||||
@ -158,12 +158,10 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
|
|||||||
|
|
||||||
for group in self.cleaned_data['groups']:
|
for group in self.cleaned_data['groups']:
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users', [
|
privileged.add_user_to_group(user.get_username(), group,
|
||||||
'add-user-to-group',
|
auth_username,
|
||||||
user.get_username(), group, '--auth-user',
|
confirm_password)
|
||||||
auth_username
|
except Exception as error:
|
||||||
], input=confirm_password.encode())
|
|
||||||
except ActionError as error:
|
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_('Failed to add new user to {group} group: {error}').
|
_('Failed to add new user to {group} group: {error}').
|
||||||
@ -178,6 +176,7 @@ class CreateUserForm(ValidNewUsernameCheckMixin,
|
|||||||
class UserUpdateForm(ValidNewUsernameCheckMixin, PasswordConfirmForm,
|
class UserUpdateForm(ValidNewUsernameCheckMixin, PasswordConfirmForm,
|
||||||
plinth.forms.LanguageSelectionFormMixin, forms.ModelForm):
|
plinth.forms.LanguageSelectionFormMixin, forms.ModelForm):
|
||||||
"""When user info is changed, also updates LDAP user."""
|
"""When user info is changed, also updates LDAP user."""
|
||||||
|
|
||||||
username = USERNAME_FIELD
|
username = USERNAME_FIELD
|
||||||
ssh_keys = forms.CharField(
|
ssh_keys = forms.CharField(
|
||||||
label=gettext_lazy('Authorized SSH Keys'), required=False,
|
label=gettext_lazy('Authorized SSH Keys'), required=False,
|
||||||
@ -192,6 +191,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, PasswordConfirmForm,
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metadata to control automatic form building."""
|
"""Metadata to control automatic form building."""
|
||||||
|
|
||||||
fields = ('username', 'groups', 'ssh_keys', 'language', 'is_active',
|
fields = ('username', 'groups', 'ssh_keys', 'language', 'is_active',
|
||||||
'confirm_password')
|
'confirm_password')
|
||||||
model = User
|
model = User
|
||||||
@ -254,18 +254,13 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, PasswordConfirmForm,
|
|||||||
user.save()
|
user.save()
|
||||||
self.save_m2m()
|
self.save_m2m()
|
||||||
|
|
||||||
output = actions.superuser_run('users',
|
old_groups = privileged.get_user_groups(self.username)
|
||||||
['get-user-groups', self.username])
|
|
||||||
old_groups = output.strip().split('\n')
|
|
||||||
old_groups = [group for group in old_groups if group]
|
old_groups = [group for group in old_groups if group]
|
||||||
|
|
||||||
if self.username != user.get_username():
|
if self.username != user.get_username():
|
||||||
try:
|
try:
|
||||||
actions.superuser_run(
|
privileged.rename_user(self.username, user.get_username())
|
||||||
'users',
|
except Exception:
|
||||||
['rename-user', self.username,
|
|
||||||
user.get_username()])
|
|
||||||
except ActionError:
|
|
||||||
messages.error(self.request,
|
messages.error(self.request,
|
||||||
_('Renaming LDAP user failed.'))
|
_('Renaming LDAP user failed.'))
|
||||||
|
|
||||||
@ -273,24 +268,20 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, PasswordConfirmForm,
|
|||||||
for old_group in old_groups:
|
for old_group in old_groups:
|
||||||
if old_group not in new_groups:
|
if old_group not in new_groups:
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users', [
|
privileged.remove_user_from_group(
|
||||||
'remove-user-from-group',
|
user.get_username(), old_group, auth_username,
|
||||||
user.get_username(), old_group, '--auth-user',
|
confirm_password)
|
||||||
auth_username
|
except Exception:
|
||||||
], input=confirm_password.encode())
|
|
||||||
except ActionError:
|
|
||||||
messages.error(self.request,
|
messages.error(self.request,
|
||||||
_('Failed to remove user from group.'))
|
_('Failed to remove user from group.'))
|
||||||
|
|
||||||
for new_group in new_groups:
|
for new_group in new_groups:
|
||||||
if new_group not in old_groups:
|
if new_group not in old_groups:
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users', [
|
privileged.add_user_to_group(user.get_username(),
|
||||||
'add-user-to-group',
|
new_group, auth_username,
|
||||||
user.get_username(), new_group, '--auth-user',
|
confirm_password)
|
||||||
auth_username
|
except Exception:
|
||||||
], input=confirm_password.encode())
|
|
||||||
except ActionError:
|
|
||||||
messages.error(self.request,
|
messages.error(self.request,
|
||||||
_('Failed to add user to group.'))
|
_('Failed to add user to group.'))
|
||||||
|
|
||||||
@ -308,14 +299,9 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, PasswordConfirmForm,
|
|||||||
else:
|
else:
|
||||||
status = 'inactive'
|
status = 'inactive'
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users', [
|
privileged.set_user_status(user.get_username(), status,
|
||||||
'set-user-status',
|
auth_username, confirm_password)
|
||||||
user.get_username(),
|
except Exception:
|
||||||
status,
|
|
||||||
'--auth-user',
|
|
||||||
auth_username,
|
|
||||||
], input=confirm_password.encode())
|
|
||||||
except ActionError:
|
|
||||||
messages.error(self.request,
|
messages.error(self.request,
|
||||||
_('Failed to change user status.'))
|
_('Failed to change user status.'))
|
||||||
|
|
||||||
@ -352,15 +338,11 @@ class UserChangePasswordForm(PasswordConfirmForm, SetPasswordForm):
|
|||||||
user = super().save(commit)
|
user = super().save(commit)
|
||||||
auth_username = self.request.user.username
|
auth_username = self.request.user.username
|
||||||
if commit:
|
if commit:
|
||||||
process_input = '{0}\n{1}'.format(
|
|
||||||
self.cleaned_data['new_password1'],
|
|
||||||
self.cleaned_data['confirm_password']).encode()
|
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users', [
|
privileged.set_user_password(
|
||||||
'set-user-password',
|
user.get_username(), self.cleaned_data['new_password1'],
|
||||||
user.get_username(), '--auth-user', auth_username
|
auth_username, self.cleaned_data['confirm_password'])
|
||||||
], input=process_input)
|
except Exception:
|
||||||
except ActionError:
|
|
||||||
messages.error(self.request,
|
messages.error(self.request,
|
||||||
_('Changing LDAP user password failed.'))
|
_('Changing LDAP user password failed.'))
|
||||||
|
|
||||||
@ -369,6 +351,7 @@ class UserChangePasswordForm(PasswordConfirmForm, SetPasswordForm):
|
|||||||
|
|
||||||
class FirstBootForm(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm):
|
class FirstBootForm(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm):
|
||||||
"""User module first boot step: create a new admin user."""
|
"""User module first boot step: create a new admin user."""
|
||||||
|
|
||||||
username = USERNAME_FIELD
|
username = USERNAME_FIELD
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -383,23 +366,17 @@ class FirstBootForm(ValidNewUsernameCheckMixin, auth.forms.UserCreationForm):
|
|||||||
first_boot.mark_step_done('users_firstboot')
|
first_boot.mark_step_done('users_firstboot')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actions.superuser_run(
|
privileged.create_user(user.get_username(),
|
||||||
'users',
|
self.cleaned_data['password1'])
|
||||||
['create-user',
|
except Exception as error:
|
||||||
user.get_username(), '--auth-user', ''],
|
|
||||||
input=self.cleaned_data['password1'].encode())
|
|
||||||
except ActionError as error:
|
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_('Creating LDAP user failed: {error}'.format(
|
_('Creating LDAP user failed: {error}'.format(
|
||||||
error=error)))
|
error=error)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actions.superuser_run(
|
privileged.add_user_to_group(user.get_username(), 'admin')
|
||||||
'users',
|
except Exception as error:
|
||||||
['add-user-to-group',
|
|
||||||
user.get_username(), 'admin'])
|
|
||||||
except ActionError as error:
|
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_('Failed to add new user to admin group: {error}'.format(
|
_('Failed to add new user to admin group: {error}'.format(
|
||||||
|
|||||||
367
actions/users → plinth/modules/users/privileged.py
Executable file → Normal file
367
actions/users → plinth/modules/users/privileged.py
Executable file → Normal file
@ -1,142 +1,53 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""Configuration helper for the LDAP user directory."""
|
||||||
Configuration helper for the LDAP user directory
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
from typing import Optional
|
||||||
|
|
||||||
import augeas
|
import augeas
|
||||||
|
|
||||||
from plinth import action_utils, utils
|
from plinth import action_utils, utils
|
||||||
|
from plinth.actions import privileged
|
||||||
|
|
||||||
INPUT_LINES = None
|
INPUT_LINES = None
|
||||||
ACCESS_CONF = '/etc/security/access.conf'
|
ACCESS_CONF = '/etc/security/access.conf'
|
||||||
LDAPSCRIPTS_CONF = '/etc/ldapscripts/freedombox-ldapscripts.conf'
|
LDAPSCRIPTS_CONF = '/etc/ldapscripts/freedombox-ldapscripts.conf'
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
def _validate_user(username, password, must_be_admin=True):
|
||||||
"""Return parsed command line arguments as dictionary"""
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
|
||||||
|
|
||||||
subparsers.add_parser('first-setup', help='Perform initial setup of LDAP')
|
|
||||||
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.add_argument('--auth-user', required=True)
|
|
||||||
|
|
||||||
subparser = subparsers.add_parser('remove-user',
|
|
||||||
help='Delete an LDAP user')
|
|
||||||
subparser.add_argument(
|
|
||||||
'username', help='Name of the LDAP user to delete. If the username is '
|
|
||||||
'the last admin user, a password should be provided through STDIN.')
|
|
||||||
|
|
||||||
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.add_argument('--auth-user', required=True)
|
|
||||||
|
|
||||||
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('rename-group',
|
|
||||||
help='Rename an LDAP group')
|
|
||||||
subparser.add_argument('old_groupname',
|
|
||||||
help='Name of the LDAP group to rename')
|
|
||||||
subparser.add_argument('new_groupname', help='Name of the new LDAP group')
|
|
||||||
|
|
||||||
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.add_argument('--auth-user', required=False)
|
|
||||||
|
|
||||||
subparser = subparsers.add_parser('set-user-status',
|
|
||||||
help='Set user as active or inactive')
|
|
||||||
subparser.add_argument('username', help='User to change status')
|
|
||||||
subparser.add_argument('status', choices=['active', 'inactive'],
|
|
||||||
help='New status of the user')
|
|
||||||
subparser.add_argument('--auth-user', required=True)
|
|
||||||
|
|
||||||
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')
|
|
||||||
subparser.add_argument('--auth-user', required=False)
|
|
||||||
|
|
||||||
help_get_group_users = 'Get the list of all users in an LDAP group'
|
|
||||||
subparser = subparsers.add_parser('get-group-users',
|
|
||||||
help=help_get_group_users)
|
|
||||||
subparser.add_argument(
|
|
||||||
'groupname', help='name of the LDAP group to get the '
|
|
||||||
'list of users')
|
|
||||||
|
|
||||||
subparsers.required = True
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def validate_user(username, must_be_admin=True):
|
|
||||||
"""Validate a user."""
|
"""Validate a user."""
|
||||||
if must_be_admin:
|
if must_be_admin:
|
||||||
admins = get_admin_users()
|
admins = _get_admin_users()
|
||||||
|
|
||||||
if not admins:
|
if not admins:
|
||||||
# any user is valid
|
# any user is valid
|
||||||
return
|
return
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
msg = 'Argument --auth-user is required'
|
raise PermissionError('Authentication user is required')
|
||||||
raise argparse.ArgumentTypeError(msg)
|
|
||||||
|
|
||||||
if username not in admins:
|
if username not in admins:
|
||||||
msg = '"{}" is not authorized to perform this action'.format(
|
msg = f'"{username}" is not authorized to perform this action'
|
||||||
username)
|
raise PermissionError(msg)
|
||||||
raise argparse.ArgumentTypeError(msg)
|
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
msg = 'Argument --auth-user is required'
|
raise PermissionError('Authentication user is required')
|
||||||
raise argparse.ArgumentTypeError(msg)
|
|
||||||
|
|
||||||
validate_password(username)
|
_validate_password(username, password)
|
||||||
|
|
||||||
|
|
||||||
def validate_password(username):
|
def _validate_password(username, password):
|
||||||
"""Raise an error if the user password is invalid."""
|
"""Raise an error if the user password is invalid."""
|
||||||
password = read_password(last=True)
|
|
||||||
if not utils.is_authenticated_user(username, password):
|
if not utils.is_authenticated_user(username, password):
|
||||||
raise argparse.ArgumentTypeError("Invalid credentials")
|
raise PermissionError('Invalid credentials')
|
||||||
|
|
||||||
|
|
||||||
def subcommand_first_setup(_):
|
@privileged
|
||||||
|
def first_setup():
|
||||||
"""Perform initial setup of LDAP."""
|
"""Perform initial setup of LDAP."""
|
||||||
# Avoid reconfiguration of slapd during module upgrades, because
|
# Avoid reconfiguration of slapd during module upgrades, because
|
||||||
# this will move the existing database.
|
# this will move the existing database.
|
||||||
@ -147,19 +58,20 @@ def subcommand_first_setup(_):
|
|||||||
action_utils.dpkg_reconfigure('slapd', {'domain': 'thisbox'})
|
action_utils.dpkg_reconfigure('slapd', {'domain': 'thisbox'})
|
||||||
|
|
||||||
|
|
||||||
def subcommand_setup(_):
|
@privileged
|
||||||
|
def setup():
|
||||||
"""Setup LDAP."""
|
"""Setup LDAP."""
|
||||||
# Update pam configs for access and mkhomedir.
|
# Update pam configs for access and mkhomedir.
|
||||||
subprocess.run(['pam-auth-update', '--package'], check=True)
|
subprocess.run(['pam-auth-update', '--package'], check=True)
|
||||||
|
|
||||||
configure_ldapscripts()
|
_configure_ldapscripts()
|
||||||
|
|
||||||
configure_ldap_authentication()
|
_configure_ldap_authentication()
|
||||||
|
|
||||||
configure_ldap_structure()
|
_configure_ldap_structure()
|
||||||
|
|
||||||
|
|
||||||
def configure_ldap_authentication():
|
def _configure_ldap_authentication():
|
||||||
"""Configure LDAP authentication."""
|
"""Configure LDAP authentication."""
|
||||||
action_utils.dpkg_reconfigure(
|
action_utils.dpkg_reconfigure(
|
||||||
'nslcd', {
|
'nslcd', {
|
||||||
@ -179,18 +91,18 @@ def configure_ldap_authentication():
|
|||||||
action_utils.service_start('nslcd')
|
action_utils.service_start('nslcd')
|
||||||
|
|
||||||
|
|
||||||
def configure_ldap_structure():
|
def _configure_ldap_structure():
|
||||||
"""Configure LDAP basic structure."""
|
"""Configure LDAP basic structure."""
|
||||||
was_running = action_utils.service_is_running('slapd')
|
was_running = action_utils.service_is_running('slapd')
|
||||||
if not was_running:
|
if not was_running:
|
||||||
action_utils.service_start('slapd')
|
action_utils.service_start('slapd')
|
||||||
|
|
||||||
setup_admin()
|
_setup_admin()
|
||||||
create_organizational_unit('users')
|
_create_organizational_unit('users')
|
||||||
create_organizational_unit('groups')
|
_create_organizational_unit('groups')
|
||||||
|
|
||||||
|
|
||||||
def create_organizational_unit(unit):
|
def _create_organizational_unit(unit):
|
||||||
"""Create an organizational unit in LDAP."""
|
"""Create an organizational unit in LDAP."""
|
||||||
distinguished_name = 'ou={unit},dc=thisbox'.format(unit=unit)
|
distinguished_name = 'ou={unit},dc=thisbox'.format(unit=unit)
|
||||||
try:
|
try:
|
||||||
@ -210,7 +122,7 @@ ou: {unit}'''.format(unit=unit)
|
|||||||
check=True)
|
check=True)
|
||||||
|
|
||||||
|
|
||||||
def setup_admin():
|
def _setup_admin():
|
||||||
"""Remove LDAP admin password and Allow root to modify the users."""
|
"""Remove LDAP admin password and Allow root to modify the users."""
|
||||||
process = subprocess.run([
|
process = subprocess.run([
|
||||||
'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H',
|
'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H',
|
||||||
@ -243,7 +155,7 @@ olcRootDN: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
|
|||||||
''')
|
''')
|
||||||
|
|
||||||
|
|
||||||
def configure_ldapscripts():
|
def _configure_ldapscripts():
|
||||||
"""Set the configuration used by ldapscripts for later user management."""
|
"""Set the configuration used by ldapscripts for later user management."""
|
||||||
# modify a copy of the config file
|
# modify a copy of the config file
|
||||||
shutil.copy('/etc/ldapscripts/ldapscripts.conf', LDAPSCRIPTS_CONF)
|
shutil.copy('/etc/ldapscripts/ldapscripts.conf', LDAPSCRIPTS_CONF)
|
||||||
@ -266,7 +178,7 @@ def configure_ldapscripts():
|
|||||||
aug.save()
|
aug.save()
|
||||||
|
|
||||||
|
|
||||||
def get_samba_users():
|
def _get_samba_users():
|
||||||
"""Get users from the Samba user database."""
|
"""Get users from the Samba user database."""
|
||||||
# 'pdbedit -L' is better for listing users but is installed only with samba
|
# 'pdbedit -L' is better for listing users but is installed only with samba
|
||||||
stdout = subprocess.check_output(
|
stdout = subprocess.check_output(
|
||||||
@ -274,14 +186,14 @@ def get_samba_users():
|
|||||||
return re.findall(r'USER_(.*)\\0', stdout)
|
return re.findall(r'USER_(.*)\\0', stdout)
|
||||||
|
|
||||||
|
|
||||||
def delete_samba_user(username):
|
def _delete_samba_user(username):
|
||||||
"""Delete a Samba user."""
|
"""Delete a Samba user."""
|
||||||
if username in get_samba_users():
|
if username in _get_samba_users():
|
||||||
subprocess.check_call(['smbpasswd', '-x', username])
|
subprocess.check_call(['smbpasswd', '-x', username])
|
||||||
disconnect_samba_user(username)
|
_disconnect_samba_user(username)
|
||||||
|
|
||||||
|
|
||||||
def disconnect_samba_user(username):
|
def _disconnect_samba_user(username):
|
||||||
"""Disconnect a Samba user."""
|
"""Disconnect a Samba user."""
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(['pkill', '-U', username, 'smbd'])
|
subprocess.check_call(['pkill', '-U', username, 'smbd'])
|
||||||
@ -290,84 +202,63 @@ def disconnect_samba_user(username):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def get_input_lines():
|
@privileged
|
||||||
"""Return list of input lines from stdin."""
|
def create_user(username: str, password: str, auth_user: Optional[str] = None,
|
||||||
global INPUT_LINES
|
auth_password: Optional[str] = None):
|
||||||
if INPUT_LINES is None:
|
|
||||||
INPUT_LINES = [line.strip() for line in sys.stdin]
|
|
||||||
return INPUT_LINES
|
|
||||||
|
|
||||||
|
|
||||||
def read_password(last=False):
|
|
||||||
"""Return the password.
|
|
||||||
|
|
||||||
Set last=True to read password from last line of the input.
|
|
||||||
|
|
||||||
"""
|
|
||||||
line = -1 if last else 0
|
|
||||||
return get_input_lines()[line]
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_create_user(arguments):
|
|
||||||
"""Create an LDAP user, set password and flush cache."""
|
"""Create an LDAP user, set password and flush cache."""
|
||||||
username = arguments.username
|
_validate_user(auth_user, auth_password)
|
||||||
auth_user = arguments.auth_user
|
|
||||||
|
|
||||||
validate_user(auth_user)
|
|
||||||
|
|
||||||
_run(['ldapadduser', username, 'users'])
|
_run(['ldapadduser', username, 'users'])
|
||||||
password = read_password()
|
_set_user_password(username, password)
|
||||||
set_user_password(username, password)
|
_flush_cache()
|
||||||
flush_cache()
|
_set_samba_user(username, password)
|
||||||
set_samba_user(username, password)
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_remove_user(arguments):
|
@privileged
|
||||||
|
def remove_user(username: str, password: Optional[str] = None):
|
||||||
"""Remove an LDAP user."""
|
"""Remove an LDAP user."""
|
||||||
username = arguments.username
|
groups = _get_user_groups(username)
|
||||||
groups = get_user_groups(username)
|
|
||||||
|
|
||||||
# require authentication if the user is last admin user
|
# require authentication if the user is last admin user
|
||||||
if get_group_users('admin') == [username]:
|
if _get_group_users('admin') == [username]:
|
||||||
validate_password(username)
|
_validate_password(username, password)
|
||||||
|
|
||||||
delete_samba_user(username)
|
_delete_samba_user(username)
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
remove_user_from_group(username, group)
|
_remove_user_from_group(username, group)
|
||||||
|
|
||||||
_run(['ldapdeleteuser', username])
|
_run(['ldapdeleteuser', username])
|
||||||
|
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
|
|
||||||
|
|
||||||
def subcommand_rename_user(arguments):
|
@privileged
|
||||||
|
def rename_user(old_username: str, new_username: str):
|
||||||
"""Rename an LDAP user."""
|
"""Rename an LDAP user."""
|
||||||
old_username = arguments.oldusername
|
groups = _get_user_groups(old_username)
|
||||||
new_username = arguments.newusername
|
|
||||||
groups = get_user_groups(old_username)
|
|
||||||
|
|
||||||
delete_samba_user(old_username)
|
_delete_samba_user(old_username)
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
remove_user_from_group(old_username, group)
|
_remove_user_from_group(old_username, group)
|
||||||
|
|
||||||
_run(['ldaprenameuser', old_username, new_username])
|
_run(['ldaprenameuser', old_username, new_username])
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
add_user_to_group(new_username, group)
|
_add_user_to_group(new_username, group)
|
||||||
|
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
|
|
||||||
|
|
||||||
def set_user_password(username, password):
|
def _set_user_password(username, password):
|
||||||
"""Set a user's password."""
|
"""Set a user's password."""
|
||||||
process = _run(['slappasswd', '-s', password], stdout=subprocess.PIPE)
|
process = _run(['slappasswd', '-s', password], stdout=subprocess.PIPE)
|
||||||
password = process.stdout.decode().strip()
|
password = process.stdout.decode().strip()
|
||||||
_run(['ldapsetpasswd', username, password])
|
_run(['ldapsetpasswd', username, password])
|
||||||
|
|
||||||
|
|
||||||
def set_samba_user(username, password):
|
def _set_samba_user(username, password):
|
||||||
"""Insert a user to the Samba database.
|
"""Insert a user to the Samba database.
|
||||||
|
|
||||||
If a user already exists, update password.
|
If a user already exists, update password.
|
||||||
@ -379,24 +270,21 @@ def set_samba_user(username, password):
|
|||||||
raise RuntimeError('Unable to add Samba user: ', proc.stderr)
|
raise RuntimeError('Unable to add Samba user: ', proc.stderr)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_set_user_password(arguments):
|
@privileged
|
||||||
|
def set_user_password(username: str, password: str, auth_user: str,
|
||||||
|
auth_password: str):
|
||||||
"""Set a user's password."""
|
"""Set a user's password."""
|
||||||
username = arguments.username
|
|
||||||
auth_user = arguments.auth_user
|
|
||||||
|
|
||||||
must_be_admin = username != auth_user
|
must_be_admin = username != auth_user
|
||||||
validate_user(auth_user, must_be_admin=must_be_admin)
|
_validate_user(auth_user, auth_password, must_be_admin=must_be_admin)
|
||||||
|
|
||||||
password = read_password()
|
_set_user_password(username, password)
|
||||||
set_user_password(username, password)
|
_set_samba_user(username, password)
|
||||||
set_samba_user(username, password)
|
|
||||||
|
|
||||||
|
|
||||||
def get_admin_users():
|
def _get_admin_users():
|
||||||
"""Returns list of members in the admin group.
|
"""Return list of members in the admin group.
|
||||||
|
|
||||||
Raises an error if the slapd service is not running.
|
|
||||||
|
|
||||||
|
Raise an error if the slapd service is not running.
|
||||||
"""
|
"""
|
||||||
admin_users = []
|
admin_users = []
|
||||||
|
|
||||||
@ -420,8 +308,8 @@ def get_admin_users():
|
|||||||
return admin_users
|
return admin_users
|
||||||
|
|
||||||
|
|
||||||
def get_group_users(groupname):
|
def _get_group_users(groupname):
|
||||||
"""Returns list of members in the group."""
|
"""Return list of members in the group."""
|
||||||
try:
|
try:
|
||||||
process = _run(['ldapgid', '-P', groupname], stdout=subprocess.PIPE)
|
process = _run(['ldapgid', '-P', groupname], stdout=subprocess.PIPE)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
@ -435,10 +323,11 @@ def get_group_users(groupname):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
def get_user_groups(username):
|
def _get_user_groups(username):
|
||||||
"""Returns only the supplementary groups of the given user.
|
"""Return only the supplementary groups of the given user.
|
||||||
|
|
||||||
Exclude the 'users' primary group from the returned list."""
|
Exclude the 'users' primary group from the returned list.
|
||||||
|
"""
|
||||||
process = _run(['ldapid', username], stdout=subprocess.PIPE, check=False)
|
process = _run(['ldapid', username], stdout=subprocess.PIPE, check=False)
|
||||||
output = process.stdout.decode().strip()
|
output = process.stdout.decode().strip()
|
||||||
if output:
|
if output:
|
||||||
@ -460,121 +349,119 @@ def get_user_groups(username):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def subcommand_get_user_groups(arguments):
|
@privileged
|
||||||
|
def get_user_groups(username: str) -> list[str]:
|
||||||
"""Return list of a given user's groups."""
|
"""Return list of a given user's groups."""
|
||||||
groups = get_user_groups(arguments.username)
|
return _get_user_groups(username)
|
||||||
if groups:
|
|
||||||
print(*groups, sep='\n')
|
|
||||||
|
|
||||||
|
|
||||||
def group_exists(groupname):
|
def _group_exists(groupname):
|
||||||
"""Return whether a group already exits."""
|
"""Return whether a group already exits."""
|
||||||
process = _run(['ldapgid', groupname], check=False)
|
process = _run(['ldapgid', groupname], check=False)
|
||||||
return process.returncode == 0
|
return process.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def create_group(groupname):
|
def _create_group(groupname):
|
||||||
"""Add an LDAP group."""
|
"""Add an LDAP group."""
|
||||||
if not group_exists(groupname):
|
if not _group_exists(groupname):
|
||||||
_run(['ldapaddgroup', groupname])
|
_run(['ldapaddgroup', groupname])
|
||||||
|
|
||||||
|
|
||||||
def subcommand_create_group(arguments):
|
@privileged
|
||||||
|
def create_group(groupname: str):
|
||||||
"""Add an LDAP group."""
|
"""Add an LDAP group."""
|
||||||
create_group(arguments.groupname)
|
_create_group(groupname)
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
|
|
||||||
|
|
||||||
def subcommand_rename_group(arguments):
|
@privileged
|
||||||
|
def rename_group(old_groupname: str, new_groupname: str):
|
||||||
"""Rename an LDAP group.
|
"""Rename an LDAP group.
|
||||||
|
|
||||||
Skip if the group to rename from doesn't exist.
|
Skip if the group to rename from doesn't exist.
|
||||||
"""
|
"""
|
||||||
old_groupname = arguments.old_groupname
|
|
||||||
new_groupname = arguments.new_groupname
|
|
||||||
|
|
||||||
if old_groupname == 'admin' or new_groupname == 'admin':
|
if old_groupname == 'admin' or new_groupname == 'admin':
|
||||||
raise argparse.ArgumentTypeError('Can\'t rename the group "admin"')
|
raise ValueError('Can\'t rename the group "admin"')
|
||||||
|
|
||||||
if group_exists(old_groupname):
|
if _group_exists(old_groupname):
|
||||||
_run(['ldaprenamegroup', old_groupname, new_groupname])
|
_run(['ldaprenamegroup', old_groupname, new_groupname])
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
|
|
||||||
|
|
||||||
def subcommand_remove_group(arguments):
|
@privileged
|
||||||
|
def remove_group(groupname: str):
|
||||||
"""Remove an LDAP group."""
|
"""Remove an LDAP group."""
|
||||||
groupname = arguments.groupname
|
|
||||||
|
|
||||||
if groupname == 'admin':
|
if groupname == 'admin':
|
||||||
raise argparse.ArgumentTypeError("Can't remove the group 'admin'")
|
raise ValueError("Can't remove the group 'admin'")
|
||||||
|
|
||||||
if group_exists(groupname):
|
if _group_exists(groupname):
|
||||||
_run(['ldapdeletegroup', groupname])
|
_run(['ldapdeletegroup', groupname])
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
|
|
||||||
|
|
||||||
def add_user_to_group(username, groupname):
|
def _add_user_to_group(username, groupname):
|
||||||
"""Add an LDAP user to an LDAP group."""
|
"""Add an LDAP user to an LDAP group."""
|
||||||
create_group(groupname)
|
_create_group(groupname)
|
||||||
_run(['ldapaddusertogroup', username, groupname])
|
_run(['ldapaddusertogroup', username, groupname])
|
||||||
|
|
||||||
|
|
||||||
def subcommand_add_user_to_group(arguments):
|
@privileged
|
||||||
|
def add_user_to_group(username: str, groupname: str,
|
||||||
|
auth_user: Optional[str] = None,
|
||||||
|
auth_password: Optional[str] = None):
|
||||||
"""Add an LDAP user to an LDAP group."""
|
"""Add an LDAP user to an LDAP group."""
|
||||||
groupname = arguments.groupname
|
|
||||||
|
|
||||||
if groupname == 'admin':
|
if groupname == 'admin':
|
||||||
validate_user(arguments.auth_user)
|
_validate_user(auth_user, auth_password)
|
||||||
|
|
||||||
add_user_to_group(arguments.username, groupname)
|
_add_user_to_group(username, groupname)
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
|
|
||||||
|
|
||||||
def remove_user_from_group(username, groupname):
|
def _remove_user_from_group(username, groupname):
|
||||||
"""Remove an LDAP user from an LDAP group."""
|
"""Remove an LDAP user from an LDAP group."""
|
||||||
_run(['ldapdeleteuserfromgroup', username, groupname])
|
_run(['ldapdeleteuserfromgroup', username, groupname])
|
||||||
|
|
||||||
|
|
||||||
def subcommand_remove_user_from_group(arguments):
|
@privileged
|
||||||
|
def remove_user_from_group(username: str, groupname: str, auth_user: str,
|
||||||
|
auth_password: str):
|
||||||
"""Remove an LDAP user from an LDAP group."""
|
"""Remove an LDAP user from an LDAP group."""
|
||||||
username = arguments.username
|
|
||||||
groupname = arguments.groupname
|
|
||||||
|
|
||||||
if groupname == 'admin':
|
if groupname == 'admin':
|
||||||
validate_user(arguments.auth_user)
|
_validate_user(auth_user, auth_password)
|
||||||
|
|
||||||
remove_user_from_group(username, groupname)
|
_remove_user_from_group(username, groupname)
|
||||||
flush_cache()
|
_flush_cache()
|
||||||
if groupname == 'freedombox-share':
|
if groupname == 'freedombox-share':
|
||||||
disconnect_samba_user(username)
|
_disconnect_samba_user(username)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_get_group_users(arguments):
|
@privileged
|
||||||
|
def get_group_users(group_name: str) -> list[str]:
|
||||||
"""Get the list of users of an LDAP group."""
|
"""Get the list of users of an LDAP group."""
|
||||||
for user in get_group_users(arguments.groupname):
|
return _get_group_users(group_name)
|
||||||
print(user)
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_set_user_status(arguments):
|
@privileged
|
||||||
|
def set_user_status(username: str, status: str, auth_user: str,
|
||||||
|
auth_password: str):
|
||||||
"""Set the status of the user."""
|
"""Set the status of the user."""
|
||||||
username = arguments.username
|
if status not in ('active', 'inactive'):
|
||||||
status = arguments.status
|
raise ValueError('Invalid status')
|
||||||
auth_user = arguments.auth_user
|
|
||||||
|
|
||||||
validate_user(auth_user)
|
_validate_user(auth_user, auth_password)
|
||||||
|
|
||||||
if status == 'active':
|
if status == 'active':
|
||||||
flag = '-e'
|
flag = '-e'
|
||||||
else:
|
else:
|
||||||
flag = '-d'
|
flag = '-d'
|
||||||
|
|
||||||
if username in get_samba_users():
|
if username in _get_samba_users():
|
||||||
subprocess.check_call(['smbpasswd', flag, username])
|
subprocess.check_call(['smbpasswd', flag, username])
|
||||||
if status == 'inactive':
|
if status == 'inactive':
|
||||||
disconnect_samba_user(username)
|
_disconnect_samba_user(username)
|
||||||
|
|
||||||
|
|
||||||
def flush_cache():
|
def _flush_cache():
|
||||||
"""Flush nscd and apache2 cache."""
|
"""Flush nscd and apache2 cache."""
|
||||||
_run(['nscd', '--invalidate=passwd'])
|
_run(['nscd', '--invalidate=passwd'])
|
||||||
_run(['nscd', '--invalidate=group'])
|
_run(['nscd', '--invalidate=group'])
|
||||||
@ -587,17 +474,3 @@ def _run(arguments, check=True, **kwargs):
|
|||||||
kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL)
|
kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL)
|
||||||
kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL)
|
kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL)
|
||||||
return subprocess.run(arguments, env=env, check=check, **kwargs)
|
return subprocess.run(arguments, env=env, check=check, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Parse arguments and perform all duties"""
|
|
||||||
arguments = parse_arguments()
|
|
||||||
|
|
||||||
subcommand = arguments.subcommand.replace('-', '_')
|
|
||||||
subcommand_method = globals()['subcommand_' + subcommand]
|
|
||||||
|
|
||||||
subcommand_method(arguments)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@ -50,9 +50,9 @@
|
|||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
Delete these accounts from command line and refresh the page to create
|
Delete these accounts from command line and refresh the page to create
|
||||||
an account that is usable with {{ box_name }}. On the command line run
|
an account that is usable with {{ box_name }}. On the command line run
|
||||||
the command 'echo "{password}" | /usr/share/plinth/actions/users
|
the command "echo '{"args": ["USERNAME", "PASSWORD"], "kwargs": {}}' |
|
||||||
remove-user {username}'. If an account is already usable with
|
sudo /usr/share/plinth/actions/actions users remove_user". If an
|
||||||
{{ box_name }}, skip this step.
|
account is already usable with {{ box_name }}, skip this step.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
Test module to exercise user actions.
|
Test module to exercise user actions.
|
||||||
@ -6,7 +5,6 @@ Test module to exercise user actions.
|
|||||||
it is recommended to run this module with root privileges in a virtual machine.
|
it is recommended to run this module with root privileges in a virtual machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pathlib
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
@ -15,9 +13,15 @@ import subprocess
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from plinth import action_utils
|
from plinth import action_utils
|
||||||
from plinth.modules import security
|
from plinth.modules.security import privileged as security_privileged
|
||||||
|
from plinth.modules.users import privileged
|
||||||
from plinth.tests import config as test_config
|
from plinth.tests import config as test_config
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures('mock_privileged')
|
||||||
|
privileged_modules_to_mock = [
|
||||||
|
'plinth.modules.users.privileged', 'plinth.modules.security.privileged'
|
||||||
|
]
|
||||||
|
|
||||||
_cleanup_users = None
|
_cleanup_users = None
|
||||||
_cleanup_groups = None
|
_cleanup_groups = None
|
||||||
|
|
||||||
@ -48,13 +52,6 @@ def _random_string(length=8):
|
|||||||
[random.choice(string.ascii_lowercase) for _ in range(length)])
|
[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):
|
def _get_password_hash(username):
|
||||||
"""Query and return the password hash of the given LDAP username"""
|
"""Query and return the password hash of the given LDAP username"""
|
||||||
query = [
|
query = [
|
||||||
@ -90,21 +87,14 @@ def _try_login_to_ssh(username, password, returncode=0):
|
|||||||
return process.returncode == returncode
|
return process.returncode == returncode
|
||||||
|
|
||||||
|
|
||||||
def _action_file():
|
|
||||||
"""Return the path to the 'users' actions file."""
|
|
||||||
current_directory = pathlib.Path(__file__).parent
|
|
||||||
return str(current_directory / '..' / '..' / '..' / '..' / 'actions' /
|
|
||||||
'users')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name='disable_restricted_access', autouse=True)
|
@pytest.fixture(name='disable_restricted_access', autouse=True)
|
||||||
def fixture_disable_restricted_access(needs_root, load_cfg):
|
def fixture_disable_restricted_access(needs_root, load_cfg):
|
||||||
"""Disable console login restrictions."""
|
"""Disable console login restrictions."""
|
||||||
restricted_access = security.get_restricted_access_enabled()
|
restricted_access = security_privileged.get_restricted_access_enabled()
|
||||||
if restricted_access:
|
if restricted_access:
|
||||||
security.set_restricted_access(False)
|
security_privileged.disable_restricted_access()
|
||||||
yield
|
yield
|
||||||
security.set_restricted_access(True)
|
security_privileged.enable_restricted_access()
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@ -135,14 +125,7 @@ def fixture_auto_cleanup_users_groups(needs_root, load_cfg):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
for group in _cleanup_groups:
|
for group in _cleanup_groups:
|
||||||
_delete_group(group)
|
privileged.remove_group(group)
|
||||||
|
|
||||||
|
|
||||||
def _call_action(arguments, check=True, **kwargs):
|
|
||||||
"""Call the action script."""
|
|
||||||
kwargs['stdout'] = kwargs.get('stdout', subprocess.PIPE)
|
|
||||||
kwargs['stderr'] = kwargs.get('stderr', subprocess.PIPE)
|
|
||||||
return subprocess.run([_action_file()] + arguments, check=check, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_user(username=None, groups=None):
|
def _create_user(username=None, groups=None):
|
||||||
@ -151,16 +134,13 @@ def _create_user(username=None, groups=None):
|
|||||||
password = username + '_passwd'
|
password = username + '_passwd'
|
||||||
admin_user, admin_password = _get_admin_user_password()
|
admin_user, admin_password = _get_admin_user_password()
|
||||||
|
|
||||||
process_input = "{0}\n{1}".format(password, admin_password).encode()
|
privileged.create_user(username, password, admin_user, admin_password)
|
||||||
_call_action(['create-user', '--auth-user', admin_user, username],
|
|
||||||
input=process_input)
|
|
||||||
|
|
||||||
if groups:
|
if groups:
|
||||||
for group in groups:
|
for group in groups:
|
||||||
admin_user, admin_password = _get_admin_user_password()
|
admin_user, admin_password = _get_admin_user_password()
|
||||||
_call_action([
|
privileged.add_user_to_group(username, group, admin_user,
|
||||||
'add-user-to-group', '--auth-user', admin_user, username, group
|
admin_password)
|
||||||
], input=admin_password.encode())
|
|
||||||
if group != 'admin':
|
if group != 'admin':
|
||||||
_cleanup_groups.add(group)
|
_cleanup_groups.add(group)
|
||||||
|
|
||||||
@ -170,11 +150,11 @@ def _create_user(username=None, groups=None):
|
|||||||
|
|
||||||
def _delete_user(username):
|
def _delete_user(username):
|
||||||
"""Utility to delete an LDAP and Samba user"""
|
"""Utility to delete an LDAP and Samba user"""
|
||||||
process_input = None
|
admin_password = None
|
||||||
if _get_group_users('admin') == [username]:
|
if privileged.get_group_users('admin') == [username]:
|
||||||
_, admin_password = _get_admin_user_password()
|
_, admin_password = _get_admin_user_password()
|
||||||
process_input = admin_password.encode()
|
|
||||||
_call_action(['remove-user', username], input=process_input)
|
privileged.remove_user(username, admin_password)
|
||||||
|
|
||||||
|
|
||||||
def _create_admin_if_does_not_exist():
|
def _create_admin_if_does_not_exist():
|
||||||
@ -186,8 +166,7 @@ def _create_admin_if_does_not_exist():
|
|||||||
|
|
||||||
def _get_admin_user_password():
|
def _get_admin_user_password():
|
||||||
"""Return an admin username and password."""
|
"""Return an admin username and password."""
|
||||||
|
admin_users = privileged.get_group_users('admin')
|
||||||
admin_users = _get_group_users('admin')
|
|
||||||
|
|
||||||
if not admin_users:
|
if not admin_users:
|
||||||
return ('', '')
|
return ('', '')
|
||||||
@ -205,36 +184,20 @@ def _rename_user(old_username, new_username=None):
|
|||||||
"""Rename a user."""
|
"""Rename a user."""
|
||||||
new_username = new_username or _random_string()
|
new_username = new_username or _random_string()
|
||||||
|
|
||||||
_call_action(['rename-user', old_username, new_username])
|
privileged.rename_user(old_username, new_username)
|
||||||
_cleanup_users.remove(old_username)
|
_cleanup_users.remove(old_username)
|
||||||
_cleanup_users.add(new_username)
|
_cleanup_users.add(new_username)
|
||||||
return new_username
|
return new_username
|
||||||
|
|
||||||
|
|
||||||
def _get_group_users(group):
|
|
||||||
"""Return the list of members in a group."""
|
|
||||||
process = _call_action(['get-group-users', group])
|
|
||||||
return process.stdout.decode().split()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_user_groups(username):
|
|
||||||
"""Return the list of groups for a user."""
|
|
||||||
process = _call_action(['get-user-groups', username])
|
|
||||||
return process.stdout.decode().split()
|
|
||||||
|
|
||||||
|
|
||||||
def _create_group(groupname=None):
|
def _create_group(groupname=None):
|
||||||
groupname = groupname or _random_string()
|
groupname = groupname or _random_string()
|
||||||
_call_action(['create-group', groupname])
|
privileged.create_group(groupname)
|
||||||
if groupname != 'admin':
|
if groupname != 'admin':
|
||||||
_cleanup_groups.add(groupname)
|
_cleanup_groups.add(groupname)
|
||||||
return groupname
|
return groupname
|
||||||
|
|
||||||
|
|
||||||
def _delete_group(groupname):
|
|
||||||
_call_action(['remove-group', groupname])
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_user():
|
def test_create_user():
|
||||||
"""Test whether creating a new user works."""
|
"""Test whether creating a new user works."""
|
||||||
_create_admin_if_does_not_exist()
|
_create_admin_if_does_not_exist()
|
||||||
@ -256,9 +219,8 @@ def test_change_user_password():
|
|||||||
old_password_hash = _get_password_hash(username)
|
old_password_hash = _get_password_hash(username)
|
||||||
new_password = 'pass $123'
|
new_password = 'pass $123'
|
||||||
|
|
||||||
process_input = "{0}\n{1}".format(new_password, admin_password).encode()
|
privileged.set_user_password(username, new_password, admin_user,
|
||||||
_call_action(['set-user-password', username, '--auth-user', admin_user],
|
admin_password)
|
||||||
input=process_input)
|
|
||||||
|
|
||||||
new_password_hash = _get_password_hash(username)
|
new_password_hash = _get_password_hash(username)
|
||||||
assert old_password_hash != new_password_hash
|
assert old_password_hash != new_password_hash
|
||||||
@ -277,9 +239,8 @@ def test_change_password_as_non_admin_user():
|
|||||||
old_password_hash = _get_password_hash(username)
|
old_password_hash = _get_password_hash(username)
|
||||||
new_password = 'pass $123'
|
new_password = 'pass $123'
|
||||||
|
|
||||||
process_input = "{0}\n{1}".format(new_password, old_password).encode()
|
privileged.set_user_password(username, new_password, username,
|
||||||
_call_action(['set-user-password', username, '--auth-user', username],
|
old_password)
|
||||||
input=process_input)
|
|
||||||
|
|
||||||
new_password_hash = _get_password_hash(username)
|
new_password_hash = _get_password_hash(username)
|
||||||
assert old_password_hash != new_password_hash
|
assert old_password_hash != new_password_hash
|
||||||
@ -298,11 +259,9 @@ def test_change_other_users_password_as_non_admin():
|
|||||||
username2, _ = _create_user()
|
username2, _ = _create_user()
|
||||||
new_password = 'pass $123'
|
new_password = 'pass $123'
|
||||||
|
|
||||||
process_input = "{0}\n{1}".format(new_password, password1).encode()
|
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
_call_action(
|
privileged.set_user_password(username2, new_password, username1,
|
||||||
['set-user-password', username2, '--auth-user', username1],
|
password1)
|
||||||
input=process_input)
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_password_for_non_existent_user():
|
def test_set_password_for_non_existent_user():
|
||||||
@ -313,11 +272,9 @@ def test_set_password_for_non_existent_user():
|
|||||||
non_existent_user = _random_string()
|
non_existent_user = _random_string()
|
||||||
fake_password = _random_string()
|
fake_password = _random_string()
|
||||||
|
|
||||||
process_input = "{0}\n{1}".format(fake_password, admin_password).encode()
|
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
_call_action([
|
privileged.set_user_password(non_existent_user, fake_password,
|
||||||
'set-user-password', non_existent_user, '--auth-user', admin_user
|
admin_user, admin_password)
|
||||||
], input=process_input)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_user():
|
def test_rename_user():
|
||||||
@ -325,15 +282,15 @@ def test_rename_user():
|
|||||||
_create_admin_if_does_not_exist()
|
_create_admin_if_does_not_exist()
|
||||||
|
|
||||||
old_username, password = _create_user(groups=['admin', _random_string()])
|
old_username, password = _create_user(groups=['admin', _random_string()])
|
||||||
old_groups = _get_user_groups(old_username)
|
old_groups = privileged.get_user_groups(old_username)
|
||||||
|
|
||||||
new_username = _rename_user(old_username)
|
new_username = _rename_user(old_username)
|
||||||
assert _try_login_to_ssh(new_username, password)
|
assert _try_login_to_ssh(new_username, password)
|
||||||
assert _try_login_to_ssh(old_username, password, returncode=5)
|
assert _try_login_to_ssh(old_username, password, returncode=5)
|
||||||
assert old_username not in _get_samba_users()
|
assert old_username not in _get_samba_users()
|
||||||
|
|
||||||
new_groups = _get_user_groups(new_username)
|
new_groups = privileged.get_user_groups(new_username)
|
||||||
old_users_groups = _get_user_groups(old_username)
|
old_users_groups = privileged.get_user_groups(old_username)
|
||||||
assert not old_users_groups # empty
|
assert not old_users_groups # empty
|
||||||
assert old_groups == new_groups
|
assert old_groups == new_groups
|
||||||
|
|
||||||
@ -357,11 +314,12 @@ def test_delete_user():
|
|||||||
|
|
||||||
username, password = _create_user(groups=[_random_string()])
|
username, password = _create_user(groups=[_random_string()])
|
||||||
_delete_user(username)
|
_delete_user(username)
|
||||||
groups_after = _get_user_groups(username)
|
groups_after = privileged.get_user_groups(username)
|
||||||
assert not groups_after # User gets removed from all groups
|
assert not groups_after # User gets removed from all groups
|
||||||
|
|
||||||
# User account cannot be found after deletion
|
# User account cannot be found after deletion
|
||||||
assert not _is_exit_zero(['ldapid', username])
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
|
subprocess.run(['ldapid', username], check=True)
|
||||||
|
|
||||||
# Deleted user cannot login to ssh
|
# Deleted user cannot login to ssh
|
||||||
assert _try_login_to_ssh(username, password, returncode=5)
|
assert _try_login_to_ssh(username, password, returncode=5)
|
||||||
@ -373,7 +331,7 @@ def test_delete_non_existent_user():
|
|||||||
"""Deleting a non-existent user should fail."""
|
"""Deleting a non-existent user should fail."""
|
||||||
non_existent_user = _random_string()
|
non_existent_user = _random_string()
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
_call_action(['delete-user', non_existent_user])
|
privileged.remove_user(non_existent_user)
|
||||||
|
|
||||||
|
|
||||||
def test_groups():
|
def test_groups():
|
||||||
@ -381,16 +339,18 @@ def test_groups():
|
|||||||
groupname = _random_string()
|
groupname = _random_string()
|
||||||
|
|
||||||
_create_group(groupname)
|
_create_group(groupname)
|
||||||
assert _is_exit_zero(['ldapgid', groupname])
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
|
subprocess.run(['ldapgid', groupname], check=True)
|
||||||
|
|
||||||
# create-group is idempotent
|
# create-group is idempotent
|
||||||
assert _is_exit_zero([_action_file(), 'create-group', groupname])
|
privileged.create_group(groupname)
|
||||||
|
|
||||||
_delete_group(groupname)
|
privileged.remove_group(groupname)
|
||||||
assert not _is_exit_zero(['ldapgid', groupname])
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
|
subprocess.run(['ldapgid', groupname], check=True)
|
||||||
|
|
||||||
# delete-group is idempotent
|
# delete-group is idempotent
|
||||||
assert _is_exit_zero([_action_file(), 'remove-group', groupname])
|
privileged.remove_group(groupname)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_admin_group_fails():
|
def test_delete_admin_group_fails():
|
||||||
@ -398,7 +358,8 @@ def test_delete_admin_group_fails():
|
|||||||
groupname = 'admin'
|
groupname = 'admin'
|
||||||
_create_group('admin')
|
_create_group('admin')
|
||||||
|
|
||||||
assert not _is_exit_zero([_action_file(), 'remove-group', groupname])
|
with pytest.raises(ValueError):
|
||||||
|
privileged.remove_group(groupname)
|
||||||
|
|
||||||
|
|
||||||
def test_user_group_interactions():
|
def test_user_group_interactions():
|
||||||
@ -408,48 +369,38 @@ def test_user_group_interactions():
|
|||||||
|
|
||||||
group1 = _random_string()
|
group1 = _random_string()
|
||||||
user1, _ = _create_user(groups=[group1])
|
user1, _ = _create_user(groups=[group1])
|
||||||
assert [group1] == _get_user_groups(user1)
|
assert [group1] == privileged.get_user_groups(user1)
|
||||||
|
|
||||||
# add-user-to-group is not idempotent
|
# add-user-to-group is not idempotent
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
_call_action(
|
privileged.add_user_to_group(user1, group1, admin_user, admin_password)
|
||||||
['add-user-to-group', '--auth-user', admin_user, user1, group1],
|
|
||||||
input=admin_password.encode())
|
|
||||||
|
|
||||||
# The same user can be added to other new groups
|
# The same user can be added to other new groups
|
||||||
group2 = _random_string()
|
group2 = _random_string()
|
||||||
_create_group(group2)
|
_create_group(group2)
|
||||||
_call_action(
|
privileged.add_user_to_group(user1, group2, admin_user, admin_password)
|
||||||
['add-user-to-group', '--auth-user', admin_user, user1, group2],
|
|
||||||
input=admin_password.encode())
|
|
||||||
|
|
||||||
# Adding a user to a non-existent group creates the group
|
# Adding a user to a non-existent group creates the group
|
||||||
group3 = _random_string()
|
group3 = _random_string()
|
||||||
_call_action(
|
privileged.add_user_to_group(user1, group3, admin_user, admin_password)
|
||||||
['add-user-to-group', '--auth-user', admin_user, user1, group3],
|
|
||||||
input=admin_password.encode())
|
|
||||||
_cleanup_groups.add(group3)
|
_cleanup_groups.add(group3)
|
||||||
|
|
||||||
# The expected groups got created and the user is part of them.
|
# The expected groups got created and the user is part of them.
|
||||||
expected_groups = [group1, group2, group3]
|
expected_groups = [group1, group2, group3]
|
||||||
assert expected_groups == _get_user_groups(user1)
|
assert expected_groups == privileged.get_user_groups(user1)
|
||||||
|
|
||||||
# Remove user from group
|
# Remove user from group
|
||||||
group_to_remove_from = random.choice(expected_groups)
|
group_to_remove_from = random.choice(expected_groups)
|
||||||
_call_action([
|
privileged.remove_user_from_group(user1, group_to_remove_from, admin_user,
|
||||||
'remove-user-from-group', '--auth-user', admin_user, user1,
|
admin_password)
|
||||||
group_to_remove_from
|
|
||||||
], input=admin_password.encode())
|
|
||||||
|
|
||||||
# User is no longer in the group that they're removed from
|
# User is no longer in the group that they're removed from
|
||||||
expected_groups.remove(group_to_remove_from)
|
expected_groups.remove(group_to_remove_from)
|
||||||
assert expected_groups == _get_user_groups(user1)
|
assert expected_groups == privileged.get_user_groups(user1)
|
||||||
|
|
||||||
# User cannot be removed from a group that they're not part of
|
# User cannot be removed from a group that they're not part of
|
||||||
random_group = _random_string()
|
random_group = _random_string()
|
||||||
_create_group(random_group)
|
_create_group(random_group)
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
_call_action([
|
privileged.remove_user_from_group(user1, random_group, admin_user,
|
||||||
'remove-user-from-group', '--auth-user', admin_user, user1,
|
admin_password)
|
||||||
random_group
|
|
||||||
], input=admin_password.encode())
|
|
||||||
@ -31,18 +31,6 @@ def fixture_users_urls():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def action_run(action, options, **kwargs):
|
|
||||||
"""Action return values."""
|
|
||||||
if action == 'users' and options == ['get-group-users', 'admin']:
|
|
||||||
return 'admin'
|
|
||||||
if action == 'ssh' and options[:2] == ['get-keys', '--username']:
|
|
||||||
return ''
|
|
||||||
if action == 'users' and options == ['get-user-groups', 'tester']:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def module_patch():
|
def module_patch():
|
||||||
"""Patch users module."""
|
"""Patch users module."""
|
||||||
@ -54,8 +42,20 @@ def module_patch():
|
|||||||
UsersAndGroups('users-and-groups-minetest',
|
UsersAndGroups('users-and-groups-minetest',
|
||||||
reserved_usernames=['debian-minetest'])
|
reserved_usernames=['debian-minetest'])
|
||||||
|
|
||||||
with patch('pwd.getpwall', return_value=pwd_users),\
|
privileged = 'plinth.modules.users.privileged'
|
||||||
patch('plinth.actions.superuser_run', side_effect=action_run):
|
with patch('pwd.getpwall', return_value=pwd_users), \
|
||||||
|
patch(f'{privileged}.create_user'), \
|
||||||
|
patch(f'{privileged}.add_user_to_group'), \
|
||||||
|
patch(f'{privileged}.set_user_password'), \
|
||||||
|
patch(f'{privileged}.set_user_status'), \
|
||||||
|
patch(f'{privileged}.rename_user'), \
|
||||||
|
patch(f'{privileged}.get_group_users') as get_group_users, \
|
||||||
|
patch('plinth.modules.ssh.privileged.set_keys'), \
|
||||||
|
patch('plinth.modules.ssh.privileged.get_keys') as get_keys, \
|
||||||
|
patch(f'{privileged}.get_user_groups') as get_user_groups:
|
||||||
|
get_group_users.return_value = ['admin']
|
||||||
|
get_keys.return_value = []
|
||||||
|
get_user_groups.return_value = []
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -15,13 +15,12 @@ from django.views.generic.edit import (CreateView, DeleteView, FormView,
|
|||||||
UpdateView)
|
UpdateView)
|
||||||
|
|
||||||
import plinth.modules.ssh.privileged as ssh_privileged
|
import plinth.modules.ssh.privileged as ssh_privileged
|
||||||
from plinth import actions, translation
|
from plinth import translation
|
||||||
from plinth.errors import ActionError
|
|
||||||
from plinth.modules import first_boot
|
from plinth.modules import first_boot
|
||||||
from plinth.utils import is_user_admin
|
from plinth.utils import is_user_admin
|
||||||
from plinth.views import AppView
|
from plinth.views import AppView
|
||||||
|
|
||||||
from . import get_last_admin_user
|
from . import get_last_admin_user, privileged
|
||||||
from .forms import (CreateUserForm, FirstBootForm, UserChangePasswordForm,
|
from .forms import (CreateUserForm, FirstBootForm, UserChangePasswordForm,
|
||||||
UserUpdateForm)
|
UserUpdateForm)
|
||||||
|
|
||||||
@ -59,6 +58,7 @@ class UserCreate(ContextMixin, SuccessMessageMixin, CreateView):
|
|||||||
|
|
||||||
class UserList(AppView, ContextMixin, django.views.generic.ListView):
|
class UserList(AppView, ContextMixin, django.views.generic.ListView):
|
||||||
"""View to list users."""
|
"""View to list users."""
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
template_name = 'users_list.html'
|
template_name = 'users_list.html'
|
||||||
title = gettext_lazy('Users')
|
title = gettext_lazy('Users')
|
||||||
@ -72,6 +72,7 @@ class UserList(AppView, ContextMixin, django.views.generic.ListView):
|
|||||||
|
|
||||||
class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
|
class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
|
||||||
"""View to update a user's details."""
|
"""View to update a user's details."""
|
||||||
|
|
||||||
template_name = 'users_update.html'
|
template_name = 'users_update.html'
|
||||||
model = User
|
model = User
|
||||||
form_class = UserUpdateForm
|
form_class = UserUpdateForm
|
||||||
@ -101,7 +102,7 @@ class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
|
|||||||
ssh_keys = ssh_privileged.get_keys(self.object.username)
|
ssh_keys = ssh_privileged.get_keys(self.object.username)
|
||||||
initial['ssh_keys'] = ssh_keys.strip()
|
initial['ssh_keys'] = ssh_keys.strip()
|
||||||
initial['language'] = self.object.userprofile.language
|
initial['language'] = self.object.userprofile.language
|
||||||
except ActionError:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return initial
|
return initial
|
||||||
@ -129,6 +130,7 @@ class UserDelete(ContextMixin, DeleteView):
|
|||||||
On GET, display a confirmation page.
|
On GET, display a confirmation page.
|
||||||
On POST, delete the user.
|
On POST, delete the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_name = 'users_delete.html'
|
template_name = 'users_delete.html'
|
||||||
model = User
|
model = User
|
||||||
slug_field = 'username'
|
slug_field = 'username'
|
||||||
@ -136,6 +138,7 @@ class UserDelete(ContextMixin, DeleteView):
|
|||||||
title = gettext_lazy('Delete User')
|
title = gettext_lazy('Delete User')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize view, override delete method."""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Avoid a warning with Django 4.0 that delete member should not be
|
# Avoid a warning with Django 4.0 that delete member should not be
|
||||||
@ -149,9 +152,8 @@ class UserDelete(ContextMixin, DeleteView):
|
|||||||
messages.success(self.request, message)
|
messages.success(self.request, message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actions.superuser_run('users',
|
privileged.remove_user(self.kwargs['slug'])
|
||||||
['remove-user', self.kwargs['slug']])
|
except Exception:
|
||||||
except ActionError:
|
|
||||||
messages.error(self.request, _('Deleting LDAP user failed.'))
|
messages.error(self.request, _('Deleting LDAP user failed.'))
|
||||||
|
|
||||||
def _delete(self, *args, **kwargs):
|
def _delete(self, *args, **kwargs):
|
||||||
@ -177,6 +179,7 @@ class UserDelete(ContextMixin, DeleteView):
|
|||||||
|
|
||||||
class UserChangePassword(ContextMixin, SuccessMessageMixin, FormView):
|
class UserChangePassword(ContextMixin, SuccessMessageMixin, FormView):
|
||||||
"""View to change user password."""
|
"""View to change user password."""
|
||||||
|
|
||||||
template_name = 'users_change_password.html'
|
template_name = 'users_change_password.html'
|
||||||
form_class = UserChangePasswordForm
|
form_class = UserChangePasswordForm
|
||||||
title = gettext_lazy('Change Password')
|
title = gettext_lazy('Change Password')
|
||||||
@ -216,8 +219,8 @@ class UserChangePassword(ContextMixin, SuccessMessageMixin, FormView):
|
|||||||
|
|
||||||
class FirstBootView(django.views.generic.CreateView):
|
class FirstBootView(django.views.generic.CreateView):
|
||||||
"""Create user account and log the user in."""
|
"""Create user account and log the user in."""
|
||||||
template_name = 'users_firstboot.html'
|
|
||||||
|
|
||||||
|
template_name = 'users_firstboot.html'
|
||||||
form_class = FirstBootForm
|
form_class = FirstBootForm
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@ -231,15 +234,11 @@ class FirstBootView(django.views.generic.CreateView):
|
|||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
"""Add admin users to context data."""
|
"""Add admin users to context data."""
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context['admin_users'] = privileged.get_group_users('admin')
|
||||||
output = actions.superuser_run('users', ['get-group-users', 'admin'])
|
|
||||||
admin_users = output.strip().split('\n') if output.strip() else []
|
|
||||||
context['admin_users'] = admin_users
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
"""Make request available to the form (to insert messages)"""
|
"""Make request available to the form (to insert messages)."""
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['request'] = self.request
|
kwargs['request'] = self.request
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|||||||
@ -109,33 +109,33 @@ def test_shortcut_list_web_apps_only(common_shortcuts):
|
|||||||
assert return_list == [cuts[0], cuts[1], cuts[2]]
|
assert return_list == [cuts[0], cuts[1], cuts[2]]
|
||||||
|
|
||||||
|
|
||||||
@patch('plinth.actions.superuser_run')
|
@patch('plinth.modules.users.privileged.get_user_groups')
|
||||||
def test_shortcut_list_with_username(superuser_run, common_shortcuts):
|
def test_shortcut_list_with_username(get_user_groups, common_shortcuts):
|
||||||
"""Test listing for particular users."""
|
"""Test listing for particular users."""
|
||||||
cuts = common_shortcuts
|
cuts = common_shortcuts
|
||||||
|
|
||||||
return_list = Shortcut.list()
|
return_list = Shortcut.list()
|
||||||
assert return_list == [cuts[0], cuts[1], cuts[2], cuts[3]]
|
assert return_list == [cuts[0], cuts[1], cuts[2], cuts[3]]
|
||||||
|
|
||||||
superuser_run.return_value = 'admin'
|
get_user_groups.return_value = ['admin']
|
||||||
return_list = Shortcut.list(username='admin')
|
return_list = Shortcut.list(username='admin')
|
||||||
assert return_list == [cuts[0], cuts[1], cuts[2], cuts[3]]
|
assert return_list == [cuts[0], cuts[1], cuts[2], cuts[3]]
|
||||||
|
|
||||||
superuser_run.return_value = 'group1'
|
get_user_groups.return_value = ['group1']
|
||||||
return_list = Shortcut.list(username='user1')
|
return_list = Shortcut.list(username='user1')
|
||||||
assert return_list == [cuts[0], cuts[1], cuts[3]]
|
assert return_list == [cuts[0], cuts[1], cuts[3]]
|
||||||
|
|
||||||
superuser_run.return_value = 'group1\ngroup2'
|
get_user_groups.return_value = ['group1', 'group2']
|
||||||
return_list = Shortcut.list(username='user2')
|
return_list = Shortcut.list(username='user2')
|
||||||
assert return_list == [cuts[0], cuts[1], cuts[2], cuts[3]]
|
assert return_list == [cuts[0], cuts[1], cuts[2], cuts[3]]
|
||||||
|
|
||||||
cut = Shortcut('group2-web-app-component-1', 'name5', 'short2', url='url4',
|
cut = Shortcut('group2-web-app-component-1', 'name5', 'short2', url='url4',
|
||||||
login_required=False, allowed_groups=['group3'])
|
login_required=False, allowed_groups=['group3'])
|
||||||
superuser_run.return_value = 'group3'
|
get_user_groups.return_value = ['group3']
|
||||||
return_list = Shortcut.list(username='user3')
|
return_list = Shortcut.list(username='user3')
|
||||||
assert return_list == [cuts[0], cuts[3], cut]
|
assert return_list == [cuts[0], cuts[3], cut]
|
||||||
|
|
||||||
superuser_run.return_value = 'group4'
|
get_user_groups.return_value = ['group4']
|
||||||
return_list = Shortcut.list(username='user4')
|
return_list = Shortcut.list(username='user4')
|
||||||
assert return_list == [cuts[0], cuts[3], cut]
|
assert return_list == [cuts[0], cuts[3], cut]
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user