Skip to content

Commit e110419

Browse files
committed
Merged in ^/personal/henrik/6.22.1-acctdeps which provides import of addresses subscribed to IETF mailing lists, and additional datatracker account creation requirements. Also a table and form for manual whitelisting of account logins, in order to handle cases which fall outside the default requirements. Fixed some tests.
- Legacy-Id: 11389
2 parents 7c033d3 + c1a8b3d commit e110419

19 files changed

Lines changed: 471 additions & 18 deletions

File tree

ietf/group/tests_info.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ def test_create(self):
396396

397397
bof_state = GroupStateName.objects.get(slug="bof")
398398

399+
area = Group.objects.filter(type="area").first()
400+
399401
# normal get
400402
r = self.client.get(url)
401403
self.assertEqual(r.status_code, 200)
@@ -412,21 +414,30 @@ def test_create(self):
412414
# acronym contains non-alphanumeric
413415
r = self.client.post(url, dict(acronym="test...", name="Testing WG", state=bof_state.pk))
414416
self.assertEqual(r.status_code, 200)
417+
self.assertTrue(len(q('form .has-error')) > 0)
415418

416419
# acronym contains hyphen
417420
r = self.client.post(url, dict(acronym="test-wg", name="Testing WG", state=bof_state.pk))
418421
self.assertEqual(r.status_code, 200)
422+
self.assertTrue(len(q('form .has-error')) > 0)
419423

420424
# acronym too short
421425
r = self.client.post(url, dict(acronym="t", name="Testing WG", state=bof_state.pk))
422426
self.assertEqual(r.status_code, 200)
427+
self.assertTrue(len(q('form .has-error')) > 0)
423428

424429
# acronym doesn't start with an alpha character
425430
r = self.client.post(url, dict(acronym="1startwithalpha", name="Testing WG", state=bof_state.pk))
426431
self.assertEqual(r.status_code, 200)
432+
self.assertTrue(len(q('form .has-error')) > 0)
427433

428-
# creation
434+
# no parent group given
429435
r = self.client.post(url, dict(acronym="testwg", name="Testing WG", state=bof_state.pk))
436+
self.assertEqual(r.status_code, 200)
437+
self.assertTrue(len(q('form .has-error')) > 0)
438+
439+
# Ok creation
440+
r = self.client.post(url, dict(acronym="testwg", name="Testing WG", state=bof_state.pk, parent=area.pk))
430441
self.assertEqual(r.status_code, 302)
431442
self.assertEqual(len(Group.objects.filter(type="wg")), num_wgs + 1)
432443
group = Group.objects.get(acronym="testwg")

ietf/ietfauth/forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import debug # pyflakes:ignore
1313

1414
from ietf.person.models import Person, Email
15+
from ietf.mailinglists.models import Whitelisted
1516

1617

1718
class RegistrationForm(forms.Form):
@@ -118,3 +119,9 @@ def clean_username(self):
118119
class TestEmailForm(forms.Form):
119120
email = forms.EmailField(required=False)
120121

122+
class WhitelistForm(ModelForm):
123+
class Meta:
124+
model = Whitelisted
125+
exclude = ['by', 'time' ]
126+
127+

ietf/ietfauth/tests.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- coding: utf-8 -*-
2-
import os, shutil
2+
from __future__ import unicode_literals
3+
4+
import os, shutil, time
35
from urlparse import urlsplit
46
from pyquery import PyQuery
57
from unittest import skipIf
@@ -15,6 +17,8 @@
1517
from ietf.person.models import Person, Email
1618
from ietf.group.models import Group, Role, RoleName
1719
from ietf.ietfauth.htpasswd import update_htpasswd_file
20+
from ietf.mailinglists.models import Subscribed
21+
1822
import ietf.ietfauth.views
1923

2024
if os.path.exists(settings.HTPASSWD_COMMAND):
@@ -94,7 +98,7 @@ def username_in_htpasswd_file(self, username):
9498

9599
return False
96100

97-
def test_create_account(self):
101+
def test_create_account_failure(self):
98102
make_test_data()
99103

100104
url = urlreverse(ietf.ietfauth.views.create_account)
@@ -103,12 +107,21 @@ def test_create_account(self):
103107
r = self.client.get(url)
104108
self.assertEqual(r.status_code, 200)
105109

106-
# register email
110+
# register email and verify failure
107111
email = 'new-account@example.com'
108112
empty_outbox()
109113
r = self.client.post(url, { 'email': email })
110114
self.assertEqual(r.status_code, 200)
111-
self.assertTrue("Account created" in unicontent(r))
115+
self.assertIn("Account creation failed", unicontent(r))
116+
117+
def register_and_verify(self, email):
118+
url = urlreverse(ietf.ietfauth.views.create_account)
119+
120+
# register email
121+
empty_outbox()
122+
r = self.client.post(url, { 'email': email })
123+
self.assertEqual(r.status_code, 200)
124+
self.assertIn("Account created", unicontent(r))
112125
self.assertEqual(len(outbox), 1)
113126

114127
# go to confirm page
@@ -130,6 +143,41 @@ def test_create_account(self):
130143

131144
self.assertTrue(self.username_in_htpasswd_file(email))
132145

146+
def test_create_whitelisted_account(self):
147+
email = "new-account@example.com"
148+
149+
# add whitelist entry
150+
r = self.client.post(urlreverse(django.contrib.auth.views.login), {"username":"secretary", "password":"secretary+password"})
151+
self.assertEqual(r.status_code, 302)
152+
self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile))
153+
154+
r = self.client.get(urlreverse(ietf.ietfauth.views.add_account_whitelist))
155+
self.assertEqual(r.status_code, 200)
156+
self.assertIn("Add a whitelist entry", unicontent(r))
157+
158+
r = self.client.post(urlreverse(ietf.ietfauth.views.add_account_whitelist), {"email": email})
159+
self.assertEqual(r.status_code, 200)
160+
self.assertIn("Whitelist entry creation successful", unicontent(r))
161+
162+
# log out
163+
r = self.client.get(urlreverse(django.contrib.auth.views.logout))
164+
self.assertEqual(r.status_code, 200)
165+
166+
# register and verify whitelisted email
167+
self.register_and_verify(email)
168+
169+
170+
def test_create_subscribed_account(self):
171+
# verify creation with email in subscribed list
172+
saved_delay = settings.LIST_ACCOUNT_DELAY
173+
settings.LIST_ACCOUNT_DELAY = 1
174+
email = "subscribed@example.com"
175+
s = Subscribed(email=email)
176+
s.save()
177+
time.sleep(1.1)
178+
self.register_and_verify(email)
179+
settings.LIST_ACCOUNT_DELAY = saved_delay
180+
133181
def test_profile(self):
134182
make_test_data()
135183

ietf/ietfauth/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.conf.urls import patterns, url
44
from django.contrib.auth.views import login, logout
55

6+
from ietf.ietfauth.views import add_account_whitelist
7+
68
urlpatterns = patterns('ietf.ietfauth.views',
79
url(r'^$', 'index'),
810
# url(r'^login/$', 'ietf_login'),
@@ -18,4 +20,5 @@
1820
url(r'^reset/$', 'password_reset'),
1921
url(r'^reset/confirm/(?P<auth>[^/]+)/$', 'confirm_password_reset'),
2022
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', 'confirm_new_email'),
23+
(r'whitelist/add/?$', add_account_whitelist),
2124
)

ietf/ietfauth/views.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
# Copyright The IETF Trust 2007, All Rights Reserved
3434

35+
from datetime import datetime as DateTime, timedelta as TimeDelta
36+
3537
from django.conf import settings
3638
from django.http import Http404 #, HttpResponse, HttpResponseRedirect
3739
from django.shortcuts import render, redirect, get_object_or_404
@@ -42,10 +44,14 @@
4244
from django.contrib.sites.models import Site
4345
from django.contrib.auth.models import User
4446

47+
import debug # pyflakes:ignore
48+
4549
from ietf.group.models import Role
46-
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm
50+
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm
4751
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
4852
from ietf.ietfauth.htpasswd import update_htpasswd_file
53+
from ietf.ietfauth.utils import role_required
54+
from ietf.mailinglists.models import Subscribed, Whitelisted
4955
from ietf.person.models import Person, Email, Alias
5056
from ietf.utils.mail import send_mail
5157

@@ -85,20 +91,25 @@ def create_account(request):
8591
if request.method == 'POST':
8692
form = RegistrationForm(request.POST)
8793
if form.is_valid():
88-
to_email = form.cleaned_data['email']
94+
to_email = form.cleaned_data['email'] # This will be lowercase if form.is_valid()
95+
existing = Subscribed.objects.filter(email=to_email).first()
96+
ok_to_create = ( Whitelisted.objects.filter(email=to_email).exists()
97+
or existing and (existing.time + TimeDelta(seconds=settings.LIST_ACCOUNT_DELAY)) < DateTime.now() )
98+
if ok_to_create:
99+
auth = django.core.signing.dumps(to_email, salt="create_account")
89100

90-
auth = django.core.signing.dumps(to_email, salt="create_account")
91-
92-
domain = Site.objects.get_current().domain
93-
subject = 'Confirm registration at %s' % domain
94-
from_email = settings.DEFAULT_FROM_EMAIL
101+
domain = Site.objects.get_current().domain
102+
subject = 'Confirm registration at %s' % domain
103+
from_email = settings.DEFAULT_FROM_EMAIL
95104

96-
send_mail(request, to_email, from_email, subject, 'registration/creation_email.txt', {
97-
'domain': domain,
98-
'auth': auth,
99-
'username': to_email,
100-
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
101-
})
105+
send_mail(request, to_email, from_email, subject, 'registration/creation_email.txt', {
106+
'domain': domain,
107+
'auth': auth,
108+
'username': to_email,
109+
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
110+
})
111+
else:
112+
return render(request, 'registration/manual.html', { 'account_request_email': settings.ACCOUNT_REQUEST_EMAIL })
102113
else:
103114
form = RegistrationForm()
104115

@@ -359,3 +370,22 @@ def test_email(request):
359370
r.set_cookie("testmailcc", cookie)
360371

361372
return r
373+
374+
@role_required('Secretariat')
375+
def add_account_whitelist(request):
376+
success = False
377+
if request.method == 'POST':
378+
form = WhitelistForm(request.POST)
379+
if form.is_valid():
380+
email = form.cleaned_data['email']
381+
entry = Whitelisted(email=email, by=request.user.person)
382+
entry.save()
383+
success = True
384+
else:
385+
form = WhitelistForm()
386+
387+
return render(request, 'ietfauth/whitelist_form.html', {
388+
'form': form,
389+
'success': success,
390+
})
391+

ietf/mailinglists/admin.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright The IETF Trust 2016, All Rights Reserved
2+
3+
from django.contrib import admin
4+
5+
from ietf.mailinglists.models import List, Subscribed, Whitelisted
6+
7+
8+
class ListAdmin(admin.ModelAdmin):
9+
list_display = ('id', 'name', 'description', 'advertised')
10+
search_fields = ('name',)
11+
admin.site.register(List, ListAdmin)
12+
13+
14+
class SubscribedAdmin(admin.ModelAdmin):
15+
list_display = ('id', 'time', 'email')
16+
raw_id_fields = ('lists',)
17+
search_fields = ('email',)
18+
admin.site.register(Subscribed, SubscribedAdmin)
19+
20+
21+
class WhitelistedAdmin(admin.ModelAdmin):
22+
list_display = ('id', 'time', 'email', 'by')
23+
admin.site.register(Whitelisted, WhitelistedAdmin)

ietf/mailinglists/management/__init__.py

Whitespace-only changes.

ietf/mailinglists/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright The IETF Trust 2016, All Rights Reserved
2+
3+
import sys
4+
from textwrap import dedent
5+
6+
import debug # pyflakes:ignore
7+
8+
from django.conf import settings
9+
from django.core.management.base import BaseCommand
10+
11+
from ietf.mailinglists.models import List, Subscribed
12+
13+
class Command(BaseCommand):
14+
"""
15+
Import list information from Mailman.
16+
17+
Import announced list names, descriptions, and subscribers, by calling the
18+
appropriate Mailman functions and adding entries to the database.
19+
20+
Run this from cron regularly, with sufficient permissions to access the
21+
mailman database files.
22+
23+
"""
24+
25+
help = dedent(__doc__).strip()
26+
27+
#option_list = BaseCommand.option_list + ( )
28+
29+
def note(self, msg):
30+
if self.verbosity > 1:
31+
self.stdout.write(msg)
32+
33+
def handle(self, *filenames, **options):
34+
"""
35+
36+
* Import announced lists, with appropriate meta-information.
37+
38+
* For each list, import the members.
39+
40+
"""
41+
42+
self.verbosity = int(options.get('verbosity'))
43+
44+
sys.path.append(settings.MAILMAN_LIB_DIR)
45+
46+
from Mailman import Utils
47+
from Mailman import MailList
48+
49+
for name in Utils.list_names():
50+
mlist = MailList.MailList(name, lock=False)
51+
self.note("List: %s" % mlist.internal_name())
52+
if mlist.advertised:
53+
list, created = List.objects.get_or_create(name=mlist.real_name, description=mlist.description, advertised=mlist.advertised)
54+
# The following calls return lowercased addresses
55+
members = mlist.getRegularMemberKeys() + mlist.getDigestMemberKeys()
56+
known = [ s.email for s in Subscribed.objects.filter(lists__name=name) ]
57+
for addr in members:
58+
if not addr in known:
59+
self.note(" Adding subscribed: %s" % (addr))
60+
new, created = Subscribed.objects.get_or_create(email=addr)
61+
new.lists.add(list)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from django.db import migrations
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
]
9+
10+
operations = [
11+
]

0 commit comments

Comments
 (0)