From b9963a45cded91ef8c60adbde5ee7e4c16716c68 Mon Sep 17 00:00:00 2001 From: Hemanth Kumar Veeranki Date: Wed, 20 Jun 2018 23:49:17 +0530 Subject: [PATCH] Restrict removal of last admin user - Don't allow disabling the only available admin account. - Don't allow deletion of the only available admin account. - Don't allow removing admin privileges of the only available admin account. Signed-off-by: Hemanth Kumar Veeranki Reviewed-by: Joseph Nuthalapati --- actions/users | 15 +++++++ plinth/forms.py | 42 ++++++++++++++++++- plinth/modules/users/__init__.py | 12 ++++++ plinth/modules/users/forms.py | 14 ++++++- .../modules/users/templates/users_list.html | 2 +- plinth/modules/users/views.py | 13 +++++- 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/actions/users b/actions/users index e1a84c228..4cf7dcb87 100755 --- a/actions/users +++ b/actions/users @@ -86,6 +86,11 @@ def parse_arguments(): subparser.add_argument('groupname', help='LDAP group to remove the user from') + help_get_admin_user = 'Get the list of all users in an LDAP group' + subparser = subparsers.add_parser('get-group-users', help=help_get_admin_user) + subparser.add_argument('groupname', help='name of the LDAP group to get the ' + 'list of users') + subparsers.required = True return parser.parse_args() @@ -345,6 +350,16 @@ def subcommand_remove_user_from_group(arguments): flush_cache() +def subcommand_get_group_users(arguments): + """ Get the list of admin users """ + process = _run(['ldapgid', arguments.groupname], stdout=subprocess.PIPE) + output = str(process.stdout).split() + users_info = output[1].split('=')[1].strip('\\n').split(',') + for user_info in users_info: + user_name = user_info.split('(')[1].split(')')[0] + print(user_name) + + def flush_cache(): """Flush nscd cache.""" _run(['nscd', '--invalidate=passwd']) diff --git a/plinth/forms.py b/plinth/forms.py index 319c3431d..0358080e9 100644 --- a/plinth/forms.py +++ b/plinth/forms.py @@ -19,10 +19,13 @@ Common forms for use by modules. """ import os +from itertools import chain from django import forms from django.conf import settings +from django.forms import CheckboxInput from django.utils import translation +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from django.utils.translation import get_language_info @@ -32,8 +35,8 @@ from plinth import utils class ServiceForm(forms.Form): """Generic configuration form for a service.""" - is_enabled = forms.BooleanField(label=_('Enable application'), - required=False) + is_enabled = forms.BooleanField( + label=_('Enable application'), required=False) class DomainSelectionForm(forms.Form): @@ -82,3 +85,38 @@ class LanguageSelectionForm(LanguageSelectionFormMixin, forms.Form): """Language selection form.""" language = LanguageSelectionFormMixin.language + + +class CheckboxSelectMultipleWithDisabled(forms.widgets.CheckboxSelectMultiple): + """ + Subclass of Django's checkbox select multiple widget that allows disabling checkbox-options. + To disable an option, pass a dict instead of a string for its label, + of the form: {'label': 'option label', 'disabled': True} + + Derived from https://djangosnippets.org/snippets/2786/ + """ + + def render(self, name, value, attrs=None, choices=(), renderer=None): + if value is None: value = [] + final_attrs = self.build_attrs(attrs) + output = [u'
    '] + global_disabled = 'disabled' in final_attrs + str_values = set([v for v in value]) + for i, (option_value, option_label) in enumerate( + chain(self.choices, choices)): + if not global_disabled and 'disabled' in final_attrs: + # If the entire group is disabled keep all options disabled + del final_attrs['disabled'] + if isinstance(option_label, dict): + if dict.get(option_label, 'disabled'): + final_attrs = dict(final_attrs, disabled='disabled') + option_label = option_label['label'] + final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) + label_for = u' for="%s"' % final_attrs['id'] + cb = CheckboxInput(final_attrs, + check_test=lambda value: value in str_values) + rendered_cb = cb.render(name, option_value) + output.append(u'
  • %s %s
  • ' % + (label_for, rendered_cb, option_label)) + output.append(u'
') + return mark_safe(u'\n'.join(output)) diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py index adf55e577..4edea4275 100644 --- a/plinth/modules/users/__init__.py +++ b/plinth/modules/users/__init__.py @@ -105,3 +105,15 @@ def remove_group(group): def register_group(group): groups[group[0]] = group[1] + + +def get_last_admin_user(): + """ Check if there is only one admin user + if yes return its name else return None + """ + admin_users = actions.superuser_run('users', + ['get-group-users','admin'] + ).strip().split('\n') + if len(admin_users) > 1: + return None + return admin_users[0] diff --git a/plinth/modules/users/forms.py b/plinth/modules/users/forms.py index 2b7188ca8..aff9d8c14 100644 --- a/plinth/modules/users/forms.py +++ b/plinth/modules/users/forms.py @@ -22,6 +22,7 @@ from django.contrib import auth, messages from django.contrib.auth.forms import SetPasswordForm, UserCreationForm from django.contrib.auth.models import Group, User from django.core.exceptions import ValidationError + from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy @@ -33,6 +34,8 @@ from plinth.modules.security import set_restricted_access from plinth.translation import set_language from plinth.utils import is_user_admin +from . import get_last_admin_user + def get_group_choices(): """Return localized group description and group name in one string.""" @@ -159,7 +162,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, fields = ('username', 'groups', 'ssh_keys', 'language', 'is_active') model = User widgets = { - 'groups': forms.widgets.CheckboxSelectMultiple(), + 'groups': plinth.forms.CheckboxSelectMultipleWithDisabled(), } def __init__(self, request, username, *args, **kwargs): @@ -171,6 +174,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, self.request = request self.username = username super(UserUpdateForm, self).__init__(*args, **kwargs) + last_admin_user = get_last_admin_user() choices = [] @@ -179,7 +183,10 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, # applications not installed yet. if c[1] in group_choices: # Replace group names with descriptions - choices.append((c[0], group_choices[c[1]])) + if c[1] == 'admin' and last_admin_user is not None: + choices.append((c[0], {'label': group_choices[c[1]], 'disabled': True})) + else: + choices.append((c[0], group_choices[c[1]])) self.fields['groups'].label = _('Permissions') self.fields['groups'].choices = choices @@ -188,6 +195,9 @@ class UserUpdateForm(ValidNewUsernameCheckMixin, self.fields['is_active'].widget = forms.HiddenInput() self.fields['groups'].disabled = True + if last_admin_user and last_admin_user == self.username: + self.fields['is_active'].disabled = True + def save(self, commit=True): """Update LDAP user name and groups after saving user model.""" user = super(UserUpdateForm, self).save(commit=False) diff --git a/plinth/modules/users/templates/users_list.html b/plinth/modules/users/templates/users_list.html index de36f745a..94a264121 100644 --- a/plinth/modules/users/templates/users_list.html +++ b/plinth/modules/users/templates/users_list.html @@ -40,7 +40,7 @@
{% for user in object_list %}
- {% if object_list|length != 1 %} + {% if user.username != last_admin_user %}