authentic/src/authentic2/manager/views.py

927 lines
31 KiB
Python

# 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 <http://www.gnu.org/licenses/>.
import base64
import csv
import itertools
import json
import pickle
import random
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from django.forms import MediaDefiningClass
from django.http import Http404, HttpResponse
from django.urls import reverse, reverse_lazy
from django.utils.encoding import force_str
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DeleteView, DetailView, FormView, TemplateView, UpdateView, View
from django.views.generic.base import ContextMixin
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin
from django_select2.views import AutoResponseView
from django_tables2 import SingleTableMixin, SingleTableView
from gadjo.templatetags.gadjo import xstatic
from authentic2.a2_rbac.models import OrganizationalUnit
from authentic2.backends import ldap_backend
from authentic2.data_transfer import ImportContext, export_site, import_site
from authentic2.decorators import json as json_view
from authentic2.forms.profile import modelform_factory
from authentic2.utils import crypto, hooks
from authentic2.utils.misc import batch_queryset, is_ajax, redirect
from . import app_settings, forms, utils, widgets
class MediaMixinBase(MediaDefiningClass, FormMixin):
pass
class MultipleOUMixin:
'''Tell templates if there are multiple OU for adaptation in breadcrumbs for example'''
def get_context_data(self, **kwargs):
kwargs['multiple_ou'] = utils.get_ou_count() > 1
return super().get_context_data(**kwargs)
class MediaMixin(metaclass=MediaMixinBase):
'''Expose needed CSS and JS files as a media object'''
class Media:
js = (
xstatic('jquery.js', 'jquery.min.js'),
reverse_lazy('a2-manager-javascript-catalog'),
xstatic('jquery-ui.js', 'jquery-ui.min.js'),
'js/gadjo.js',
'jquery/js/jquery.form.js',
'admin/js/urlify.js',
'authentic2/js/purl.js',
'authentic2/manager/js/manager.js',
)
css = {'all': ('authentic2/manager/css/style.css',)}
def get_context_data(self, **kwargs):
kwargs['media'] = self.media
ctx = super().get_context_data(**kwargs)
if 'form' in ctx:
ctx['media'] += ctx['form'].media
return ctx
class PermissionMixin:
'''Control access to views based on permissions'''
permissions = None
permissions_global = False
permission_model = None
permission_pk_url_kwarg = None
def authorize(self, request, *args, **kwargs):
model = self.get_permission_model()
if model and not self.permissions_global:
app_label = model._meta.app_label
model_name = model._meta.model_name
add_perm = '%s.add_%s' % (app_label, model_name)
self.can_add = request.user.has_perm_any(add_perm)
permission_object = self.get_permission_object()
if permission_object:
self.object = permission_object
permissions = ('view', 'change', 'delete', 'manage_members')
for permission in permissions:
perm = '%s.%s_%s' % (app_label, permission, model_name)
setattr(self, 'can_' + permission, request.user.has_perm(perm, permission_object))
if self.permissions and not request.user.has_perms(self.permissions, permission_object):
raise PermissionDenied
elif self.permissions and not request.user.has_perm_any(self.permissions):
raise PermissionDenied
else:
if self.permissions:
if self.permissions_global and not request.user.has_perms(self.permissions):
raise PermissionDenied
if not self.permissions_global and not request.user.has_perm_any(self.permissions):
raise PermissionDenied
def get_permission_model(self):
return self.permission_model or getattr(self, 'model', None)
def get_permission_object(self):
if self.permission_model and self.permission_pk_url_kwarg:
try:
return self.permission_model.objects.get(pk=self.kwargs[self.permission_pk_url_kwarg])
except self.permission_model.DoesNotExist:
raise Http404(
gettext("No %(verbose_name)s found matching the query")
% {'verbose_name': self.permission_model._meta.verbose_name}
)
elif hasattr(self, 'get_object') and (
(hasattr(self, 'pk_url_kwarg') and self.pk_url_kwarg in self.kwargs)
or (hasattr(self, 'slug_url_kwarg') and self.slug_url_kwarg in self.kwargs)
):
return self.get_object()
else:
return None
def dispatch(self, request, *args, **kwargs):
response = self.authorize(request, *args, **kwargs) # pylint: disable=assignment-from-no-return
if response is not None:
return response
return super().dispatch(request, *args, **kwargs)
def filter_view(request, qs):
model = qs.model
perm = '%s.search_%s' % (model._meta.app_label, model._meta.model_name)
return request.user.filter_by_perm(perm, qs)
class FilterQuerysetByPermMixin:
def get_queryset(self):
qs = super().get_queryset()
return filter_view(self.request, qs)
class FilterTableQuerysetByPermMixin:
def get_table_data(self):
qs = super().get_table_data()
if getattr(self, 'filter_table_by_perm', True):
qs = filter_view(self.request, qs)
return qs
class TableQuerysetMixin:
def get_table_queryset(self):
return self.get_queryset()
def get_table_data(self):
return self.get_table_queryset()
class SearchFormMixin:
"""Handle a search form on the current table view.
The search form class must implement a .filter(qs) method returning a new queryset."""
search_form_class = None
def get_search_form_class(self):
return self.search_form_class
def get_search_form_kwargs(self):
return {'request': self.request, 'data': self.request.GET.dict()}
def get_search_form(self):
form_class = self.get_search_form_class()
if not form_class:
return
return form_class(**self.get_search_form_kwargs())
def dispatch(self, request, *args, **kwargs):
self.search_form = self.get_search_form()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if self.search_form:
ctx['search_form'] = self.search_form
return ctx
def filter_by_search(self, qs):
if self.search_form and self.search_form.is_valid():
qs = self.search_form.filter(qs)
return qs
def get_table_data(self):
qs = super().get_table_data()
qs = self.filter_by_search(qs)
return qs
class FormatsContextData:
'''Export list of supported formats in context'''
formats = ['csv']
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formats'] = self.formats
return ctx
class Action:
'''Describe an action for view supporting multiples actions.'''
name = None
title = None
confirm = None
url_name = None
url = None
popup = True
permission = None
def __init__(
self, name=None, title=None, confirm=None, url_name=None, url=None, popup=None, permission=None
):
if name is not None:
self.name = name
if title is not None:
self.title = title
if confirm is not None:
self.confirm = confirm
if url_name is not None:
self.url_name = url_name
if url is not None:
self.url = url
if popup is not None:
self.popup = popup
if permission is not None:
self.permission = permission
def display(self, instance, request):
if self.permission:
return request.user.has_perm(self.permission, instance)
return True
class AjaxFormViewMixin:
'''Implement a JSON response for view which can be included in an AJAX popup'''
success_url = '.'
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
return self.return_ajax_response(request, response)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['request_is_ajax'] = is_ajax(self.request)
return ctx
def return_ajax_response(self, request, response):
if not is_ajax(request):
return response
data = {}
if 'Location' in response:
location = response['Location']
# empty location means that the view can be used from anywhere
# and so the redirect URL should not be used
# otherwise compute an absolute URI from the relative URI
if location and (
not location.startswith('http://')
or not location.startswith('https://')
or not location.startswith('/')
):
location = request.build_absolute_uri(location)
data['location'] = location
if hasattr(response, 'render'):
response.render()
data['content'] = force_str(response.content)
return HttpResponse(json.dumps(data), content_type='application/json')
class TitleMixin:
'''Mixin to provide a title to the view's template'''
title = ''
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['title'] = self.title
ctx['manager_site_title'] = app_settings.SITE_TITLE
return ctx
class ActionMixin:
'''Describe the main action implementd by a view'''
action = None
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if self.action:
ctx['action'] = self.action
return ctx
class OtherActionsMixin:
'''Describe secondary actions possible on a view'''
other_actions = None
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['other_actions'] = tuple(self.get_displayed_other_actions())
return ctx
def get_other_actions(self):
return self.other_actions or []
def get_displayed_other_actions(self):
actions = []
other_actions = list(self.get_other_actions())
hooks.call_hooks('manager_modify_other_actions', self, other_actions)
for action in other_actions:
if callable(action.display) and not action.display(self.object, self.request):
continue
if action.display:
actions.append(action)
return actions
def post(self, request, *args, **kwargs):
self.object = self.get_object()
for action in self.get_displayed_other_actions():
if action.name in request.POST:
response = None
if hasattr(action, 'do'):
response = action.do(self, request, self.object)
else:
method = getattr(self, 'action_' + action.name, None)
if method:
response = method(request, *args, **kwargs)
hooks.call_hooks(
'event',
name='manager-action',
user=self.request.user,
action=action,
instance=self.object,
)
if response:
return response
self.request.method = 'GET'
return self.get(request, *args, **kwargs)
parent = super()
if hasattr(parent, 'post'):
return parent.post(request, *args, **kwargs)
return self.get(request, *args, **kwargs)
class ExportMixin:
'''Help in implementd export views'''
http_method_names = ['get', 'head', 'options']
export_prefix = ''
def get_export_prefix(self):
return self.export_prefix
def get_resource(self):
return self.resource_class()
def get_data(self):
qs = self.get_table_data()
return batch_queryset(qs)
def get_dataset(self):
return self.get_resource().export(self.get_data())
def get(self, request, *args, **kwargs):
if kwargs['format'].lower() != 'csv':
raise Http404('unknown format')
# use QUOTE_ALL to prevent CSV injection, see https://owasp.org/www-community/attacks/CSV_Injection
content = self.get_dataset().get_csv(quoting=csv.QUOTE_ALL)
return self.export_response(content=content, content_type='text/csv', export_format='csv')
def export_response(self, content, content_type, export_format):
response = HttpResponse(content, content_type=content_type)
filename = '%s%s.%s' % (self.get_export_prefix(), now().strftime('%Y%m%d_%H%M%S'), export_format)
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
return response
class FormNeedsRequest:
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if getattr(self.get_form_class(), 'need_request', False):
kwargs['request'] = self.request
return kwargs
class ModelNameMixin(MediaMixin):
'''Mixin to provide a model name to view's template'''
def get_model_name(self):
return self.model._meta.verbose_name
def get_instance_name(self):
if hasattr(self, 'get_object'):
return str(self.get_object())
return ''
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['model_name'] = self.get_model_name()
return ctx
class TableHookMixin:
'''Helper class for table views, hiding the OU column from tables if an OU filter exists'''
def get_table(self, **kwargs):
table = super().get_table(**kwargs)
hooks.call_hooks('manager_modify_table', self, table)
return table
class BaseTableView(
MultipleOUMixin,
TitleMixin,
TableHookMixin,
FormatsContextData,
ModelNameMixin,
PermissionMixin,
SearchFormMixin,
FilterQuerysetByPermMixin,
TableQuerysetMixin,
SingleTableView,
):
'''Base class for views showing a table of objects'''
class SubTableViewMixin(
TableHookMixin,
FormatsContextData,
ModelNameMixin,
PermissionMixin,
SearchFormMixin,
FilterTableQuerysetByPermMixin,
TableQuerysetMixin,
SingleObjectMixin,
SingleTableMixin,
ContextMixin,
):
'''Helper class for views showing a table of objects related to one object'''
context_object_name = 'object'
paginate_by = None
class SimpleSubTableView(TitleMixin, SubTableViewMixin, TemplateView):
'''Base class for views showing a simple table of objects related to one object'''
class BaseSubTableView(MultipleOUMixin, TitleMixin, SubTableViewMixin, FormNeedsRequest, FormView):
'''Base class for views showing a table of objects related to one object'''
success_url = '.'
class BaseDeleteView(TitleMixin, ModelNameMixin, PermissionMixin, AjaxFormViewMixin, DeleteView):
'''Base class for views implementing deletion of an object'''
template_name = 'authentic2/manager/delete.html'
context_object_name = 'object'
@property
def permissions(self):
app_label = self.model._meta.app_label
model_name = self.model._meta.model_name
return ['%s.delete_%s' % (app_label, model_name)]
@property
def title(self):
return _('Delete %s') % self.get_instance_name()
def get_success_url(self):
return '../../'
class ModelFormView(MediaMixin, FormNeedsRequest):
'''Base class for views showing a form for a model'''
fields = None
form_class = None
def get_fields(self):
return self.fields
def get_form_class(self):
return modelform_factory(self.model, form=self.form_class, fields=self.get_fields())
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
hooks.call_hooks('manager_modify_form', self, form)
return form
class BaseDetailView(MultipleOUMixin, TitleMixin, ModelNameMixin, PermissionMixin, ModelFormView, DetailView):
context_object_name = 'object'
form_class = None
@property
def permissions(self):
app_label = self.model._meta.app_label
model_name = self.model._meta.model_name
return ['%s.view_%s' % (app_label, model_name)]
def get_form(self):
form_class = self.get_form_class()
if getattr(form_class, 'need_request', False):
form = form_class(request=self.request, instance=self.object)
else:
form = form_class(instance=self.object)
for field in form.fields.values():
widget = field.widget
widget.attrs['disabled'] = ''
if 'readonly' in widget.attrs:
del widget.attrs['readonly']
return form
def get_context_data(self, **kwargs):
form = self.get_form()
hooks.call_hooks('manager_modify_form', self, form)
kwargs['form'] = form
ctx = super().get_context_data(**kwargs)
return ctx
class BaseAddView(
MultipleOUMixin, TitleMixin, ModelNameMixin, PermissionMixin, AjaxFormViewMixin, ModelFormView, CreateView
):
'''Base class for views for adding an instance of a model'''
template_name = 'authentic2/manager/form.html'
success_view_name = None
context_object_name = 'object'
@property
def permissions(self):
app_label = self.model._meta.app_label
model_name = self.model._meta.model_name
return ['%s.add_%s' % (app_label, model_name)]
@property
def title(self):
return 'Add %s' % super().get_model_name()
def get_success_url(self):
return reverse(self.success_view_name, kwargs={'pk': self.object.pk})
class BaseEditView(
MultipleOUMixin,
SuccessMessageMixin,
TitleMixin,
ModelNameMixin,
PermissionMixin,
AjaxFormViewMixin,
ModelFormView,
UpdateView,
):
'''Base class for views for editing an instance of a model'''
template_name = 'authentic2/manager/form.html'
context_object_name = 'object'
@property
def permissions(self):
app_label = self.model._meta.app_label
model_name = self.model._meta.model_name
return ['%s.change_%s' % (app_label, model_name)]
@property
def title(self):
return _('Edit %s') % self.get_instance_name()
def get_success_url(self):
return '..'
class HomepageView(TitleMixin, PermissionMixin, MediaMixin, TemplateView):
template_name = 'authentic2/manager/homepage.html'
permissions = [
'a2_rbac.search_role',
'a2_rbac.search_organizationalunit',
'auth.search_group',
'custom_user.search_user',
]
default_entries = [
{
'class': 'icon-identity-management',
'href': reverse_lazy('a2-manager-homepage'),
'label': _('Identity management'),
'order': -1,
'sub': False,
'skip_homepage': True,
'slug': 'identity-management',
},
{
'class': 'icon-organizational-units',
'href': reverse_lazy('a2-manager-ous'),
'label': _('Organizational units'),
'help_text': _('Organizational units are used to group users and roles.'),
'order': 1,
'permissions': 'a2_rbac.search_organizationalunit',
'skip_menu': True,
'slug': 'organizational-units',
},
{
'class': 'icon-users',
'href': reverse_lazy('a2-manager-users'),
'label': _('Users'),
'help_text': _('Users are the main actors in identity management.'),
'order': -1,
'permissions': 'custom_user.search_user',
'sub': True,
'slug': 'users',
},
{
'class': 'icon-roles',
'href': reverse_lazy('a2-manager-roles'),
'label': _('Roles'),
'help_text': _('Roles are used to give some user specific access rights.'),
'order': -1,
'permissions': 'a2_rbac.search_role',
'sub': True,
'slug': 'roles',
},
{
'class': 'icon-services',
'href': reverse_lazy('a2-manager-services'),
'label': _('Services'),
'help_text': _('Services are applications using this central authority for identities.'),
'order': 1,
'permissions': 'authentic2.search_service',
'skip_menu': True,
'slug': 'services',
},
{
'label': _('Authentication frontends'),
'slug': 'authn',
'href': reverse_lazy('a2-manager-authenticators'),
'permissions': 'authenticators.search_baseauthenticator',
'place': 'sidebar',
},
{
'label': _('Global journal'),
'slug': 'journal',
'href': reverse_lazy('a2-manager-journal'),
'permissions': ['custom_user.view_user', 'a2_rbac.view_role'],
'permissions_global': True,
'place': 'sidebar',
},
{
'label': _('Directory servers'),
'slug': 'tech-info',
'href': reverse_lazy('a2-manager-tech-info'),
'permissions': 'superuser',
'place': 'sidebar',
'condition': lambda: bool(ldap_backend.LDAPBackend.get_config()),
},
{
'label': _('API Clients'),
'slug': 'api-clients',
'href': reverse_lazy('a2-manager-api-clients'),
'permissions': ['authentic2.admin_apiclient'],
'place': 'sidebar',
},
]
def dispatch(self, request, *args, **kwargs):
if app_settings.HOMEPAGE_URL:
return redirect(request, app_settings.HOMEPAGE_URL)
return super().dispatch(request, *args, **kwargs)
def get_homepage_entries(self):
entries = []
for hook_entries in itertools.chain(
self.default_entries, hooks.call_hooks('manager_homepage_entries', self)
):
if not hasattr(hook_entries, 'append'):
hook_entries = [hook_entries]
for entry in hook_entries:
permissions = entry.get('permissions')
if permissions == 'superuser' and not self.request.user.is_superuser:
continue
if permissions:
if entry.get('permissions_global'):
if not self.request.user.has_perms(permissions):
continue
else:
if not self.request.user.has_perm_any(permissions):
continue
condition = entry.get('condition')
if condition and not condition():
continue
entries.append(entry)
# use possible key order to sort
# list.sort() is supposed to be a stable sort (already sorted entries
# are kept in the same order)
entries.sort(key=lambda d: d.get('order', 0))
return entries
def get_context_data(self, **kwargs):
entries = []
sidebar_entries = []
for entry in self.get_homepage_entries():
if entry.get('skip_homepage', False):
continue
if entry.get('place') == 'sidebar':
sidebar_entries.append(entry)
else:
entries.append(entry)
kwargs['entries'] = entries
kwargs['sidebar_entries'] = sidebar_entries
kwargs['bg_image_random'] = random.randint(1, 8)
return super().get_context_data(**kwargs)
homepage = HomepageView.as_view()
class TechnicalInformationView(TitleMixin, MediaMixin, TemplateView):
template_name = 'authentic2/manager/tech_info.html'
title = _("Technical information")
def get(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
import ldap
backend = ldap_backend.LDAPBackend
kwargs['ldap_list'] = []
for block in backend.get_config():
config = block.copy()
try:
conn = backend.get_connection(config, raises=True)
except ldap.LDAPError as e:
config['error'] = True
config['errmsg'] = str(e)
else:
config['block'] = json.dumps(block, indent=2, ensure_ascii=False)
# retrieve ldap uri, not directly visible in configuration block
config['ldap_uri'] = conn.get_option(ldap.OPT_URI)
# user filters need to be formatted to ldapsearch syntax
config['user_filter'] = force_str(block.get('user_filter'), '').replace('%s', '*')
config['sync_ldap_users_filter'] = (
force_str(block.get('sync_ldap_users_filter'), '').replace('%s', '*').replace('%s', '*')
)
kwargs['ldap_list'].append(config)
return super().get_context_data(**kwargs)
tech_info = TechnicalInformationView.as_view()
class MenuJson(HomepageView):
def get(self, request, *args, **kwargs):
menu_entries = []
for entry in self.get_homepage_entries():
if entry.get('place') == 'sidebar':
continue
if entry.get('skip_menu', False):
continue
menu_entries.append(
{
'label': str(entry['label']),
'slug': entry.get('slug', ''),
'url': request.build_absolute_uri(str(entry['href'])),
'sub': entry.get('sub', False),
}
)
return menu_entries
menu_json = json_view(MenuJson.as_view())
class HideOUColumnMixin:
'''Helper class for table views, hiding the OU column from tables if an OU filter exists'''
def get_table(self, **kwargs):
exclude_ou = False
if (
hasattr(self, 'search_form')
and self.search_form.is_valid()
and self.search_form.cleaned_data.get('ou') is not None
):
exclude_ou = True
if OrganizationalUnit.objects.count() < 2:
exclude_ou = True
if exclude_ou:
exclude = kwargs.setdefault('exclude', [])
if 'ou' not in exclude:
exclude.append('ou')
return super().get_table(**kwargs)
class Select2View(AutoResponseView):
'''Overrided default django-select2 view to enforce security checks on Select2 AJAX requests.'''
def get_widget_or_404(self):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'filter_by_perm'):
raise Http404('Invalid user')
field_data = self.kwargs.get('field_id', self.request.GET.get('field_id', None))
try:
field_data = crypto.loads(field_data)
except (crypto.SignatureExpired, crypto.BadSignature):
raise Http404('Invalid or expired signature.')
widget_class = field_data.get('class')
if not widget_class or not hasattr(widgets, widget_class):
raise Http404('Missing or unknown widget class.')
widget = getattr(widgets, widget_class)()
if not isinstance(
widget, (widgets.SimpleModelSelect2Widget, widgets.SimpleModelSelect2MultipleWidget)
):
raise Http404('Reference to invalid widget class')
qs = widget.get_queryset()
qs.query.where = pickle.loads(base64.b64decode(field_data['where_clause']))
# check permissions again as current user may not be the one who obtained the field_id
perm = '%s.search_%s' % (qs.model._meta.app_label, qs.model._meta.model_name)
qs = self.request.user.filter_by_perm(perm, qs)
widget.queryset = qs
widget.view = self
return widget
select2 = Select2View.as_view()
class SiteExport(View):
def get(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied
return HttpResponse(json.dumps(export_site(), indent=4), content_type='application/json')
site_export = SiteExport.as_view()
class SiteImportView(MediaMixin, TitleMixin, FormView):
form_class = forms.SiteImportForm
template_name = 'authentic2/manager/import_form.html'
success_url = reverse_lazy('a2-manager-homepage')
title = _('Site Import')
def form_valid(self, form):
try:
with transaction.atomic():
import_site(form.cleaned_data['site_json'], ImportContext())
except ValidationError as e:
form.add_error('site_json', e)
return self.form_invalid(form)
return super().form_valid(form)
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
site_import = SiteImportView.as_view()
class SearchOUMixin:
@cached_property
def ou(self):
try:
ou_id = int(self.request.GET['search-ou'])
except (ValueError, KeyError):
return None
else:
return OrganizationalUnit.objects.filter(pk=ou_id).first()
def get_context_data(self, **kwargs):
return super().get_context_data(ou=self.ou, **kwargs)
class PermissionDeniedView(MediaMixin, TemplateView):
template_name = 'authentic2/manager/403.html'
def render_to_response(self, context, **response_kwargs):
response_kwargs['status'] = 403
return super().render_to_response(context, **response_kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['exception'] = self.kwargs['exception']
return context
permission_denied = PermissionDeniedView.as_view()