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 <hems.india1997@gmail.com>
Reviewed-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
This commit is contained in:
Hemanth Kumar Veeranki 2018-06-20 23:49:17 +05:30 committed by Joseph Nuthalapati
parent b605c9da8a
commit b9963a45cd
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
6 changed files with 91 additions and 7 deletions

View File

@ -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'])

View File

@ -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'<ul>']
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'<li><label%s>%s %s</label></li>' %
(label_for, rendered_cb, option_label))
output.append(u'</ul>')
return mark_safe(u'\n'.join(output))

View File

@ -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]

View File

@ -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)

View File

@ -40,7 +40,7 @@
<div class="list-group">
{% for user in object_list %}
<div class="list-group-item clearfix">
{% if object_list|length != 1 %}
{% if user.username != last_admin_user %}
<a href="{% url 'users:delete' user.username %}"
class="btn btn-default btn-sm pull-right"
role="button"

View File

@ -28,6 +28,8 @@ from django.utils.translation import ugettext as _, ugettext_lazy
from .forms import CreateUserForm, UserChangePasswordForm, UserUpdateForm, \
FirstBootForm
from . import get_last_admin_user
from plinth import actions
from plinth.errors import ActionError
from plinth.modules import first_boot
@ -72,6 +74,13 @@ class UserList(ContextMixin, django.views.generic.ListView):
template_name = 'users_list.html'
title = ugettext_lazy('Users')
def get_context_data(self, *args, **kwargs):
context = super(UserList, self).get_context_data(*args, **kwargs)
last_admin_user = get_last_admin_user()
if last_admin_user is not None:
context['last_admin_user'] = last_admin_user
return context
class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
"""View to update a user's details."""
@ -85,7 +94,7 @@ class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
def dispatch(self, request, *args, **kwargs):
"""Handle a request and return a HTTP response."""
if self.request.user.get_username() != self.kwargs['slug'] \
and not is_user_admin(self.request):
and not is_user_admin(self.request):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
@ -157,7 +166,7 @@ class UserChangePassword(ContextMixin, SuccessMessageMixin, FormView):
def dispatch(self, request, *args, **kwargs):
"""Handle a request and return a HTTP response."""
if self.request.user.get_username() != self.kwargs['slug'] \
and not is_user_admin(self.request):
and not is_user_admin(self.request):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)