425 lines
14 KiB
Python
425 lines
14 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 pprint
|
|
from copy import deepcopy
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib import admin
|
|
from django.contrib.admin.utils import flatten_fieldsets
|
|
from django.contrib.auth.admin import UserAdmin
|
|
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
|
from django.contrib.sessions.models import Session
|
|
from django.db import transaction
|
|
from django.utils import timezone
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.views.decorators.cache import never_cache
|
|
|
|
from . import app_settings, attribute_kinds, decorators, models, utils
|
|
from .custom_user.models import DeletedUser, User
|
|
from .forms.profile import BaseUserForm, modelform_factory
|
|
from .nonce.models import Nonce
|
|
|
|
|
|
def cleanup_action(modeladmin, request, queryset):
|
|
queryset.cleanup()
|
|
|
|
|
|
cleanup_action.short_description = _('Cleanup expired objects')
|
|
|
|
|
|
class CleanupAdminMixin(admin.ModelAdmin):
|
|
def get_actions(self, request):
|
|
actions = super(CleanupAdminMixin, self).get_actions(request)
|
|
if hasattr(self.model.objects.none(), 'cleanup'):
|
|
actions['cleanup_action'] = cleanup_action, 'cleanup_action', cleanup_action.short_description
|
|
return actions
|
|
|
|
|
|
class NonceModelAdmin(admin.ModelAdmin):
|
|
list_display = ("value", "context", "not_on_or_after")
|
|
|
|
|
|
admin.site.register(Nonce, NonceModelAdmin)
|
|
|
|
|
|
class AttributeValueAdmin(admin.ModelAdmin):
|
|
list_display = ('content_type', 'owner', 'attribute', 'content')
|
|
|
|
|
|
admin.site.register(models.AttributeValue, AttributeValueAdmin)
|
|
|
|
|
|
class LogoutUrlAdmin(admin.ModelAdmin):
|
|
list_display = ('provider', 'logout_url', 'logout_use_iframe', 'logout_use_iframe_timeout')
|
|
|
|
|
|
admin.site.register(models.LogoutUrl, LogoutUrlAdmin)
|
|
|
|
|
|
class AuthenticationEventAdmin(admin.ModelAdmin):
|
|
list_display = ('when', 'who', 'how', 'nonce')
|
|
list_filter = ('how',)
|
|
date_hierarchy = 'when'
|
|
search_fields = ('who', 'nonce', 'how')
|
|
|
|
|
|
admin.site.register(models.AuthenticationEvent, AuthenticationEventAdmin)
|
|
|
|
|
|
class UserExternalIdAdmin(admin.ModelAdmin):
|
|
list_display = ('user', 'source', 'external_id', 'created', 'updated')
|
|
list_filter = ('source',)
|
|
date_hierarchy = 'created'
|
|
search_fields = ('user__username', 'source', 'external_id')
|
|
|
|
|
|
admin.site.register(models.UserExternalId, UserExternalIdAdmin)
|
|
|
|
|
|
DB_SESSION_ENGINES = (
|
|
'django.contrib.sessions.backends.db',
|
|
'django.contrib.sessions.backends.cached_db',
|
|
'mellon.session_backends.cached_db',
|
|
)
|
|
|
|
if settings.SESSION_ENGINE in DB_SESSION_ENGINES:
|
|
|
|
class SessionAdmin(admin.ModelAdmin):
|
|
def _session_data(self, obj):
|
|
return pprint.pformat(obj.get_decoded()).replace('\n', '<br>\n')
|
|
|
|
_session_data.allow_tags = True
|
|
_session_data.short_description = _('session data')
|
|
list_display = ['session_key', 'ips', 'user', '_session_data', 'expire_date']
|
|
fields = ['session_key', 'ips', 'user', '_session_data', 'expire_date']
|
|
readonly_fields = ['ips', 'user', '_session_data']
|
|
date_hierarchy = 'expire_date'
|
|
actions = ['clear_expired']
|
|
|
|
def ips(self, session):
|
|
content = session.get_decoded()
|
|
ips = content.get('ips', set())
|
|
return ', '.join(ips)
|
|
|
|
ips.short_description = _('IP adresses')
|
|
|
|
def user(self, session):
|
|
from django.contrib import auth
|
|
from django.contrib.auth import models as auth_models
|
|
|
|
content = session.get_decoded()
|
|
if auth.SESSION_KEY not in content:
|
|
return
|
|
user_id = content[auth.SESSION_KEY]
|
|
if auth.BACKEND_SESSION_KEY not in content:
|
|
return
|
|
backend_class = content[auth.BACKEND_SESSION_KEY]
|
|
backend = auth.load_backend(backend_class)
|
|
try:
|
|
user = backend.get_user(user_id) or auth_models.AnonymousUser()
|
|
except Exception:
|
|
user = _('deleted user %r') % user_id
|
|
return user
|
|
|
|
user.short_description = _('user')
|
|
|
|
def clear_expired(self, request, queryset):
|
|
queryset.filter(expire_date__lt=timezone.now()).delete()
|
|
|
|
clear_expired.short_description = _('clear expired sessions')
|
|
|
|
admin.site.register(Session, SessionAdmin)
|
|
|
|
|
|
class ExternalUserListFilter(admin.SimpleListFilter):
|
|
title = _('external')
|
|
|
|
parameter_name = 'external'
|
|
|
|
def lookups(self, request, model_admin):
|
|
return (('1', _('Yes')), ('0', _('No')))
|
|
|
|
def queryset(self, request, queryset):
|
|
"""
|
|
Returns the filtered queryset based on the value
|
|
provided in the query string and retrievable via
|
|
`self.value()`.
|
|
"""
|
|
if self.value() == '1':
|
|
return queryset.filter(userexternalid__isnull=False)
|
|
elif self.value() == '0':
|
|
return queryset.filter(userexternalid__isnull=True)
|
|
return queryset
|
|
|
|
|
|
class UserRealmListFilter(admin.SimpleListFilter):
|
|
# Human-readable title which will be displayed in the
|
|
# right admin sidebar just above the filter options.
|
|
title = _('realm')
|
|
|
|
# Parameter for the filter that will be used in the URL query.
|
|
parameter_name = 'realm'
|
|
|
|
def lookups(self, request, model_admin):
|
|
"""
|
|
Returns a list of tuples. The first element in each
|
|
tuple is the coded value for the option that will
|
|
appear in the URL query. The second element is the
|
|
human-readable name for the option that will appear
|
|
in the right sidebar.
|
|
"""
|
|
return app_settings.REALMS
|
|
|
|
def queryset(self, request, queryset):
|
|
"""
|
|
Returns the filtered queryset based on the value
|
|
provided in the query string and retrievable via
|
|
`self.value()`.
|
|
"""
|
|
if self.value():
|
|
return queryset.filter(username__endswith=u'@' + self.value())
|
|
return queryset
|
|
|
|
|
|
class UserChangeForm(BaseUserForm):
|
|
error_messages = {
|
|
'missing_credential': _("You must at least give a username or an email to your user"),
|
|
}
|
|
|
|
password = ReadOnlyPasswordHashField(
|
|
label=_("Password"),
|
|
help_text=_(
|
|
"Raw passwords are not stored, so there is no way to see "
|
|
"this user's password, but you can change the password "
|
|
"using <a href=\"password/\">this form</a>."
|
|
),
|
|
)
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = '__all__'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(UserChangeForm, self).__init__(*args, **kwargs)
|
|
f = self.fields.get('user_permissions', None)
|
|
if f is not None:
|
|
f.queryset = f.queryset.select_related('content_type')
|
|
|
|
def clean_password(self):
|
|
# Regardless of what the user provides, return the initial value.
|
|
# This is done here, rather than on the field, because the
|
|
# field does not have access to the initial value
|
|
return self.initial["password"]
|
|
|
|
def clean(self):
|
|
if not self.cleaned_data.get('username') and not self.cleaned_data.get('email'):
|
|
raise forms.ValidationError(
|
|
self.error_messages['missing_credential'],
|
|
code='missing_credential',
|
|
)
|
|
|
|
def is_field_locked(self, name):
|
|
return False
|
|
|
|
|
|
class UserCreationForm(BaseUserForm):
|
|
"""
|
|
A form that creates a user, with no privileges, from the given username and
|
|
password.
|
|
"""
|
|
|
|
error_messages = {
|
|
'password_mismatch': _("The two password fields didn't match."),
|
|
'missing_credential': _("You must at least give a username or an email to your user"),
|
|
}
|
|
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
|
password2 = forms.CharField(
|
|
label=_("Password confirmation"),
|
|
widget=forms.PasswordInput,
|
|
help_text=_("Enter the same password as above, for verification."),
|
|
)
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ("username",)
|
|
|
|
def clean_password2(self):
|
|
password1 = self.cleaned_data.get("password1")
|
|
password2 = self.cleaned_data.get("password2")
|
|
if password1 and password2 and password1 != password2:
|
|
raise forms.ValidationError(
|
|
self.error_messages['password_mismatch'],
|
|
code='password_mismatch',
|
|
)
|
|
return password2
|
|
|
|
def clean(self):
|
|
if not self.cleaned_data.get('username') and not self.cleaned_data.get('email'):
|
|
raise forms.ValidationError(
|
|
self.error_messages['missing_credential'],
|
|
code='missing_credential',
|
|
)
|
|
|
|
def save(self, commit=True):
|
|
user = super(UserCreationForm, self).save(commit=False)
|
|
user.set_password(self.cleaned_data["password1"])
|
|
if commit:
|
|
user.save()
|
|
return user
|
|
|
|
|
|
class AuthenticUserAdmin(UserAdmin):
|
|
fieldsets = (
|
|
(None, {'fields': ('uuid', 'ou', 'password')}),
|
|
(_('Personal info'), {'fields': ('username', 'first_name', 'last_name', 'email')}),
|
|
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups')}),
|
|
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'deactivation')}),
|
|
)
|
|
add_fieldsets = (
|
|
(
|
|
None,
|
|
{
|
|
'classes': ('wide',),
|
|
'fields': ('ou', 'username', 'first_name', 'last_name', 'email', 'password1', 'password2'),
|
|
},
|
|
),
|
|
)
|
|
readonly_fields = ('uuid',)
|
|
list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter)
|
|
list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email']
|
|
actions = UserAdmin.actions + ['mark_as_inactive']
|
|
|
|
def get_fieldsets(self, request, obj=None):
|
|
fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj))
|
|
if obj:
|
|
if not request.user.is_superuser:
|
|
fieldsets[2][1]['fields'] = filter(lambda x: x != 'is_superuser', fieldsets[2][1]['fields'])
|
|
qs = models.Attribute.objects.all()
|
|
insertion_idx = 2
|
|
else:
|
|
qs = models.Attribute.objects.filter(required=True)
|
|
insertion_idx = 1
|
|
if qs.exists():
|
|
fieldsets = list(fieldsets)
|
|
fieldsets.insert(
|
|
insertion_idx,
|
|
(
|
|
_('Attributes'),
|
|
{'fields': [at.name for at in qs if at.name not in ['first_name', 'last_name']]},
|
|
),
|
|
)
|
|
return fieldsets
|
|
|
|
def get_form(self, request, obj=None, **kwargs):
|
|
self.form = modelform_factory(
|
|
self.model, form=UserChangeForm, fields=models.Attribute.objects.values_list('name', flat=True)
|
|
)
|
|
self.add_form = modelform_factory(
|
|
self.model,
|
|
form=UserCreationForm,
|
|
fields=models.Attribute.objects.filter(required=True).values_list('name', flat=True),
|
|
)
|
|
if 'fields' in kwargs:
|
|
fields = kwargs.pop('fields')
|
|
else:
|
|
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
|
|
if obj:
|
|
qs = models.Attribute.objects.all()
|
|
else:
|
|
qs = models.Attribute.objects.filter(required=True)
|
|
non_model_fields = set([a.name for a in qs]) - set(['first_name', 'last_name'])
|
|
fields = list(set(fields) - set(non_model_fields))
|
|
kwargs['fields'] = fields
|
|
return super(AuthenticUserAdmin, self).get_form(request, obj=obj, **kwargs)
|
|
|
|
@transaction.atomic
|
|
def mark_as_inactive(self, request, queryset):
|
|
timestamp = timezone.now()
|
|
for user in queryset:
|
|
user.mark_as_inactive(timestamp=timestamp)
|
|
|
|
mark_as_inactive.short_description = _('Mark as inactive')
|
|
|
|
|
|
admin.site.register(User, AuthenticUserAdmin)
|
|
|
|
|
|
class AttributeForm(forms.ModelForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super(AttributeForm, self).__init__(*args, **kwargs)
|
|
choices = self.kind_choices()
|
|
self.fields['kind'].choices = choices
|
|
self.fields['kind'].widget = forms.Select(choices=choices)
|
|
|
|
@decorators.to_iter
|
|
def kind_choices(self):
|
|
return attribute_kinds.get_choices()
|
|
|
|
class Meta:
|
|
model = models.Attribute
|
|
fields = '__all__'
|
|
|
|
|
|
class AttributeAdmin(admin.ModelAdmin):
|
|
form = AttributeForm
|
|
list_display = (
|
|
'label',
|
|
'disabled',
|
|
'name',
|
|
'kind',
|
|
'order',
|
|
'required',
|
|
'asked_on_registration',
|
|
'user_editable',
|
|
'user_visible',
|
|
)
|
|
list_editable = ('order',)
|
|
|
|
def get_queryset(self, request):
|
|
return self.model.all_objects.all()
|
|
|
|
|
|
admin.site.register(models.Attribute, AttributeAdmin)
|
|
|
|
|
|
class DeletedUserAdmin(admin.ModelAdmin):
|
|
list_display = ['deleted', 'old_user_id', 'old_uuid', 'old_email']
|
|
date_hierarchy = 'deleted'
|
|
search_fields = ['=old_user_id', '^old_uuid', 'old_email']
|
|
|
|
|
|
admin.site.register(DeletedUser, DeletedUserAdmin)
|
|
|
|
|
|
@never_cache
|
|
def login(request, extra_context=None):
|
|
return utils.redirect_to_login(request, login_url=utils.get_manager_login_url())
|
|
|
|
|
|
admin.site.login = login
|
|
|
|
|
|
@never_cache
|
|
def logout(request, extra_context=None):
|
|
return utils.redirect_to_login(request, login_url='auth_logout')
|
|
|
|
|
|
admin.site.logout = logout
|
|
|
|
admin.site.register(models.PasswordReset)
|