# authentic2 - versatile identity manager # Copyright (C) 2010-2019 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import json from django.core.exceptions import PermissionDenied, ValidationError from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from django.views.generic import FormView, TemplateView from django.views.generic.detail import SingleObjectMixin from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.db import transaction from django.db.models.query import Q, Prefetch from django.db.models import Count, F from django.contrib.auth import get_user_model from django_rbac.utils import get_role_model, get_permission_model, get_ou_model from authentic2.forms.profile import modelform_factory from authentic2.utils import redirect from authentic2 import hooks, data_transfer from . import tables, views, resources, forms, app_settings from .utils import has_show_username class RolesMixin(object): service_roles = True admin_roles = False def get_queryset(self): qs = super(RolesMixin, self).get_queryset() qs = qs.select_related('ou') Permission = get_permission_model() permission_ct = ContentType.objects.get_for_model(Permission) ct_ct = ContentType.objects.get_for_model(ContentType) ou_ct = ContentType.objects.get_for_model(get_ou_model()) permission_qs = Permission.objects.filter(target_ct_id__in=[ct_ct.id, ou_ct.id]) \ .values_list('id', flat=True) # only non role-admin roles, they are accessed through the # RoleManager views if not self.admin_roles: qs = qs.filter( Q(admin_scope_ct__isnull=True) | Q(admin_scope_ct=permission_ct, admin_scope_id__in=permission_qs)) if not self.service_roles: qs = qs.filter(service__isnull=True) return qs class RolesView(views.HideOUColumnMixin, RolesMixin, views.BaseTableView): template_name = 'authentic2/manager/roles.html' model = get_role_model() table_class = tables.RoleTable search_form_class = forms.RoleSearchForm permissions = ['a2_rbac.search_role'] title = _('Roles') def get_queryset(self): qs = super(RolesView, self).get_queryset() qs = qs.annotate(member_count=Count('members')) return qs def get_search_form_kwargs(self): kwargs = super(RolesView, self).get_search_form_kwargs() kwargs['queryset'] = self.get_queryset() return kwargs listing = RolesView.as_view() class RoleAddView(views.BaseAddView): template_name = 'authentic2/manager/role_add.html' model = get_role_model() title = _('Add role') success_view_name = 'a2-manager-role-members' exclude_fields = ('slug',) def get_initial(self): initial = super().get_initial() search_ou = self.request.GET.get('search-ou') if search_ou: initial['ou'] = search_ou return initial def get_form_class(self): form = forms.get_role_form_class() fields = [x for x in form.base_fields.keys() if x not in self.exclude_fields] return modelform_factory(self.model, form=form, fields=fields) def form_valid(self, form): response = super(RoleAddView, self).form_valid(form) hooks.call_hooks('event', name='manager-add-role', user=self.request.user, instance=form.instance, form=form) return response add = RoleAddView.as_view() class RolesExportView(views.ExportMixin, RolesView): resource_class = resources.RoleResource export_prefix = 'roles-export-' def get(self, request, *args, **kwargs): export_format = kwargs['format'].lower() if export_format == 'json': export = data_transfer.export_site( data_transfer.ExportContext( role_qs=self.get_table_data(), export_roles=True, export_ous=False)) return self.export_response(json.dumps(export, indent=4), 'application/json', 'json') return super(RolesExportView, self).get(request, *args, **kwargs) export = RolesExportView.as_view() class RoleViewMixin(RolesMixin): model = get_role_model() class RoleEditView(RoleViewMixin, views.BaseEditView): template_name = 'authentic2/manager/role_edit.html' title = _('Edit role description') def get_form_class(self): return forms.get_role_form_class() def form_valid(self, form): response = super(RoleEditView, self).form_valid(form) hooks.call_hooks('event', name='manager-edit-role', user=self.request.user, instance=form.instance, form=form) return response edit = RoleEditView.as_view() class RoleMembersView(views.HideOUColumnMixin, RoleViewMixin, views.BaseSubTableView): template_name = 'authentic2/manager/role_members.html' table_class = tables.RoleMembersTable form_class = forms.ChooseUserForm success_url = '.' search_form_class = forms.UserSearchForm permissions = ['a2_rbac.view_role'] @property def title(self): return self.get_instance_name() @property def can_manage_members(self): return self.object.can_manage_members and getattr(self, '_can_manage_members', False) @can_manage_members.setter def can_manage_members(self, value): self._can_manage_members = value def get_table_queryset(self): children = self.object.children(include_self=False) via_prefetch = Prefetch('roles', queryset=children, to_attr='via') return self.object.all_members().prefetch_related(via_prefetch) def form_valid(self, form): user = form.cleaned_data['user'] action = form.cleaned_data['action'] if self.can_manage_members: if action == 'add': if self.object.members.filter(pk=user.pk).exists(): messages.warning(self.request, _('User already in this role.')) else: self.object.members.add(user) hooks.call_hooks('event', name='manager-add-role-member', user=self.request.user, role=self.object, member=user) elif action == 'remove': if not self.object.members.filter(pk=user.pk).exists(): messages.warning(self.request, _('User was not in this role.')) else: self.object.members.remove(user) hooks.call_hooks('event', name='manager-remove-role-member', user=self.request.user, role=self.object, member=user) else: messages.warning(self.request, _('You are not authorized')) return super(RoleMembersView, self).form_valid(form) def get_form_kwargs(self): kwargs = super(RoleMembersView, self).get_form_kwargs() # if role's members can only be from the same OU we filter user based on the role's OU if app_settings.ROLE_MEMBERS_FROM_OU: kwargs['ou'] = self.object.ou return kwargs def get_context_data(self, **kwargs): ctx = super(RoleMembersView, self).get_context_data(**kwargs) ctx['children'] = views.filter_view(self.request, self.object.children(include_self=False, annotate=True)) ctx['parents'] = views.filter_view(self.request, self.object.parents( include_self=False, annotate=True).order_by(F('ou').asc(nulls_first=True), 'name')) ctx['has_multiple_ou'] = get_ou_model().objects.count() > 1 ctx['admin_roles'] = views.filter_view(self.request, self.object.get_admin_role().children(include_self=False, annotate=True)) ctx['from_ldap'] = self._can_manage_members and not self.can_manage_members return ctx def is_ou_specified(self): return self.search_form.is_valid() \ and self.search_form.cleaned_data.get('ou') def get_table(self, **kwargs): show_username = has_show_username() if not show_username and self.is_ou_specified(): show_username = self.is_ou_specified().show_username if not show_username: exclude = kwargs.setdefault('exclude', []) if 'username' not in exclude: exclude.append('username') return super().get_table(**kwargs) members = RoleMembersView.as_view() class RoleDeleteView(RoleViewMixin, views.BaseDeleteView): title = _('Delete role') template_name = 'authentic2/manager/role_delete.html' def post(self, request, *args, **kwargs): if not self.can_delete: raise PermissionDenied return super(RoleDeleteView, self).post(request, *args, **kwargs) def get_success_url(self): return reverse('a2-manager-roles') def form_valid(self, form): response = super(RoleDeleteView, self).form_valid(form) hooks.call_hooks('event', name='manager-delete-role', user=self.request.user, role=form.instance) return response delete = RoleDeleteView.as_view() class RolePermissionsView(RoleViewMixin, views.BaseSubTableView): template_name = 'authentic2/manager/role_permissions.html' table_class = tables.PermissionTable form_class = forms.ChoosePermissionForm success_url = '.' permissions = ['a2_rbac.admin_permission'] title = _('Permissions') def get_table_queryset(self): return self.object.permissions.all() def form_valid(self, form): if self.can_change: operation = form.cleaned_data.get('operation') ou = form.cleaned_data.get('ou') target = form.cleaned_data.get('target') action = form.cleaned_data.get('action') Permission = get_permission_model() if action == 'add' and operation and target: perm, created = Permission.objects \ .get_or_create(operation=operation, ou=ou, target_ct=ContentType.objects.get_for_model( target), target_id=target.pk) self.object.permissions.add(perm) hooks.call_hooks('event', name='manager-add-permission', user=self.request.user, role=self.object, permission=perm) elif action == 'remove': try: permission_id = int(self.request.POST.get('permission', '')) perm = Permission.objects.get(id=permission_id) except (ValueError, Permission.DoesNotExist): pass else: if self.object.permissions.filter(id=permission_id).exists(): self.object.permissions.remove(perm) hooks.call_hooks('event', name='manager-remove-permission', user=self.request.user, role=self.object, permission=perm) else: messages.warning(self.request, _('You are not authorized')) return super(RolePermissionsView, self).form_valid(form) permissions = RolePermissionsView.as_view() class RoleMembersExportView(views.ExportMixin, RoleMembersView): resource_class = resources.UserResource permissions = ['a2_rbac.view_role'] def get_data(self): return self.get_table_data() members_export = RoleMembersExportView.as_view() class RoleAddChildView(views.AjaxFormViewMixin, views.TitleMixin, views.PermissionMixin, views.FormNeedsRequest, SingleObjectMixin, FormView): title = _('Add child role') model = get_role_model() form_class = forms.RolesForm success_url = '..' template_name = 'authentic2/manager/form.html' permissions = ['a2_rbac.manage_members_role'] def dispatch(self, request, *args, **kwargs): self.object = self.get_object() return super(RoleAddChildView, self).dispatch(request, *args, **kwargs) def form_valid(self, form): parent = self.get_object() for role in form.cleaned_data['roles']: parent.add_child(role) hooks.call_hooks('event', name='manager-add-child-role', user=self.request.user, parent=parent, child=role) return super(RoleAddChildView, self).form_valid(form) add_child = RoleAddChildView.as_view() class RoleAddParentView(views.AjaxFormViewMixin, views.TitleMixin, views.FormNeedsRequest, SingleObjectMixin, FormView): title = _('Add parent role') model = get_role_model() form_class = forms.RoleParentsForm success_url = '..' template_name = 'authentic2/manager/form.html' def dispatch(self, request, *args, **kwargs): self.object = self.get_object() if self.object.is_internal(): raise PermissionDenied return super(RoleAddParentView, self).dispatch(request, *args, **kwargs) def form_valid(self, form): child = self.get_object() for role in form.cleaned_data['roles']: child.add_parent(role) hooks.call_hooks('event', name='manager-add-child-role', user=self.request.user, parent=role, child=child) return super(RoleAddParentView, self).form_valid(form) add_parent = RoleAddParentView.as_view() class RoleRemoveChildView(views.AjaxFormViewMixin, SingleObjectMixin, views.PermissionMixin, TemplateView): title = _('Remove child role') model = get_role_model() success_url = '../..' template_name = 'authentic2/manager/role_remove_child.html' permissions = ['a2_rbac.manage_members_role'] def dispatch(self, request, *args, **kwargs): self.object = self.get_object() self.child = self.get_queryset().get(pk=kwargs['child_pk']) return super(RoleRemoveChildView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): ctx = super(RoleRemoveChildView, self).get_context_data(**kwargs) ctx['child'] = self.child return ctx def post(self, request, *args, **kwargs): self.object.remove_child(self.child) hooks.call_hooks('event', name='manager-remove-child-role', user=self.request.user, parent=self.object, child=self.child) return redirect(self.request, self.success_url) remove_child = RoleRemoveChildView.as_view() class RoleRemoveParentView(views.AjaxFormViewMixin, SingleObjectMixin, TemplateView): title = _('Remove parent role') model = get_role_model() success_url = '../..' template_name = 'authentic2/manager/role_remove_parent.html' def dispatch(self, request, *args, **kwargs): self.object = self.get_object() if self.object.is_internal(): raise PermissionDenied self.parent = self.get_queryset().get(pk=kwargs['parent_pk']) return super(RoleRemoveParentView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): ctx = super(RoleRemoveParentView, self).get_context_data(**kwargs) ctx['parent'] = self.parent return ctx def post(self, request, *args, **kwargs): if not self.request.user.has_perm('a2_rbac.manage_members_role', self.parent): raise PermissionDenied self.object.remove_parent(self.parent) hooks.call_hooks('event', name='manager-remove-child-role', user=self.request.user, parent=self.parent, child=self.object) return redirect(self.request, self.success_url) remove_parent = RoleRemoveParentView.as_view() class RoleAddAdminRoleView(views.AjaxFormViewMixin, views.TitleMixin, views.PermissionMixin, views.FormNeedsRequest, SingleObjectMixin, FormView): title = _('Add admin role') model = get_role_model() form_class = forms.RolesForm success_url = '..' template_name = 'authentic2/manager/form.html' permissions = ['a2_rbac.change_role'] def dispatch(self, request, *args, **kwargs): self.object = self.get_object() return super(RoleAddAdminRoleView, self).dispatch(request, *args, **kwargs) def form_valid(self, form): administered_role = self.get_object() for role in form.cleaned_data['roles']: administered_role.get_admin_role().add_child(role) hooks.call_hooks('event', name='manager-add-admin-role', user=self.request.user, role=administered_role, admin_role=role) return super(RoleAddAdminRoleView, self).form_valid(form) add_admin_role = RoleAddAdminRoleView.as_view() class RoleRemoveAdminRoleView(views.TitleMixin, views.AjaxFormViewMixin, SingleObjectMixin, views.PermissionMixin, TemplateView): title = _('Remove admin role') model = get_role_model() success_url = '../..' template_name = 'authentic2/manager/role_remove_admin_role.html' permissions = ['a2_rbac.change_role'] def dispatch(self, request, *args, **kwargs): self.object = self.get_object() self.child = self.get_queryset().get(pk=kwargs['role_pk']) return super(RoleRemoveAdminRoleView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): ctx = super(RoleRemoveAdminRoleView, self).get_context_data(**kwargs) ctx['child'] = self.child return ctx def post(self, request, *args, **kwargs): self.object.get_admin_role().remove_child(self.child) hooks.call_hooks('event', name='manager-remove-admin-role', user=self.request.user, role=self.object, admin_role=self.child) return redirect(self.request, self.success_url) remove_admin_role = RoleRemoveAdminRoleView.as_view() class RoleAddAdminUserView(views.AjaxFormViewMixin, views.TitleMixin, views.PermissionMixin, views.FormNeedsRequest, SingleObjectMixin, FormView): title = _('Add admin user') model = get_role_model() form_class = forms.UsersForm success_url = '..' template_name = 'authentic2/manager/form.html' permissions = ['a2_rbac.change_role'] def dispatch(self, request, *args, **kwargs): self.object = self.get_object() return super(RoleAddAdminUserView, self).dispatch(request, *args, **kwargs) def form_valid(self, form): administered_role = self.get_object() for user in form.cleaned_data['users']: administered_role.get_admin_role().members.add(user) hooks.call_hooks('event', name='manager-add-admin-role-user', user=self.request.user, role=administered_role, admin=user) return super(RoleAddAdminUserView, self).form_valid(form) add_admin_user = RoleAddAdminUserView.as_view() class RoleRemoveAdminUserView(views.TitleMixin, views.AjaxFormViewMixin, SingleObjectMixin, views.PermissionMixin, TemplateView): title = _('Remove admin user') model = get_role_model() success_url = '../..' template_name = 'authentic2/manager/role_remove_admin_user.html' permissions = ['a2_rbac.change_role'] def dispatch(self, request, *args, **kwargs): self.object = self.get_object() self.user = get_user_model().objects.get(pk=kwargs['user_pk']) return super(RoleRemoveAdminUserView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): ctx = super(RoleRemoveAdminUserView, self).get_context_data(**kwargs) ctx['user'] = self.user return ctx def post(self, request, *args, **kwargs): self.object.get_admin_role().members.remove(self.user) hooks.call_hooks('event', name='remove-remove-admin-role-user', user=self.request.user, role=self.object, admin=self.user) return redirect(self.request, self.success_url) remove_admin_user = RoleRemoveAdminUserView.as_view() class RolesImportView(views.PermissionMixin, views.TitleMixin, views.MediaMixin, views.FormNeedsRequest, FormView): form_class = forms.RolesImportForm model = get_role_model() template_name = 'authentic2/manager/import_form.html' title = _('Roles Import') def get_initial(self): initial = super().get_initial() search_ou = self.request.GET.get('search-ou') if search_ou: initial['ou'] = search_ou return initial def post(self, request, *args, **kwargs): if not self.can_add: raise PermissionDenied return super().post(request, *args, **kwargs) def form_valid(self, form): self.ou = form.cleaned_data['ou'] try: context = data_transfer.ImportContext(import_ous=False, set_ou=self.ou) with transaction.atomic(): data_transfer.import_site(form.cleaned_data['site_json'], context) except ValidationError as e: form.add_error('site_json', e) return self.form_invalid(form) return super().form_valid(form) def get_success_url(self): messages.success( self.request, _('Roles have been successfully imported inside "%s" organizational unit.') % self.ou ) return reverse('a2-manager-roles') + '?search-ou=%s' % self.ou.pk roles_import = RolesImportView.as_view()