mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-29 10:10:19 +00:00
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:
parent
b605c9da8a
commit
b9963a45cd
@ -86,6 +86,11 @@ def parse_arguments():
|
|||||||
subparser.add_argument('groupname',
|
subparser.add_argument('groupname',
|
||||||
help='LDAP group to remove the user from')
|
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
|
subparsers.required = True
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
@ -345,6 +350,16 @@ def subcommand_remove_user_from_group(arguments):
|
|||||||
flush_cache()
|
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():
|
def flush_cache():
|
||||||
"""Flush nscd cache."""
|
"""Flush nscd cache."""
|
||||||
_run(['nscd', '--invalidate=passwd'])
|
_run(['nscd', '--invalidate=passwd'])
|
||||||
|
|||||||
@ -19,10 +19,13 @@ Common forms for use by modules.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.forms import CheckboxInput
|
||||||
from django.utils import translation
|
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 ugettext_lazy as _
|
||||||
from django.utils.translation import get_language_info
|
from django.utils.translation import get_language_info
|
||||||
|
|
||||||
@ -32,8 +35,8 @@ from plinth import utils
|
|||||||
|
|
||||||
class ServiceForm(forms.Form):
|
class ServiceForm(forms.Form):
|
||||||
"""Generic configuration form for a service."""
|
"""Generic configuration form for a service."""
|
||||||
is_enabled = forms.BooleanField(label=_('Enable application'),
|
is_enabled = forms.BooleanField(
|
||||||
required=False)
|
label=_('Enable application'), required=False)
|
||||||
|
|
||||||
|
|
||||||
class DomainSelectionForm(forms.Form):
|
class DomainSelectionForm(forms.Form):
|
||||||
@ -82,3 +85,38 @@ class LanguageSelectionForm(LanguageSelectionFormMixin, forms.Form):
|
|||||||
"""Language selection form."""
|
"""Language selection form."""
|
||||||
|
|
||||||
language = LanguageSelectionFormMixin.language
|
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))
|
||||||
|
|||||||
@ -105,3 +105,15 @@ def remove_group(group):
|
|||||||
|
|
||||||
def register_group(group):
|
def register_group(group):
|
||||||
groups[group[0]] = group[1]
|
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]
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from django.contrib import auth, messages
|
|||||||
from django.contrib.auth.forms import SetPasswordForm, UserCreationForm
|
from django.contrib.auth.forms import SetPasswordForm, UserCreationForm
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.translation import ugettext_lazy
|
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.translation import set_language
|
||||||
from plinth.utils import is_user_admin
|
from plinth.utils import is_user_admin
|
||||||
|
|
||||||
|
from . import get_last_admin_user
|
||||||
|
|
||||||
|
|
||||||
def get_group_choices():
|
def get_group_choices():
|
||||||
"""Return localized group description and group name in one string."""
|
"""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')
|
fields = ('username', 'groups', 'ssh_keys', 'language', 'is_active')
|
||||||
model = User
|
model = User
|
||||||
widgets = {
|
widgets = {
|
||||||
'groups': forms.widgets.CheckboxSelectMultiple(),
|
'groups': plinth.forms.CheckboxSelectMultipleWithDisabled(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, request, username, *args, **kwargs):
|
def __init__(self, request, username, *args, **kwargs):
|
||||||
@ -171,6 +174,7 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
|
|||||||
self.request = request
|
self.request = request
|
||||||
self.username = username
|
self.username = username
|
||||||
super(UserUpdateForm, self).__init__(*args, **kwargs)
|
super(UserUpdateForm, self).__init__(*args, **kwargs)
|
||||||
|
last_admin_user = get_last_admin_user()
|
||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
|
|
||||||
@ -179,7 +183,10 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
|
|||||||
# applications not installed yet.
|
# applications not installed yet.
|
||||||
if c[1] in group_choices:
|
if c[1] in group_choices:
|
||||||
# Replace group names with descriptions
|
# 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'].label = _('Permissions')
|
||||||
self.fields['groups'].choices = choices
|
self.fields['groups'].choices = choices
|
||||||
@ -188,6 +195,9 @@ class UserUpdateForm(ValidNewUsernameCheckMixin,
|
|||||||
self.fields['is_active'].widget = forms.HiddenInput()
|
self.fields['is_active'].widget = forms.HiddenInput()
|
||||||
self.fields['groups'].disabled = True
|
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):
|
def save(self, commit=True):
|
||||||
"""Update LDAP user name and groups after saving user model."""
|
"""Update LDAP user name and groups after saving user model."""
|
||||||
user = super(UserUpdateForm, self).save(commit=False)
|
user = super(UserUpdateForm, self).save(commit=False)
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for user in object_list %}
|
{% for user in object_list %}
|
||||||
<div class="list-group-item clearfix">
|
<div class="list-group-item clearfix">
|
||||||
{% if object_list|length != 1 %}
|
{% if user.username != last_admin_user %}
|
||||||
<a href="{% url 'users:delete' user.username %}"
|
<a href="{% url 'users:delete' user.username %}"
|
||||||
class="btn btn-default btn-sm pull-right"
|
class="btn btn-default btn-sm pull-right"
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@ -28,6 +28,8 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
|||||||
|
|
||||||
from .forms import CreateUserForm, UserChangePasswordForm, UserUpdateForm, \
|
from .forms import CreateUserForm, UserChangePasswordForm, UserUpdateForm, \
|
||||||
FirstBootForm
|
FirstBootForm
|
||||||
|
from . import get_last_admin_user
|
||||||
|
|
||||||
from plinth import actions
|
from plinth import actions
|
||||||
from plinth.errors import ActionError
|
from plinth.errors import ActionError
|
||||||
from plinth.modules import first_boot
|
from plinth.modules import first_boot
|
||||||
@ -72,6 +74,13 @@ class UserList(ContextMixin, django.views.generic.ListView):
|
|||||||
template_name = 'users_list.html'
|
template_name = 'users_list.html'
|
||||||
title = ugettext_lazy('Users')
|
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):
|
class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
|
||||||
"""View to update a user's details."""
|
"""View to update a user's details."""
|
||||||
@ -85,7 +94,7 @@ class UserUpdate(ContextMixin, SuccessMessageMixin, UpdateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""Handle a request and return a HTTP response."""
|
"""Handle a request and return a HTTP response."""
|
||||||
if self.request.user.get_username() != self.kwargs['slug'] \
|
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
|
raise PermissionDenied
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@ -157,7 +166,7 @@ class UserChangePassword(ContextMixin, SuccessMessageMixin, FormView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""Handle a request and return a HTTP response."""
|
"""Handle a request and return a HTTP response."""
|
||||||
if self.request.user.get_username() != self.kwargs['slug'] \
|
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
|
raise PermissionDenied
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user