Added support for raw hash password edditing
This commit is contained in:
parent
e804a1d102
commit
3e1d9f7d22
4
TODO.md
4
TODO.md
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',)),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
73
orchestra/contrib/orchestration/managers.py
Normal file
73
orchestra/contrib/orchestration/managers.py
Normal 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
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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¤cy=%s' % (
|
||||
trans.bill.account_id,
|
||||
|
|
|
@ -99,7 +99,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
'display_state',
|
||||
'amount',
|
||||
'currency',
|
||||
'process'
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue