Skip to content

Commit a99aa32

Browse files
committed
Refactor account code to use the built-in Django signing framework
(reusing code previously written for the community lists) instead of the a custom MD5 scheme, add tests of all views, rewrite custom form handling code to use plain forms and ensure that the data is properly validated and errors visible in the UI. Move help texts on the Person form up to the model. - Legacy-Id: 11136
1 parent 210188b commit a99aa32

16 files changed

Lines changed: 639 additions & 570 deletions

ietf/ietfauth/forms.py

Lines changed: 69 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,235 +1,96 @@
1-
import datetime
2-
import hashlib
3-
import subprocess
1+
import re
42

53
from django import forms
64
from django.forms import ModelForm
7-
from django.conf import settings
5+
from django.db import models
86
from django.contrib.auth.models import User
9-
from django.contrib.sites.models import Site
10-
from django.utils.translation import ugettext_lazy as _
7+
from django.utils.html import mark_safe
8+
from django.core.urlresolvers import reverse as urlreverse
119

12-
from ietf.utils.mail import send_mail
13-
from ietf.person.models import Person, Email, Alias
14-
from ietf.group.models import Role
10+
from ietf.person.models import Person, Email
1511

1612

1713
class RegistrationForm(forms.Form):
18-
1914
email = forms.EmailField(label="Your email (lowercase)")
20-
realm = 'IETF'
21-
expire = 3
22-
23-
def save(self, *args, **kwargs):
24-
# why is there a save when it doesn't save?
25-
self.send_email()
26-
return True
27-
28-
def send_email(self):
29-
domain = Site.objects.get_current().domain
30-
subject = 'Confirm registration at %s' % domain
31-
from_email = settings.DEFAULT_FROM_EMAIL
32-
to_email = self.cleaned_data['email']
33-
today = datetime.date.today().strftime('%Y%m%d')
34-
auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, self.realm)).hexdigest()
35-
context = {
36-
'domain': domain,
37-
'today': today,
38-
'realm': self.realm,
39-
'auth': auth,
40-
'username': to_email,
41-
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
42-
}
43-
send_mail(self.request, to_email, from_email, subject, 'registration/creation_email.txt', context)
4415

4516
def clean_email(self):
4617
email = self.cleaned_data.get('email', '')
4718
if not email:
4819
return email
4920
if email.lower() != email:
50-
raise forms.ValidationError(_('The supplied address contained uppercase letters. Please use a lowercase email address.'))
51-
if User.objects.filter(username=email).count():
52-
raise forms.ValidationError(_('An account with the email address you provided already exists.'))
21+
raise forms.ValidationError('The supplied address contained uppercase letters. Please use a lowercase email address.')
22+
if User.objects.filter(username=email).exists():
23+
raise forms.ValidationError('An account with the email address you provided already exists.')
5324
return email
5425

5526

56-
class RecoverPasswordForm(RegistrationForm):
57-
58-
realm = 'IETF'
59-
60-
def send_email(self):
61-
domain = Site.objects.get_current().domain
62-
subject = 'Password reset at %s' % domain
63-
from_email = settings.DEFAULT_FROM_EMAIL
64-
today = datetime.date.today().strftime('%Y%m%d')
65-
to_email = self.cleaned_data['email']
66-
today = datetime.date.today().strftime('%Y%m%d')
67-
auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, self.realm)).hexdigest()
68-
context = {
69-
'domain': domain,
70-
'today': today,
71-
'realm': self.realm,
72-
'auth': auth,
73-
'username': to_email,
74-
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
75-
}
76-
send_mail(self.request, to_email, from_email, subject, 'registration/password_reset_email.txt', context)
27+
class PasswordForm(forms.Form):
28+
password = forms.CharField(widget=forms.PasswordInput)
29+
password_confirmation = forms.CharField(widget=forms.PasswordInput,
30+
help_text="Enter the same password as above, for verification.")
7731

78-
def clean_email(self):
79-
email = self.cleaned_data.get('email', '')
32+
def clean_password_confirmation(self):
33+
password = self.cleaned_data.get("password", "")
34+
password_confirmation = self.cleaned_data["password_confirmation"]
35+
if password != password_confirmation:
36+
raise forms.ValidationError("The two password fields didn't match.")
37+
return password_confirmation
38+
39+
40+
def ascii_cleaner(supposedly_ascii):
41+
outside_printable_ascii_pattern = r'[^\x20-\x7F]'
42+
if re.search(outside_printable_ascii_pattern, supposedly_ascii):
43+
raise forms.ValidationError("Please only enter ASCII characters.")
44+
return supposedly_ascii
45+
46+
class PersonForm(ModelForm):
47+
class Meta:
48+
model = Person
49+
exclude = ('time', 'user')
50+
51+
def clean_ascii(self):
52+
return ascii_cleaner(self.cleaned_data.get("ascii") or u"")
53+
54+
def clean_ascii_short(self):
55+
return ascii_cleaner(self.cleaned_data.get("ascii_short") or u"")
56+
57+
58+
class NewEmailForm(forms.Form):
59+
new_email = forms.EmailField(label="New email address", required=False)
60+
61+
def clean_new_email(self):
62+
email = self.cleaned_data.get("new_email", "")
63+
if email:
64+
existing = Email.objects.filter(address=email).first()
65+
if existing:
66+
raise forms.ValidationError("Email address '%s' is already assigned to account '%s' (%s)" % (existing, existing.person and existing.person.user, existing.person))
8067
return email
8168

8269

83-
class PasswordForm(forms.Form):
70+
class RoleEmailForm(forms.Form):
71+
email = forms.ModelChoiceField(label="Role email", queryset=Email.objects.all())
72+
73+
def __init__(self, role, *args, **kwargs):
74+
super(RoleEmailForm, self).__init__(*args, **kwargs)
75+
76+
f = self.fields["email"]
77+
f.label = u"%s in %s" % (role.name, role.group.acronym.upper())
78+
f.help_text = u"Email to use for <i>%s</i> role in %s" % (role.name, role.group.name)
79+
f.queryset = f.queryset.filter(models.Q(person=role.person_id) | models.Q(role=role))
80+
f.initial = role.email_id
81+
f.choices = [(e.pk, e.address if e.active else u"({})".format(e.address)) for e in f.queryset]
82+
83+
84+
class ResetPasswordForm(forms.Form):
85+
email = forms.EmailField(label="Your email (lowercase)")
86+
87+
def clean_email(self):
88+
email = self.cleaned_data["email"]
89+
if not User.objects.filter(username=email).exists():
90+
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(urlreverse("create_account"))))
91+
return email
8492

85-
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
86-
password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput,
87-
help_text=_("Enter the same password as above, for verification."))
88-
89-
def __init__(self, *args, **kwargs):
90-
self.username = kwargs.pop('username')
91-
self.update_user = User.objects.filter(username=self.username).count() > 0
92-
super(PasswordForm, self).__init__(*args, **kwargs)
93-
94-
def clean_password2(self):
95-
password1 = self.cleaned_data.get("password1", "")
96-
password2 = self.cleaned_data["password2"]
97-
if password1 != password2:
98-
raise forms.ValidationError(_("The two password fields didn't match."))
99-
return password2
100-
101-
def get_password(self):
102-
return self.cleaned_data.get('password1')
103-
104-
def create_user(self):
105-
user = User.objects.create(username=self.username,
106-
email=self.username)
107-
email = Email.objects.filter(address=self.username)
108-
person = None
109-
if email.count():
110-
email = email[0]
111-
if email.person:
112-
person = email.person
113-
else:
114-
email = None
115-
if not person:
116-
person = Person.objects.create(user=user,
117-
name=self.username,
118-
ascii=self.username)
119-
if not email:
120-
email = Email.objects.create(address=self.username,
121-
person=person)
122-
email.person = person
123-
email.save()
124-
person.user = user
125-
person.save()
126-
return user
127-
128-
def get_user(self):
129-
return User.objects.get(username=self.username)
130-
131-
def save_password_file(self):
132-
if getattr(settings, 'USE_PYTHON_HTDIGEST', None):
133-
pass_file = settings.HTPASSWD_FILE
134-
realm = settings.HTDIGEST_REALM
135-
password = self.get_password()
136-
username = self.username
137-
prefix = '%s:%s:' % (username, realm)
138-
key = hashlib.md5(prefix + password).hexdigest()
139-
f = open(pass_file, 'r+')
140-
pos = f.tell()
141-
line = f.readline()
142-
while line:
143-
if line.startswith(prefix):
144-
break
145-
pos=f.tell()
146-
line = f.readline()
147-
f.seek(pos)
148-
f.write('%s%s\n' % (prefix, key))
149-
f.close()
150-
else:
151-
p = subprocess.Popen([settings.HTPASSWD_COMMAND, "-b", settings.HTPASSWD_FILE, self.username, self.get_password()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
152-
stdout, stderr = p.communicate()
153-
154-
def save(self):
155-
if self.update_user:
156-
user = self.get_user()
157-
else:
158-
user = self.create_user()
159-
user.set_password(self.get_password())
160-
user.save()
161-
self.save_password_file()
162-
return user
16393

16494
class TestEmailForm(forms.Form):
16595
email = forms.EmailField(required=False)
16696

167-
class PersonForm(ModelForm):
168-
request = None
169-
new_emails = []
170-
class Meta:
171-
model = Person
172-
exclude = ('time','user')
173-
174-
def confirm_address(self,email):
175-
person = self.instance
176-
domain = Site.objects.get_current().domain
177-
user = person.user
178-
if len(email) == 0:
179-
return
180-
subject = 'Confirm email address for %s' % person.name
181-
from_email = settings.DEFAULT_FROM_EMAIL
182-
to_email = email
183-
today = datetime.date.today().strftime('%Y%m%d')
184-
auth = hashlib.md5('%s%s%s%s' % (settings.SECRET_KEY, today, to_email, user)).hexdigest()
185-
context = {
186-
'today': today,
187-
'domain': domain,
188-
'user': user,
189-
'email': email,
190-
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
191-
'auth': auth,
192-
}
193-
send_mail(self.request, to_email, from_email, subject, 'registration/add_email_email.txt', context)
194-
195-
def save(self, force_insert=False, force_update=False, commit=True):
196-
m = super(PersonForm, self).save(commit=False)
197-
self.new_emails = [v for k,v in self.data.items() if k[:10] == u'new_email_' and u'@' in v]
198-
199-
for email in self.new_emails:
200-
self.confirm_address(email)
201-
202-
# Process email active flags
203-
emails = Email.objects.filter(person=self.instance)
204-
for email in emails:
205-
email.active = self.data.__contains__(email.address)
206-
if commit:
207-
email.save()
208-
209-
# Process email for roles
210-
for k,v in self.data.items():
211-
if k[:11] == u'role_email_':
212-
role = Role.objects.get(id=k[11:])
213-
email = Email.objects.get(address = v)
214-
role.email = email
215-
if commit:
216-
role.save()
217-
218-
# Make sure the alias table contains any new and/or old names.
219-
old_names = set([x.name for x in Alias.objects.filter(person=self.instance)])
220-
curr_names = set([x for x in [self.instance.name,
221-
self.instance.ascii,
222-
self.instance.ascii_short,
223-
self.data['name'],
224-
self.data['ascii'],
225-
self.data['ascii_short']] if len(x)])
226-
new_names = curr_names - old_names
227-
for name in new_names:
228-
alias = Alias(person=self.instance,name=name)
229-
if commit:
230-
alias.save()
231-
232-
if commit:
233-
m.save()
234-
return m
235-

ietf/ietfauth/htpasswd.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import subprocess, hashlib
2+
3+
from django.conf import settings
4+
5+
def save_htpasswd_file(username, password):
6+
if getattr(settings, 'USE_PYTHON_HTDIGEST', None):
7+
pass_file = settings.HTPASSWD_FILE
8+
realm = settings.HTDIGEST_REALM
9+
prefix = '%s:%s:' % (username, realm)
10+
key = hashlib.md5(prefix + password).hexdigest()
11+
f = open(pass_file, 'r+')
12+
pos = f.tell()
13+
line = f.readline()
14+
while line:
15+
if line.startswith(prefix):
16+
break
17+
pos=f.tell()
18+
line = f.readline()
19+
f.seek(pos)
20+
f.write('%s%s\n' % (prefix, key))
21+
f.close()
22+
else:
23+
p = subprocess.Popen([settings.HTPASSWD_COMMAND, "-b", settings.HTPASSWD_FILE, username, password], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
24+
stdout, stderr = p.communicate()

0 commit comments

Comments
 (0)