Added support for raw hash password edditing

This commit is contained in:
Marc Aymerich 2016-05-11 12:56:10 +00:00
parent e804a1d102
commit 3e1d9f7d22
15 changed files with 200 additions and 40 deletions

View File

@ -460,3 +460,7 @@ with open(file) as handler:
# change filter By PHP version: by detail
# Mark transaction process as executed should not override higher transaction states
# Show password and set password management commands -sam -A|--all --systemuser --account --mailbox vs raw passwords on forms

View File

@ -3,6 +3,7 @@ from functools import partial
from django import forms
from django.contrib.admin import helpers
from django.contrib.auth.hashers import identify_hasher
from django.core import validators
from django.forms.models import modelformset_factory, BaseModelFormSet
from django.template import Template, Context
@ -84,8 +85,11 @@ class AdminPasswordChangeForm(forms.Form):
error_messages = {
'password_mismatch': _("The two password fields didn't match."),
'password_missing': _("No password has been provided."),
'bad_hash': _("Invalid password format or unknown hashing algorithm."),
}
required_css_class = 'required'
password = forms.CharField(label=_("Password"), required=False,
widget=forms.TextInput(attrs={'size':'120'}))
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput,
required=False, validators=[validate_password])
password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput,
@ -93,9 +97,14 @@ class AdminPasswordChangeForm(forms.Form):
def __init__(self, user, *args, **kwargs):
self.related = kwargs.pop('related', [])
self.raw = kwargs.pop('raw', False)
self.user = user
super(AdminPasswordChangeForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.password_provided = False
for ix, rel in enumerate(self.related):
self.fields['password_%i' % ix] = forms.CharField(label=_("Password"), required=False,
widget=forms.TextInput(attrs={'size':'120'}))
setattr(self, 'clean_password_%i' % ix, partial(self.clean_password, ix=ix))
self.fields['password1_%i' % ix] = forms.CharField(label=_("Password"),
widget=forms.PasswordInput, required=False)
self.fields['password2_%i' % ix] = forms.CharField(label=_("Password (again)"),
@ -108,35 +117,53 @@ class AdminPasswordChangeForm(forms.Form):
password1 = self.cleaned_data.get('password1%s' % ix)
password2 = self.cleaned_data.get('password2%s' % ix)
if password1 and password2:
self.password_provided = True
if password1 != password2:
raise forms.ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
elif password1 or password2:
self.password_provided = True
raise forms.ValidationError(
self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2
def clean_password(self, ix=''):
if ix != '':
ix = '_%i' % ix
password = self.cleaned_data.get('password%s' % ix)
if password:
self.password_provided = True
try:
hasher = identify_hasher(password)
except ValueError:
raise forms.ValidationError(
self.error_messages['bad_hash'],
code='bad_hash',
)
return password
def clean(self):
cleaned_data = super(AdminPasswordChangeForm, self).clean()
for data in cleaned_data.values():
if data:
return
raise forms.ValidationError(
self.error_messages['password_missing'],
code='password_missing',
)
if not self.password_provided:
raise forms.ValidationError(
self.error_messages['password_missing'],
code='password_missing',
)
def save(self, commit=True):
"""
Saves the new password.
"""
password = self.cleaned_data["password1"]
field_name = 'password' if self.raw else 'password1'
password = self.cleaned_data[field_name]
if password:
self.user.set_password(password)
if self.raw:
self.password = password
else:
self.user.set_password(password)
if commit:
try:
self.user.save(update_fields=['password'])
@ -144,16 +171,19 @@ class AdminPasswordChangeForm(forms.Form):
# password is not a field but an attribute
self.user.save() # Trigger the backend
for ix, rel in enumerate(self.related):
password = self.cleaned_data['password1_%s' % ix]
password = self.cleaned_data['%s_%s' % (field_name, ix)]
if password:
set_password = getattr(rel, 'set_password')
set_password(password)
if raw:
rel.password = password
else:
set_password = getattr(rel, 'set_password')
set_password(password)
if commit:
rel.save(update_fields=['password'])
return self.user
def _get_changed_data(self):
data = super(AdminPasswordChangeForm, self).changed_data
data = super().changed_data
for name in self.fields.keys():
if name not in data:
return []
@ -173,7 +203,7 @@ class SendEmailForm(forms.Form):
widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
def __init__(self, *args, **kwargs):
super(SendEmailForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
initial = kwargs.get('initial')
if 'to' in initial:
self.fields['to'].widget = SpanWidget(original=initial['to'])

View File

@ -7,7 +7,7 @@ from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import unquote
from django.contrib.auth import update_session_auth_hash
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect, Http404
from django.http import HttpResponseRedirect, Http404, HttpResponse
from django.forms.models import BaseInlineFormSet
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
@ -17,9 +17,12 @@ from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
from orchestra.models.utils import has_db_field
from ..utils.python import random_ascii, pairwise
from .forms import AdminPasswordChangeForm
#, AdminRawPasswordChangeForm
#from django.contrib.auth.forms import AdminPasswordChangeForm
from .utils import action_to_view
@ -240,8 +243,11 @@ class ChangePasswordAdminMixin(object):
return [
url(r'^(\d+)/password/$',
self.admin_site.admin_view(self.change_password),
name='%s_%s_change_password' % info)
] + super(ChangePasswordAdminMixin, self).get_urls()
name='%s_%s_change_password' % info),
url(r'^(\d+)/hash/$',
self.admin_site.admin_view(self.show_hash),
name='%s_%s_show_hash' % info)
] + super().get_urls()
def get_change_password_username(self, obj):
return str(obj)
@ -252,7 +258,10 @@ class ChangePasswordAdminMixin(object):
raise PermissionDenied
# TODO use this insetad of self.get_object(), in other places
obj = get_object_or_404(self.get_queryset(request), pk=id)
raw = request.GET.get('raw', '0') == '1'
can_raw = has_db_field(obj, 'password')
if raw and not can_raw:
raise TypeError("%s has no password db field for raw password edditing." % obj)
related = []
for obj_name_attr in ('username', 'name', 'hostname'):
try:
@ -268,12 +277,12 @@ class ChangePasswordAdminMixin(object):
else:
account = obj
if account.username == obj_name:
for rel in account.get_related_passwords():
for rel in account.get_related_passwords(db_field=raw):
if not isinstance(obj, type(rel)):
related.append(rel)
if request.method == 'POST':
form = self.change_password_form(obj, request.POST, related=related)
form = self.change_password_form(obj, request.POST, related=related, raw=raw)
if form.is_valid():
form.save()
change_message = self.construct_change_message(request, form, None)
@ -283,18 +292,18 @@ class ChangePasswordAdminMixin(object):
update_session_auth_hash(request, form.user) # This is safe
return HttpResponseRedirect('..')
else:
form = self.change_password_form(obj, related=related)
form = self.change_password_form(obj, related=related, raw=raw)
fieldsets = [
(obj._meta.verbose_name.capitalize(), {
'classes': ('wide',),
'fields': ('password1', 'password2')
'fields': ('password',) if raw else ('password1', 'password2'),
}),
]
for ix, rel in enumerate(related):
fieldsets.append((rel._meta.verbose_name.capitalize(), {
'classes': ('wide',),
'fields': ('password1_%i' % ix, 'password2_%i' % ix)
'fields': ('password_%i' % ix,) if raw else ('password1_%i' % ix, 'password2_%i' % ix)
}))
obj_username = self.get_change_password_username(obj)
@ -302,6 +311,8 @@ class ChangePasswordAdminMixin(object):
context = {
'title': _('Change password: %s') % obj_username,
'adminform': adminForm,
'raw': raw,
'can_raw': can_raw,
'errors': admin.helpers.AdminErrorList(form, []),
'form_url': form_url,
'is_popup': (IS_POPUP_VAR in request.POST or
@ -322,3 +333,9 @@ class ChangePasswordAdminMixin(object):
return TemplateResponse(request,
self.change_user_password_template,
context, current_app=self.admin_site.name)
def show_hash(self, request, id):
if not request.user.is_superuser:
raise PermissionDenied
obj = get_object_or_404(self.get_queryset(request), pk=id)
return HttpResponse(obj.password)

View File

@ -246,3 +246,8 @@ PASSLIB_CONFIG = (
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
"superuser__sha512_crypt__default_rounds = 120000\n"
)
SHELL_PLUS_PRE_IMPORTS = (
('orchestra.contrib.orchestration.managers', ('orchestrate',)),
)

View File

@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
#from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
#from orchestra.contrib.orchestration import Operation
from orchestra.core import services
from orchestra.models.utils import has_db_field
from orchestra.utils.mail import send_email_template
from . import settings
@ -158,7 +159,7 @@ class Account(auth.AbstractBaseUser):
return True
return auth._user_has_module_perms(self, app_label)
def get_related_passwords(self):
def get_related_passwords(self, db_field=False):
related = [
self.main_systemuser,
]
@ -173,5 +174,8 @@ class Account(auth.AbstractBaseUser):
rel = model.objects.get(account=self, **kwargs)
except model.DoesNotExist:
continue
if db_field:
if not has_db_field(rel, 'password'):
continue
related.append(rel)
return related

View File

@ -112,7 +112,8 @@ class DatabaseUserChangeForm(forms.ModelForm):
password = ReadOnlySQLPasswordHashField(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>."))
"using <a href='../password/'>this form</a>. "
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
class Meta:
model = DatabaseUser

View File

@ -3,7 +3,6 @@ from django.shortcuts import redirect
def last(modeladmin, request, queryset):
last_id = queryset.order_by('id').values_list('id', flat=True).first()
url = reverse('admin:mailer_message_change', args=(last_id,))
print(url)
last = queryset.model.objects.latest('id')
url = reverse('admin:mailer_message_change', args=(last.pk,))
return redirect(url)

View File

@ -103,7 +103,8 @@ def get_backend_url(ids):
return ''
def message_user(request, logs):
def get_messages(logs):
messages = []
total, successes, async = 0, 0, 0
ids = []
async_ids = []
@ -140,7 +141,7 @@ def message_user(request, logs):
msg += ', ' + str(async_msg)
msg = msg.format(errors=errors, async=async, async_url=async_url, total=total, url=url,
name=log.backend)
messages.error(request, mark_safe(msg + '.'))
messages.append(('error', msg + '.'))
elif successes:
if async_msg:
if total == 1:
@ -160,7 +161,13 @@ def message_user(request, logs):
total=total, url=url, async_url=async_url, async=async, successes=successes,
name=log.backend
)
messages.success(request, mark_safe(msg + '.'))
messages.append(('success', msg + '.'))
else:
msg = async_msg.format(url=url, async_url=async_url, async=async, name=log.backend)
messages.success(request, mark_safe(msg + '.'))
messages.append(('success', msg + '.'))
return messages
def message_user(request, logs):
for func, msg in get_messages(logs):
getattr(messages, func)(request, mark_safe(msg))

View File

@ -0,0 +1,73 @@
import sys
from threading import local
from django.contrib.admin.models import LogEntry
from django.db.models.signals import pre_delete, post_save, m2m_changed
from django.dispatch import receiver
from django.utils.decorators import ContextDecorator
from orchestra.utils.python import OrderedSet
from . import manager, Operation, helpers
from .middlewares import OperationsMiddleware
from .models import BackendLog, BackendOperation
@receiver(post_save, dispatch_uid='orchestration.post_save_manager_collector')
def post_save_collector(sender, *args, **kwargs):
if sender not in (BackendLog, BackendOperation, LogEntry):
instance = kwargs.get('instance')
orchestrate.collect(Operation.SAVE, **kwargs)
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_manager_collector')
def pre_delete_collector(sender, *args, **kwargs):
if sender not in (BackendLog, BackendOperation, LogEntry):
orchestrate.collect(Operation.DELETE, **kwargs)
@receiver(m2m_changed, dispatch_uid='orchestration.m2m_manager_collector')
def m2m_collector(sender, *args, **kwargs):
# m2m relations without intermediary models are shit. Model.post_save is not sent and
# by the time related.post_save is sent rel objects are not accessible via RelatedManager.all()
if kwargs.pop('action') == 'post_add' and kwargs['pk_set']:
orchestrate.collect(Operation.SAVE, **kwargs)
class orchestrate(ContextDecorator):
thread_locals = local()
thread_locals.pending_operations = None
thread_locals.route_cache = None
@classmethod
def collect(cls, action, **kwargs):
""" Collects all pending operations derived from model signals """
if cls.thread_locals.pending_operations is None:
# No active orchestrate context manager
return
kwargs['operations'] = cls.thread_locals.pending_operations
kwargs['route_cache'] = cls.thread_locals.route_cache
instance = kwargs.pop('instance')
manager.collect(instance, action, **kwargs)
def __enter__(self):
cls = type(self)
self.old_pending_operations = cls.thread_locals.pending_operations
cls.thread_locals.pending_operations = OrderedSet()
self.old_route_cache = cls.thread_locals.route_cache
cls.thread_locals.route_cache = {}
def __exit__(self, exc_type, exc_value, traceback):
cls = type(self)
if not exc_type:
operations = cls.thread_locals.pending_operations
if operations:
scripts, serialize = manager.generate(operations)
logs = manager.execute(scripts, serialize=serialize)
for t, msg in helpers.get_messages(logs):
if t == 'error':
sys.stderr.write('%s: %s\n' % (t, msg))
else:
sys.stdout.write('%s: %s\n' % (t, msg))
cls.thread_locals.pending_operations = self.old_pending_operations
cls.thread_locals.route_cache = self.old_route_cache

View File

@ -11,19 +11,19 @@ from orchestra.utils.python import OrderedSet
from . import manager, Operation
from .helpers import message_user
from .models import BackendLog
from .models import BackendLog, BackendOperation
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
def post_save_collector(sender, *args, **kwargs):
if sender not in (BackendLog, Operation, LogEntry):
if sender not in (BackendLog, BackendOperation, LogEntry):
instance = kwargs.get('instance')
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
def pre_delete_collector(sender, *args, **kwargs):
if sender not in (BackendLog, Operation, LogEntry):
if sender not in (BackendLog, BackendOperation, LogEntry):
OperationsMiddleware.collect(Operation.DELETE, **kwargs)

View File

@ -213,6 +213,11 @@ def reissue(modeladmin, request, queryset):
messages.error(request, _("One transaction should be selected."))
return
trans = queryset[0]
if trans.state != trans.REJECTED:
messages.error(request,
_("Only rejected transactions can be reissued, "
"please reject current transaction if necessary."))
return
url = reverse('admin:payments_transaction_add')
url += '?account=%i&bill=%i&source=%s&amount=%s&currency=%s' % (
trans.bill.account_id,

View File

@ -99,7 +99,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
'display_state',
'amount',
'currency',
'process'
)
}),
)

View File

@ -62,7 +62,8 @@ class UserChangeForm(forms.ModelForm):
password = auth_forms.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 it by "
"using <a href=\"../password/\">this form</a>."))
"using <a href='../password/'>this form</a>. "
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
def clean_password(self):
# Regardless of what the user provides, return the initial value.

View File

@ -1,4 +1,5 @@
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.apps import apps
import importlib
@ -16,6 +17,14 @@ def get_model(label, import_module=True):
return model
def has_db_field(obj, field_name):
try:
obj._meta.get_field(field_name)
except FieldDoesNotExist:
return False
return True
def get_field_value(obj, field_name):
names = field_name.split('__')
rel = getattr(obj, names.pop(0))

View File

@ -8,7 +8,13 @@
<div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
<p>{% blocktrans with username=obj_username %}Enter a new password for the user <strong>{{ username }}</strong>, suggestion '{{ password }}'.{% endblocktrans %}</p>
<p>
{% if raw %}
{% blocktrans with username=obj_username %}Enter a new password hash for user <strong>{{ username }}</strong>. Switch to <a href="./?raw=0">text password form</a>.{% endblocktrans %}
{% elif can_raw %}
{% blocktrans with username=obj_username %}Enter a new password for user <strong>{{ username }}</strong>, suggestion '{{ password }}'. Switch to <a href="./?raw=1">raw password form</a>.{% endblocktrans %}
{% endif %}
</p>
{% if errors %}
<p class="errornote">