feat: treat django auth username as case insensitive throughout the datatracker (#5165)
* feat: insensitive username matching at django authentication * feat: use iexact when using the User object manager * fix: more places to ignore username case * fix: remove unused management command * fix: avoid get when probing for object existance * fix: force lowercase new usernames in secr/rolodex * fix: use explicit arguments when creating user
This commit is contained in:
parent
a10464a250
commit
c38ade6e1b
|
@ -192,7 +192,7 @@ def api_new_meeting_registration(request):
|
|||
except ValueError as e:
|
||||
return err(400, "Unexpected POST data: %s" % e)
|
||||
response = "Accepted, New registration" if created else "Accepted, Updated registration"
|
||||
if User.objects.filter(username=email).exists() or Email.objects.filter(address=email).exists():
|
||||
if User.objects.filter(username__iexact=email).exists() or Email.objects.filter(address=email).exists():
|
||||
pass
|
||||
else:
|
||||
send_account_creation_email(request, email)
|
||||
|
|
|
@ -36,7 +36,7 @@ def lookup_community_list(username=None, acronym=None):
|
|||
group = get_object_or_404(Group, acronym=acronym)
|
||||
clist = CommunityList.objects.filter(group=group).first() or CommunityList(group=group)
|
||||
else:
|
||||
user = get_object_or_404(User, username=username)
|
||||
user = get_object_or_404(User, username__iexact=username)
|
||||
clist = CommunityList.objects.filter(user=user).first() or CommunityList(user=user)
|
||||
|
||||
return clist
|
||||
|
|
21
ietf/ietfauth/backends.py
Normal file
21
ietf/ietfauth/backends.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
# From https://simpleisbetterthancomplex.com/tutorial/2017/02/06/how-to-implement-case-insensitive-username.html
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class CaseInsensitiveModelBackend(ModelBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
UserModel = get_user_model()
|
||||
if username is None:
|
||||
username = kwargs.get(UserModel.USERNAME_FIELD)
|
||||
try:
|
||||
case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
|
||||
user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
|
||||
except UserModel.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a non-existing user (#20760).
|
||||
UserModel().set_password(password)
|
||||
else:
|
||||
if user.check_password(password) and self.user_can_authenticate(user):
|
||||
return user
|
|
@ -31,7 +31,7 @@ class RegistrationForm(forms.Form):
|
|||
return email
|
||||
if email.lower() != email:
|
||||
raise forms.ValidationError('The supplied address contained uppercase letters. Please use a lowercase email address.')
|
||||
if User.objects.filter(username=email).exists():
|
||||
if User.objects.filter(username__iexact=email).exists():
|
||||
raise forms.ValidationError('An account with the email address you provided already exists.')
|
||||
return email
|
||||
|
||||
|
@ -199,7 +199,7 @@ class ResetPasswordForm(forms.Form):
|
|||
In addition to EmailField's checks, verifies that a User matching the username exists.
|
||||
"""
|
||||
username = self.cleaned_data["username"]
|
||||
if not User.objects.filter(username=username).exists():
|
||||
if not User.objects.filter(username__iexact=username).exists():
|
||||
raise forms.ValidationError(mark_safe(
|
||||
"Didn't find a matching account. "
|
||||
"If you don't have an account yet, you can <a href=\"{}\">create one</a>.".format(
|
||||
|
@ -266,6 +266,6 @@ class ChangeUsernameForm(forms.Form):
|
|||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data['username']
|
||||
if User.objects.filter(username=username).exists():
|
||||
if User.objects.filter(username__iexact=username).exists():
|
||||
raise ValidationError("A login with that username already exists. Please contact the secretariat to get this resolved.")
|
||||
return username
|
||||
|
|
|
@ -159,7 +159,7 @@ def confirm_account(request, auth):
|
|||
except django.core.signing.BadSignature:
|
||||
raise Http404("Invalid or expired auth")
|
||||
|
||||
if User.objects.filter(username=email).exists():
|
||||
if User.objects.filter(username__iexact=email).exists():
|
||||
return redirect(profile)
|
||||
|
||||
success = False
|
||||
|
@ -390,7 +390,7 @@ def confirm_new_email(request, auth):
|
|||
except django.core.signing.BadSignature:
|
||||
raise Http404("Invalid or expired auth")
|
||||
|
||||
person = get_object_or_404(Person, user__username=username)
|
||||
person = get_object_or_404(Person, user__username__iexact=username)
|
||||
|
||||
# do another round of validation since the situation may have
|
||||
# changed since submitting the request
|
||||
|
@ -417,7 +417,7 @@ def password_reset(request):
|
|||
# The form validation checks that a matching User exists. Add the person__isnull check
|
||||
# because the OneToOne field does not gracefully handle checks for user.person is Null.
|
||||
# If we don't get a User here, we know it's because there's no related Person.
|
||||
user = User.objects.filter(username=submitted_username, person__isnull=False).first()
|
||||
user = User.objects.filter(username__iexact=submitted_username, person__isnull=False).first()
|
||||
if not (user and user.person.email_set.filter(active=True).exists()):
|
||||
form.add_error(
|
||||
'username',
|
||||
|
@ -465,7 +465,7 @@ def confirm_password_reset(request, auth):
|
|||
except django.core.signing.BadSignature:
|
||||
raise Http404("Invalid or expired auth")
|
||||
|
||||
user = get_object_or_404(User, username=username, password__endswith=password, last_login=last_login)
|
||||
user = get_object_or_404(User, username__iexact=username, password__endswith=password, last_login=last_login)
|
||||
if request.user.is_authenticated and request.user != user:
|
||||
return HttpResponseForbidden(
|
||||
f'This password reset link is not for the signed-in user. '
|
||||
|
@ -707,7 +707,7 @@ def login(request, extra_context=None):
|
|||
if request.method == "POST":
|
||||
form = AuthenticationForm(request, data=request.POST)
|
||||
username = form.data.get('username')
|
||||
user = User.objects.filter(username=username).first()
|
||||
user = User.objects.filter(username__iexact=username).first() # Consider _never_ actually looking for the User username and only looking at Email
|
||||
if not user:
|
||||
# try to find user ID from the email address
|
||||
email = Email.objects.filter(address=username).first()
|
||||
|
|
|
@ -95,7 +95,7 @@ def get_user_email(user):
|
|||
if emails:
|
||||
user._email_cache = emails[0]
|
||||
for email in emails:
|
||||
if email.address == user.username:
|
||||
if email.address.lower() == user.username.lower():
|
||||
user._email_cache = email
|
||||
else:
|
||||
try:
|
||||
|
|
|
@ -47,7 +47,6 @@ def merge_persons(request, source, target, file=sys.stdout, verbose=False):
|
|||
|
||||
# check for any remaining relationships and exit if more found
|
||||
objs = [source]
|
||||
# request.user = User.objects.filter(is_superuser=True).first()
|
||||
deletable_objects = admin.utils.get_deleted_objects(objs, request, admin.site)
|
||||
deletable_objects_summary = deletable_objects[1]
|
||||
if len(deletable_objects_summary) > 1: # should only include one object (Person)
|
||||
|
|
|
@ -81,13 +81,13 @@ class EditPersonForm(forms.ModelForm):
|
|||
self.initial['user'] = self.instance.user.username
|
||||
|
||||
def clean_user(self):
|
||||
user = self.cleaned_data['user']
|
||||
user = self.cleaned_data['user'].lower()
|
||||
if user:
|
||||
# if Django User object exists return it, otherwise create one
|
||||
try:
|
||||
user_obj = User.objects.get(username=user)
|
||||
user_obj = User.objects.get(username__iexact=user)
|
||||
except User.DoesNotExist:
|
||||
user_obj = User.objects.create_user(user,user)
|
||||
user_obj = User.objects.create_user(username=user, email=user)
|
||||
|
||||
return user_obj
|
||||
else:
|
||||
|
@ -133,11 +133,11 @@ class NewPersonForm(forms.ModelForm):
|
|||
exclude = ('time','user')
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data['email']
|
||||
email = self.cleaned_data['email'].lower()
|
||||
|
||||
# error if there is already an account (User, Person) associated with this email
|
||||
try:
|
||||
user = User.objects.get(username=email)
|
||||
user = User.objects.get(username__iexact=email)
|
||||
person = Person.objects.get(user=user)
|
||||
if user and person:
|
||||
raise forms.ValidationError("This account already exists. [name=%s, id=%s, email=%s]" % (person.name,person.id,email))
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import IntegrityError
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.utils.http import urlencode
|
||||
|
@ -91,10 +90,10 @@ def add_proceed(request):
|
|||
)
|
||||
|
||||
# in theory a user record could exist which wasn't associated with a Person
|
||||
try:
|
||||
user = User.objects.create_user(email, email)
|
||||
except IntegrityError:
|
||||
user = User.objects.get(username=email)
|
||||
|
||||
user = User.objects.filter(username__iexact=email).first()
|
||||
if not user:
|
||||
user = User.objects.create_user(username=email, email=email)
|
||||
|
||||
person.user = user
|
||||
person.save()
|
||||
|
@ -179,7 +178,7 @@ def edit(request, id):
|
|||
|
||||
if 'user' in person_form.changed_data and person_form.initial['user']:
|
||||
try:
|
||||
source = User.objects.get(username=person_form.initial['user'])
|
||||
source = User.objects.get(username__iexact=person_form.initial['user'])
|
||||
merge_users(source, person_form.cleaned_data['user'])
|
||||
source.is_active = False
|
||||
source.save()
|
||||
|
|
|
@ -168,7 +168,7 @@ STATICFILES_FINDERS = (
|
|||
|
||||
WSGI_APPLICATION = "ietf.wsgi.application"
|
||||
|
||||
AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', )
|
||||
AUTHENTICATION_BACKENDS = ( 'ietf.ietfauth.backends.CaseInsensitiveModelBackend', )
|
||||
|
||||
FILE_UPLOAD_PERMISSIONS = 0o644
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ def api_submission(request):
|
|||
if form.is_valid():
|
||||
log('got valid submission form for %s' % form.filename)
|
||||
username = form.cleaned_data['user']
|
||||
user = User.objects.filter(username=username)
|
||||
user = User.objects.filter(username__iexact=username)
|
||||
if user.count() == 0:
|
||||
# See if a secondary login was being used
|
||||
email = Email.objects.filter(address=username, active=True)
|
||||
|
@ -232,7 +232,7 @@ def api_submit(request):
|
|||
if form.is_valid():
|
||||
log('got valid submission form for %s' % form.filename)
|
||||
username = form.cleaned_data['user']
|
||||
user = User.objects.filter(username=username)
|
||||
user = User.objects.filter(username__iexact=username)
|
||||
if user.count() == 0:
|
||||
# See if a secondary login was being used
|
||||
email = Email.objects.filter(address=username, active=True)
|
||||
|
|
|
@ -58,7 +58,7 @@ def notify(request, org, notification):
|
|||
|
||||
if not user.is_authenticated:
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
user = User.objects.get(username__iexact=username)
|
||||
except User.DoesNotExist:
|
||||
return HttpResponse("Invalid username/password")
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ def import_htpasswd_file(filename, verbosity=1, overwrite=False):
|
|||
' "%s"' % (file.name, line))
|
||||
username, password = line.strip().split(':', 1)
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
user = User.objects.get(username__iexact=username)
|
||||
if overwrite == True or not user.password:
|
||||
if password.startswith('{SHA}'):
|
||||
user.password = "sha1$$%s" % password[len('{SHA}'):]
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
# Copyright The IETF Trust 2018-2020, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.person.models import Person, PersonEvent
|
||||
from ietf.utils.mail import send_mail
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ("""
|
||||
Send GDPR consent request emails to persons who have not indicated consent
|
||||
to having their personal information stored. Each send is logged as a
|
||||
PersonEvent.
|
||||
|
||||
By default email sending happens at a rate of 1 message per second; the
|
||||
rate can be adjusted with the -r option. At the start of a run, an estimate
|
||||
is given of how many persons to send to, and how long the run will take.
|
||||
|
||||
By default, emails are not sent out if there is less than 6 days since the
|
||||
previous consent request email. The interval can be adjusted with the -m
|
||||
option. One effect of this is that it is possible to break of a run and
|
||||
re-start it with for instance a different rate, without having duplicate
|
||||
messages go out to persons that were handled in the interrupted run.
|
||||
""")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('-n', '--dry-run', action='store_true', default=False,
|
||||
help="Don't send email, just list recipients")
|
||||
parser.add_argument('-d', '--date', help="Date of deletion (mentioned in message)")
|
||||
parser.add_argument('-m', '--minimum-interval', type=int, default=6,
|
||||
help="Minimum interval between re-sending email messages, default: %(default)s days")
|
||||
parser.add_argument('-r', '--rate', type=float, default=1.0,
|
||||
help='Rate of sending mail, default: %(default)s/s')
|
||||
parser.add_argument('-R', '--reminder', action='store_true', default=False,
|
||||
help='Preface the subject with "Reminder:"')
|
||||
parser.add_argument('user', nargs='*')
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Don't send copies of the whole bulk mailing to the debug mailbox
|
||||
if settings.SERVER_MODE == 'production':
|
||||
settings.EMAIL_COPY_TO = "Email Debug Copy <outbound@ietf.org>"
|
||||
#
|
||||
event_type = 'gdpr_notice_email'
|
||||
# Arguments
|
||||
# --date
|
||||
if 'date' in options and options['date'] != None:
|
||||
try:
|
||||
date = datetime.datetime.strptime(options['date'], "%Y-%m-%d").date()
|
||||
except ValueError as e:
|
||||
raise CommandError('%s' % e)
|
||||
else:
|
||||
date = datetime.date.today() + datetime.timedelta(days=30)
|
||||
days = (date - datetime.date.today()).days
|
||||
if days <= 1:
|
||||
raise CommandError('date must be more than 1 day in the future')
|
||||
# --rate
|
||||
delay = 1.0/options['rate']
|
||||
# --minimum_interval
|
||||
minimum_interval = options['minimum_interval']
|
||||
latest_previous = timezone.now() - datetime.timedelta(days=minimum_interval)
|
||||
# user
|
||||
self.stdout.write('Querying the database for matching person records ...')
|
||||
if 'user' in options and options['user']:
|
||||
persons = Person.objects.filter(user__username__in=options['user'])
|
||||
else:
|
||||
exclude = PersonEvent.objects.filter(time__gt=latest_previous, type=event_type)
|
||||
persons = Person.objects.exclude(consent=True).exclude(personevent__in=exclude)
|
||||
# Report the size of the run
|
||||
runtime = persons.count() * delay
|
||||
self.stdout.write('Sending to %d users; estimated time a bit more than %d:%02d hours' % (persons.count(), runtime//3600, runtime%3600//60))
|
||||
subject='Personal Information in the IETF Datatracker'
|
||||
if options['reminder']:
|
||||
subject = "Reminder: " + subject
|
||||
for person in persons:
|
||||
fields = ', '.join(person.needs_consent())
|
||||
if fields and person.email_set.exists():
|
||||
if options['dry_run']:
|
||||
print(("%-32s %-32s %-32s %-32s %s" % (person.email(), person.name_from_draft or '', person.name, person.ascii, fields)).encode('utf8'))
|
||||
else:
|
||||
to = [ e.address for e in person.email_set.filter(active=True) ] # pyflakes:ignore
|
||||
if not to:
|
||||
to = [ e.address for e in person.email_set.all() ] # pyflakes:ignore
|
||||
self.stdout.write("Sendimg email to %s" % to)
|
||||
send_mail(None, to, "<gdprnoreply@ietf.org>",
|
||||
subject=subject,
|
||||
template='utils/personal_information_notice.txt',
|
||||
context={
|
||||
'date': date, 'days': days, 'fields': fields,
|
||||
'person': person, 'settings': settings,
|
||||
},
|
||||
)
|
||||
PersonEvent.objects.create(person=person, type='gdpr_notice_email',
|
||||
desc="Sent GDPR notice email to %s with confirmation deadline %s" % (to, date))
|
||||
time.sleep(delay)
|
||||
|
Loading…
Reference in a new issue