Merge lp:~canonical-isd-hackers/canonical-identity-provider/sreg_121533 into lp:canonical-identity-provider/release

Proposed by Danny Tamez
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: no longer in the source branch.
Merged at revision: 169
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/sreg_121533
Merge into: lp:canonical-identity-provider/release
Diff against target: 2117 lines (+1188/-231)
21 files modified
.bzrignore (+2/-0)
django_project/config_dev/urls.py (+1/-4)
identityprovider/api10/handlers.py (+11/-6)
identityprovider/fixtures/test.json (+128/-1)
identityprovider/forms.py (+178/-6)
identityprovider/media/ubuntu/narrow.css (+6/-1)
identityprovider/media/ubuntu/styles.css (+14/-2)
identityprovider/models/account.py (+1/-2)
identityprovider/models/openidmodels.py (+19/-2)
identityprovider/models/team.py (+19/-0)
identityprovider/teams.py (+4/-2)
identityprovider/templates/decide.html (+99/-12)
identityprovider/tests/test_admin.py (+5/-2)
identityprovider/tests/test_forms.py (+260/-4)
identityprovider/tests/test_models_openidmodels.py (+18/-4)
identityprovider/tests/test_models_team.py (+57/-3)
identityprovider/tests/test_views_server.py (+171/-70)
identityprovider/tests/utils.py (+51/-0)
identityprovider/views/server.py (+133/-108)
identityprovider/views/ui.py (+7/-1)
payload/__init__.py (+4/-1)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/sreg_121533
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Needs Fixing
Review via email: mp+66393@code.launchpad.net

Commit message

Enhancements to the decide page as per bug 121533

Description of the change

This branch adds enhancements to the decide page. It provides checkboxes that let the user decide whether or not to send individual items requested by the consumer on to the consumer. It remembers the state of those choices for the next time the user is presented with those options. If also employs javascript to condense multiple teams that are requested with a single line and one checkbox for convenience. The user can choose to expand the list and deselect or select individual teams as desired. One part of the bug is not implemented because of another SSO bug and that is the section dealing with additional info requested from the consumer changing the behavior of auto login.

To test:
fab bootstrap
fab setup_postgresql_server
. .env/bin/activate
./django_project/manage.py loaddata tests.json
fab run

Go to http://localhost:8000/consumer
select whatever sreg data you wish to request.
for the teams you can enter the following: ubuntu-team,kubuntu-team,isd-team

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

A few changes:

1. No need to add those extra files to the global .bzrignore file as they are not relevant to the project.
2. l. 363: don't like embedding html just for style issues: we should mark the field as required or some other class using css. you can do this by specific the css class in the widget in l. 367
3. l. 376 you're hiding an attribute: instead of changing the meaning of the 'value' attribute, use a different name. a way to do this would be

for key, v in self.data.items():
    if value == v:
        break
else:
    key = None

4. l. 578: why using JSONDecoder, JSONEncoder, instead of just simplejson.loads, simplejson.dumps ? That way you can be sure the returned data is a dictionary, for example.
5. l. 600: this breaks installing the function during the normal server setup
6. l. 680: since you're testing for number of items, if it's one, why do you loop over them? you could just use team_form.fields.0
7. l. 681,684: punctuation should be included in the string to be translated
8. l. 885: not sure that's a valid request. If the method is GET, shouldn't the args be somewhere else? The test is testing a GET request, not an invalid request.
9. l. 1836: why not do 'approved_data = {}'?
10. l. 1837: this can be replaced by "if 'teams' in approved_data" if the previous change was applied
11: l. 1914,1918: use virtualenv('python...') instead of local with the explicit path
12: l. 1917: maybe you can integrate this into the 'test' target directly, instead of having an extra target?

Small issues:

l. 90: fix proper indentation
l. 102: I'd rather have all arguments go into the next line than having a dangling one
l. 277,283,284: break multiple imports into separate lines
l. 359: can just be 'for key in data', or since you're using the value later 'for key, value in data.items()'
l. 374: since you're getting the value anyway, you could just do 'for k, v in self.data.items()' and use v
l. 381,463,1607: use parenthesis for line continuations instead of \
l. 632: \ not needed as parenthesis are already used
l. 1191,1199,1591,1595: use multiple lines for different imports
l. 1477: this can be replaced by a much nicer idiom. something like:

self.patch = patch('settings', 'SSO_RESTRICT_RP', False)
self.patch.start()

and in tearDown

self.patch.stop()

l. 1764: I think you can just use 'if form_data.approved_by_user'

Questions:

l. 318: is this condition intended only for the default case, or should it be reevaluated everytime it's False?
l. 447: does this *have* to be a dict? as you're returning the same value as for the keys.
l. 651: are we using yui somewhere else? if so we may want to just import yui from a template we then include where we need to, so as to make sure we always require the same version of it, and not try to load different versions on separate pages.

Comments:
l. 877: the tearDown for this should not be needed, but I'm fine with it

review: Needs Fixing
Revision history for this message
Danny Tamez (zematynnad) wrote :

Made all but a few changes::
#6 below. Fields is a dictionary not a list. Iterating over it returns each key. Trying to do something like teams_form.fields.keys.0 didn't produce the same result - perhaps because of the order in which django applies the . operator.
#8 The point of the test was to show that if the method was GET (even if all the other conditions were the same) the approved fields wouldn't be shown.
#12 This is not using nose so it didn't make sense to merge it with the test() target.

question re: l. 318: I didn't write that portion but as best as I can tell it was only meant to be evaluated with the form's creation so I moved the logic to __init__(). (here and for teams as well)

question re: l. 651, We are using yui in other places and the page does "work" when using <script type="text/javascript" src="/assets/identityprovider/lazr-js/yui/yui-min.js"></script> but the page always first shows the teams expanded and then collapses them. It doesn't do that with the yui-min from the cdn.

Revision history for this message
ISD Branch Mangler (isd-branches-mangler) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-06-17 15:34:24 +0000
3+++ .bzrignore 2011-07-05 15:46:33 +0000
4@@ -11,3 +11,5 @@
5 .coverage*
6 coverage
7 *.xml
8+tags
9+.backup
10
11=== modified file 'django_project/config_dev/urls.py'
12--- django_project/config_dev/urls.py 2011-06-17 12:59:08 +0000
13+++ django_project/config_dev/urls.py 2011-07-05 15:46:33 +0000
14@@ -5,8 +5,6 @@
15 import preflight
16 import preflight.urls
17
18-from identityprovider.decorators import check_readonly
19-
20 admin.autodiscover()
21 preflight.autodiscover()
22
23@@ -23,11 +21,10 @@
24 (r'^preflight/', include(preflight.urls)),
25 (r'^', include('identityprovider.urls')),
26 )
27-
28 if settings.SERVE_STATIC_MEDIA:
29 urlpatterns += patterns('',
30 (r'assets/identityprovider/(.*)', 'django.views.static.serve',
31 {'document_root': settings.SSO_MEDIA_ROOT}),
32 (r'media/(.*)', 'django.views.static.serve',
33 {'document_root': settings.MEDIA_ROOT}),
34- )
35+)
36
37=== modified file 'identityprovider/api10/handlers.py'
38--- identityprovider/api10/handlers.py 2011-06-15 22:24:43 +0000
39+++ identityprovider/api10/handlers.py 2011-07-05 15:46:33 +0000
40@@ -37,7 +37,7 @@
41 NewCaptchaError,
42 VerifyCaptchaError,
43 )
44-from identityprovider.views.server import get_team_memberships
45+from identityprovider.models.team import TeamMembership
46 from identityprovider.signals import (
47 account_created,
48 account_email_validated,
49@@ -78,6 +78,7 @@
50 return seria
51 Emitter.register('lazr.restful', LazrRestfulEmitter, 'application/json')
52
53+
54 class LazrRestfulHandler(BaseHandler):
55 allowed_methods = ('GET', 'POST')
56
57@@ -140,6 +141,7 @@
58 else:
59 return self.response
60
61+
62 class CaptchaHandler(LazrRestfulHandler):
63 response = {
64 "total_size": 0,
65@@ -280,7 +282,7 @@
66 'status': 'ok',
67 'message': "Password changed"
68 }
69-
70+
71
72 def _serialize_account(user):
73 emails = EmailAddress.objects.filter(account=user,
74@@ -354,7 +356,6 @@
75 application_token_invalidated.send(
76 sender=self, openid_identifier=data['consumer_key'])
77
78-
79 @api_user_required
80 @named_operation
81 def team_memberships(self, request):
82@@ -365,8 +366,9 @@
83
84 if len(accounts) == 1:
85 account = accounts[0]
86- memberships = get_team_memberships(data['team_names'],
87- account, False)
88+ memberships = (
89+ TeamMembership.get_team_memberships_for_user(
90+ data['team_names'], account, False))
91 return memberships
92 else:
93 return []
94@@ -402,7 +404,10 @@
95 @named_operation
96 def team_memberships(self, request):
97 team_names = request.data['team_names']
98- memberships = get_team_memberships(team_names, request.user, True)
99+
100+ memberships = (
101+ TeamMembership.get_team_memberships(
102+ team_names, request.user, True))
103 return memberships
104
105 @named_operation
106
107=== modified file 'identityprovider/fixtures/test.json'
108--- identityprovider/fixtures/test.json 2011-03-08 14:31:20 +0000
109+++ identityprovider/fixtures/test.json 2011-07-05 15:46:33 +0000
110@@ -173,6 +173,28 @@
111 }
112 },
113 {
114+ "pk": 46,
115+ "model": "identityprovider.emailaddress",
116+ "fields": {
117+ "status": 4,
118+ "lp_person": 18,
119+ "account": null,
120+ "email": "support@kubuntu.com",
121+ "date_created": "2006-10-16 18:31:43.621341"
122+ }
123+ },
124+ {
125+ "pk": 47,
126+ "model": "identityprovider.emailaddress",
127+ "fields": {
128+ "status": 4,
129+ "lp_person": 19,
130+ "account": null,
131+ "email": "support@isd.com",
132+ "date_created": "2006-10-16 18:31:43.621341"
133+ }
134+ },
135+ {
136 "pk": 61,
137 "model": "identityprovider.emailaddress",
138 "fields": {
139@@ -367,6 +389,88 @@
140 }
141 },
142 {
143+ "pk": 18,
144+ "model": "identityprovider.person",
145+ "fields": {
146+ "displayname": "Kubuntu Team",
147+ "teamowner": 1,
148+ "teamdescription": "This Team is responsible for the Kubuntu Distribution",
149+ "name": "kubuntu-team",
150+ "language": null,
151+ "fti": "'team':3A,5A 'kubuntu':2A,4A 'kubuntu-team':1A",
152+ "defaultmembershipperiod": null,
153+ "defaultrenewalperiod": null,
154+ "subscriptionpolicy": 1,
155+ "merged": null,
156+ "datecreated": "2005-06-06 08:59:51.60576",
157+ "addressline1": null,
158+ "addressline2": null,
159+ "organization": null,
160+ "city": null,
161+ "province": null,
162+ "country": null,
163+ "postcode": null,
164+ "phone": null,
165+ "homepage_content": null,
166+ "icon": null,
167+ "mugshot": null,
168+ "hide_email_addresses": false,
169+ "creation_rationale": null,
170+ "creation_comment": null,
171+ "registrant": null,
172+ "logo": null,
173+ "renewal_policy": 10,
174+ "personal_standing": 0,
175+ "personal_standing_reason": null,
176+ "mail_resumption_date": null,
177+ "mailing_list_auto_subscribe_policy": 1,
178+ "mailing_list_receive_duplicates": true,
179+ "visibility": 1,
180+ "verbose_bugnotifications": false
181+ }
182+ },
183+ {
184+ "pk": 19,
185+ "model": "identityprovider.person",
186+ "fields": {
187+ "displayname": "ISD Team",
188+ "teamowner": 1,
189+ "teamdescription": "This Team is responsible for ISD",
190+ "name": "isd-team",
191+ "language": null,
192+ "fti": "'team':3A,5A 'isd':2A,4A 'isd-team':1A",
193+ "defaultmembershipperiod": null,
194+ "defaultrenewalperiod": null,
195+ "subscriptionpolicy": 1,
196+ "merged": null,
197+ "datecreated": "2005-06-06 08:59:51.60576",
198+ "addressline1": null,
199+ "addressline2": null,
200+ "organization": null,
201+ "city": null,
202+ "province": null,
203+ "country": null,
204+ "postcode": null,
205+ "phone": null,
206+ "homepage_content": null,
207+ "icon": null,
208+ "mugshot": null,
209+ "hide_email_addresses": false,
210+ "creation_rationale": null,
211+ "creation_comment": null,
212+ "registrant": null,
213+ "logo": null,
214+ "renewal_policy": 10,
215+ "personal_standing": 0,
216+ "personal_standing_reason": null,
217+ "mail_resumption_date": null,
218+ "mailing_list_auto_subscribe_policy": 1,
219+ "mailing_list_receive_duplicates": true,
220+ "visibility": 1,
221+ "verbose_bugnotifications": false
222+ }
223+ },
224+ {
225 "pk": 243610,
226 "model": "identityprovider.person",
227 "fields": {
228@@ -501,6 +605,22 @@
229 }
230 },
231 {
232+ "pk": 2,
233+ "model": "identityprovider.teamparticipation",
234+ "fields": {
235+ "team": 18,
236+ "person": 1
237+ }
238+ },
239+ {
240+ "pk": 3,
241+ "model": "identityprovider.teamparticipation",
242+ "fields": {
243+ "team": 19,
244+ "person": 1
245+ }
246+ },
247+ {
248 "pk": 158,
249 "model": "identityprovider.teamparticipation",
250 "fields": {
251@@ -524,7 +644,14 @@
252 "person": 243610
253 }
254 },
255-
256+ {
257+ "pk": 164,
258+ "model": "identityprovider.teamparticipation",
259+ "fields": {
260+ "team": 18,
261+ "person": 243610
262+ }
263+ },
264 {
265 "pk": 5,
266 "model": "identityprovider.openidrpsummary",
267
268=== modified file 'identityprovider/forms.py'
269--- identityprovider/forms.py 2010-09-21 22:04:10 +0000
270+++ identityprovider/forms.py 2011-07-05 15:46:33 +0000
271@@ -5,17 +5,33 @@
272
273 from django import forms
274 from django.contrib import auth
275-from django.forms import fields, widgets
276+from django.forms import Form, fields, widgets
277+from django.utils import translation
278 from django.utils.safestring import mark_safe
279 from django.utils.translation import ugettext as _
280
281-from identityprovider.models import Account, EmailAddress, verify_token_string
282+from identityprovider.const import (
283+ SREG_DATA_FIELDS_ORDER,
284+ SREG_LABELS,
285+)
286+from identityprovider.models import (
287+ Account,
288+ EmailAddress,
289+ verify_token_string,
290+ TeamMembership,
291+)
292 from identityprovider.models.const import EmailStatus
293 from identityprovider.utils import (
294- get_person_and_account_by_email, CannotResetPasswordException,
295- PersonAndAccountNotFoundException, encrypt_launchpad_password,
296- password_policy_compliant)
297-from identityprovider.widgets import ROAwareTextInput, ROAwareSelect
298+ get_person_and_account_by_email,
299+ CannotResetPasswordException,
300+ PersonAndAccountNotFoundException,
301+ encrypt_launchpad_password,
302+ password_policy_compliant,
303+)
304+from identityprovider.widgets import (
305+ ROAwareTextInput,
306+ ROAwareSelect,
307+)
308
309 logger = logging.getLogger('sso')
310
311@@ -289,3 +305,159 @@
312 class PreAuthorizeForm(forms.Form):
313 trust_root = forms.CharField(error_messages=default_errors)
314 callback = forms.CharField(error_messages=default_errors)
315+
316+
317+class SRegRequestForm(Form):
318+ """A form object for user control over OpenID sreg data.
319+ """
320+ fields = {}
321+
322+ @property
323+ def data_approved_by_user(self):
324+ """Get the list of sreg data actually approved by the user for the
325+ request.
326+ """
327+ if self.request_method == 'POST':
328+ return dict([(f, self.data[f]) for f in self.data
329+ if self.field_approved(f)])
330+ else:
331+ return {}
332+
333+ def __init__(self, request, sreg_request, rpconfig, approved_data=None):
334+ self.request = request
335+ self.request_method = request.META.get('REQUEST_METHOD')
336+ self.sreg_request = sreg_request
337+ self.rpconfig = rpconfig
338+ self.approved_data = approved_data
339+ sreg_fields = [f for f in SREG_DATA_FIELDS_ORDER if f in set(
340+ self.sreg_request.required + self.sreg_request.optional)]
341+ self.data = self._get_sreg_data_for_user(request.user, sreg_fields)
342+
343+ super(SRegRequestForm, self).__init__(self.data)
344+ self._init_fields(self.data)
345+ self.should_display = len(self.data) > 0
346+
347+ def _get_sreg_data_for_user(self, user, sreg_fields):
348+ """Get the sreg data to ask about in the form based on the user's
349+ account record.
350+ """
351+ values = {}
352+ values['fullname'] = user.displayname
353+ if user.preferredemail is not None:
354+ values['email'] = user.preferredemail.email
355+ if user.person is not None:
356+ values['nickname'] = user.person.name
357+ if user.person.time_zone is not None:
358+ values['timezone'] = user.person.time_zone
359+ if user.preferredlanguage is not None:
360+ values['language'] = user.preferredlanguage
361+ else:
362+ values['language'] = translation.get_language_from_request(
363+ self.request)
364+ logger.debug("values (sreg_fields) = " + str(values))
365+
366+ return dict([(f, values[f]) for f in sreg_fields if f in values])
367+
368+ def _init_fields(self, data):
369+ """Initialises form fields for the user's sreg data.
370+ """
371+ for key, val in data.items():
372+ label = "%s: %s" % (SREG_LABELS.get(key, key), val)
373+ attrs = {}
374+ if key in self.sreg_request.required:
375+ attrs['class'] = 'required'
376+ if self.rpconfig is not None:
377+ attrs['disabled'] = 'disabled'
378+ self.fields[key] = fields.BooleanField(
379+ label=label, widget=forms.CheckboxInput(attrs=attrs,
380+ check_test=self.check_test))
381+
382+ def check_test(self, value):
383+ """Determines if a checkbox should be pre-checked based on previously
384+ approved user data, openid request and relying party type.
385+ """
386+ for k, v in self.data.items():
387+ if value == v:
388+ value = k
389+ break
390+
391+ if self.rpconfig and value in self.sreg_request.required:
392+ return True
393+ elif (self.approved_data and
394+ value in self.approved_data.get('requested', [])):
395+ return value in self.approved_data.get('approved', [])
396+ elif self.rpconfig:
397+ return True
398+ else:
399+ return value in self.sreg_request.required
400+
401+ def field_approved(self, field):
402+ """Check if the field should be returned in the response based on user
403+ preferences and overridden for trusted relying parties.
404+ """
405+ if self.rpconfig is not None and field in self.sreg_request.required:
406+ return True
407+ elif field in self.request.POST:
408+ return True
409+ else:
410+ return False
411+
412+
413+class TeamsRequestForm(Form):
414+ """A form object for user control over OpenID teams data.
415+ """
416+
417+ fields = {}
418+
419+ @property
420+ def teams_approved_by_user(self):
421+ """Get the list of teams actually approved by the user for the request.
422+ """
423+ if self.request_method == 'POST':
424+ return [t for t in self.data if t in self.request.POST]
425+ else:
426+ return []
427+
428+ def __init__(self, request, teams_request, rpconfig, approved_data=None):
429+ self.request = request
430+ self.request_method = request.META.get('REQUEST_METHOD')
431+ self.teams_request = teams_request
432+ self.rpconfig = rpconfig
433+ self.approved_data = approved_data
434+ self.data = self._get_teams_for_user(request.user,
435+ rpconfig and rpconfig.can_query_any_team)
436+
437+ super(TeamsRequestForm, self).__init__(self.data)
438+ self._init_fields(self.data)
439+ all_teams = self.teams_request.allRequestedTeams()
440+ membership = TeamMembership.get_team_memberships_for_user(
441+ all_teams, self.request.user,
442+ self.rpconfig and self.rpconfig.can_query_any_team)
443+ self.should_display = len(membership) > 0
444+
445+ def _get_teams_for_user(self, user, include_private=False):
446+ """Get the list of teams to ask about in the form based on the user's
447+ team membership.
448+ """
449+ all_teams = self.teams_request.allRequestedTeams()
450+ return dict([(t, t) for t in
451+ TeamMembership.get_team_memberships_for_user(all_teams, user,
452+ include_private)])
453+
454+ def _init_fields(self, form_data):
455+ """Initialises form fields for the user's team memberships.
456+ """
457+ for team in form_data:
458+ self.fields[team] = fields.BooleanField(
459+ label=team, widget=forms.CheckboxInput(
460+ check_test=self.check_test))
461+
462+ def check_test(self, value):
463+ """Determines if a checkbox should be pre-checked based on previously
464+ approved user data and relying party type.
465+ """
466+ if (self.approved_data and
467+ value in self.approved_data.get('requested', [])):
468+ return value in self.approved_data.get('approved', [])
469+ else:
470+ return self.rpconfig != None
471
472=== modified file 'identityprovider/media/ubuntu/narrow.css'
473--- identityprovider/media/ubuntu/narrow.css 2010-09-18 05:38:59 +0000
474+++ identityprovider/media/ubuntu/narrow.css 2011-07-05 15:46:33 +0000
475@@ -36,7 +36,12 @@
476 margin: 0px;
477 }
478
479-#mainbar form input[type="text"], #mainbar form input[type="password"], form textarea, #content input {
480+#mainbar form input[type="text"],
481+#mainbar form input[type="password"],
482+form textarea,
483+#content input[type="text"],
484+#content input[type="password"]
485+{
486 width: 96%;
487 padding: 2px;
488 }
489
490=== modified file 'identityprovider/media/ubuntu/styles.css'
491--- identityprovider/media/ubuntu/styles.css 2010-11-16 15:02:08 +0000
492+++ identityprovider/media/ubuntu/styles.css 2011-07-05 15:46:33 +0000
493@@ -249,7 +249,14 @@
494 ul li:last-child {
495 margin-bottom: 0;
496 }
497-
498+ul.teams-list {
499+ list-style: none;
500+ margin-left: 20px;
501+ margin-top: 5px;
502+}
503+.required + label {
504+ font-weight: bold;
505+}
506 p.call-to-action {
507 color: #333;
508 }
509@@ -711,7 +718,12 @@
510 margin: 0px;
511 }
512
513- #mainbar form input[type="text"], #mainbar form input[type="password"], form textarea, #content input {
514+ #mainbar form input[type="text"],
515+ #mainbar form input[type="password"],
516+ form textarea,
517+ #content input[type="text"],
518+ #content input[type="password"]
519+ {
520 width: 96%;
521 padding: 2%;
522 }
523
524=== modified file 'identityprovider/models/account.py'
525--- identityprovider/models/account.py 2011-05-12 13:44:51 +0000
526+++ identityprovider/models/account.py 2011-07-05 15:46:33 +0000
527@@ -271,7 +271,6 @@
528 except Consumer.DoesNotExist:
529 return []
530
531-
532 def save(self, force_insert=False, force_update=False, **kwargs):
533 if settings.READ_ONLY_MODE:
534 return
535@@ -321,4 +320,4 @@
536 verbose_name_plural = _("LP OpenID Identifiers")
537
538 def __unicode__(self):
539- return _("LP OpenID Idetifier for %s") % unicode(self.account)
540+ return _("LP OpenID Identifier for %s") % unicode(self.lp_account)
541
542=== modified file 'identityprovider/models/openidmodels.py'
543--- identityprovider/models/openidmodels.py 2010-11-16 14:11:00 +0000
544+++ identityprovider/models/openidmodels.py 2011-07-05 15:46:33 +0000
545@@ -12,6 +12,7 @@
546
547 from django.conf import settings
548 from django.db import models
549+from django.utils import simplejson as json
550 from django.utils.translation import ugettext_lazy as _
551
552 from identityprovider.const import NEVER_EXPIRES, SREG_LABELS
553@@ -139,7 +140,7 @@
554 select={'len': 'length(trust_root)'},
555 where=[condition],
556 params=(url, url),
557- order_by=['-len'] # Select the longest match
558+ order_by=['-len'] # Select the longest match
559 )
560 return q[0] if q else None
561
562@@ -177,7 +178,8 @@
563
564 class OpenIDRPSummaryManager(models.Manager):
565
566- def record(self, account, trust_root, openid_identifier=None):
567+ def record(self, account, trust_root, openid_identifier=None,
568+ approved_data=None):
569 if settings.READ_ONLY_MODE:
570 return None
571 if openid_identifier is None:
572@@ -193,6 +195,8 @@
573 account=account,
574 trust_root=trust_root,
575 openid_identifier=openid_identifier)
576+ if approved_data:
577+ summary.set_approved_data(approved_data)
578 return summary
579
580
581@@ -205,6 +209,7 @@
582 date_last_used = models.DateTimeField(default=datetime.datetime.utcnow,
583 blank=True, editable=False)
584 total_logins = models.IntegerField(default=1)
585+ approved_data = models.TextField(blank=True, null=True, default='')
586
587 objects = OpenIDRPSummaryManager()
588
589@@ -218,6 +223,18 @@
590 self.date_last_used = datetime.datetime.utcnow()
591 self.save()
592
593+ def get_approved_data(self):
594+ try:
595+ data = json.loads(self.approved_data)
596+ assert(type(data) is dict)
597+ return data
598+ except (AssertionError, TypeError, ValueError):
599+ return None
600+
601+ def set_approved_data(self, approved_data):
602+ self.approved_data = json.dumps(approved_data)
603+ self.save()
604+
605
606 class DjangoOpenIDStore(OpenIDStore):
607 """
608
609=== modified file 'identityprovider/models/team.py'
610--- identityprovider/models/team.py 2010-04-21 15:29:24 +0000
611+++ identityprovider/models/team.py 2011-07-05 15:46:33 +0000
612@@ -3,6 +3,7 @@
613
614 from django.db import models
615
616+from identityprovider.const import PERSON_VISIBILITY_PUBLIC
617 from identityprovider.models import Person
618
619 __all__ = (
620@@ -47,6 +48,24 @@
621 db_table = u'teammembership'
622 unique_together = ('person', 'team')
623
624+ @staticmethod
625+ def get_team_memberships_for_user(team_names, user, include_private=False):
626+ memberships = []
627+ for team_name in team_names:
628+ try:
629+ team = Person.objects.get(name=team_name)
630+ except Person.DoesNotExist:
631+ team = None
632+ if team is None or not team.is_team():
633+ continue
634+ # Control access to private teams
635+ if (team.visibility != PERSON_VISIBILITY_PUBLIC and
636+ not include_private):
637+ continue
638+ if user.person_in_team(team):
639+ memberships.append(team_name)
640+ return memberships
641+
642
643 class TeamParticipation(models.Model):
644 team = models.ForeignKey(Person, db_column='team',
645
646=== modified file 'identityprovider/teams.py'
647--- identityprovider/teams.py 2010-04-21 15:29:24 +0000
648+++ identityprovider/teams.py 2011-07-05 15:46:33 +0000
649@@ -39,8 +39,10 @@
650 @since: 2.1.1
651 """
652
653-from openid.message import registerNamespaceAlias, \
654- NamespaceAliasRegistrationError
655+from openid.message import (
656+ registerNamespaceAlias,
657+ NamespaceAliasRegistrationError,
658+)
659 from openid.extension import Extension
660 from openid import oidutil
661
662
663=== modified file 'identityprovider/templates/decide.html'
664--- identityprovider/templates/decide.html 2011-05-11 13:02:21 +0000
665+++ identityprovider/templates/decide.html 2011-07-05 15:46:33 +0000
666@@ -6,6 +6,11 @@
667 GNU Affero General Public License version 3 (see the file LICENSE).
668 {% endcomment %}
669
670+{% block extra_header %}
671+- <script src="http://yui.yahooapis.com/3.3.0/build/yui/yui-min.js"></script>
672+{% endblock %}
673+
674+
675 {% block title %}
676 {% blocktrans %}Authenticate to {{ trust_root }}{% endblocktrans %}
677 {% endblock %}
678@@ -27,25 +32,35 @@
679 {% blocktrans %}If you proceed, the following information will be available to the website:{% endblocktrans %}
680 {% endif %}
681
682+ <div class="actions">
683+ <form action="{{ action }}" method="POST" name="decideform">
684+ {% csrf_token %}
685 <div class="info-items">
686- {% if sreg_data or teams_data %}
687+ {% if sreg_form.should_display or teams_form.should_display %}
688 <ul class="list">
689- {% for key, value in sreg_data %}
690- <li>{{ key }}: <b>{{ value }}</b></li>
691- {% endfor %}
692- {% if teams_data %}
693- <li>{% trans "Team membership"%}: <b>{{ teams_data|join:", " }}</b></li>
694- {% endif %}
695+ {% for field in sreg_form %}
696+ <li>{{ field|safe }} {{ field.label_tag }}</li>
697+ {% endfor %}
698+ {% if teams_form.should_display %}
699+ {% ifequal teams_form.fields|length 1 %}
700+ {% for field in teams_form %}
701+ <li>{{ field|safe }} {% trans "Team membership:"%} {{ field.label_tag }}</li>
702+ {% endfor %}
703+ {% else %}
704+ <li id="teamslist_item">{% trans "Team membership:"%}
705+ <ul class="teams-list" id="teamslist">
706+ {% for field in teams_form %}
707+ <li>{{ field|safe }} {{ field.label_tag }}</li>
708+ {% endfor %}
709+ </ul>
710+ </li>
711+ {% endifequal %}
712+ {% endif %}
713 </ul>
714- {% else %}
715- <p>{% trans "Only your identity URL" %}</p>
716 {% endif %}
717 </div>
718 </div>
719
720- <div class="actions">
721- <form action="{{ action }}" method="POST" name="decideform">
722- {% csrf_token %}
723 <p>
724 <button type="submit" class="btn" name="yes"><span><span>{% trans "Yes, sign me in" %}</span></span></button>
725 {% trans "or" %}
726@@ -54,6 +69,78 @@
727 </form>
728 <script type="text/javascript">
729 document.decideform.yes.focus();
730+
731+ YUI().use('anim-base', function(Y) {
732+ var anim = new Y.Anim({
733+ node: '#teamslist',
734+ duration: 0.15,
735+ to: { height: 0 },
736+ });
737+
738+ var onClick = function(e) {
739+ e.preventDefault();
740+ Y.one('#reveal_wrapper').remove();
741+ node = Y.one('#teamslist');
742+ node.setStyle('height', '0');
743+ node.setStyle('display', 'block');
744+ anim.setAttrs({ 'to': { height: teamslist_height } });
745+ anim.run();
746+ };
747+
748+ var setTeamStates = function(e) {
749+ state = e.currentTarget.get('checked');
750+ nl = Y.all('#teamslist li input').set('checked', state);
751+ }
752+
753+ var refreshTeamStates = function(e) {
754+ nl = Y.all('#teamslist li input');
755+ checked = 0;
756+ total = nl.size();
757+ for (i = 0; i < nl.size(); i++) {
758+ if (nl.item(i).get('checked')) {
759+ checked ++;
760+ }
761+ }
762+ if (checked == 0) {
763+ Y.one('#checkme').set('checked', false);
764+ } else {
765+ Y.one('#checkme').set('checked', true);
766+ }
767+ }
768+
769+ Y.on("domready", function() {
770+ // don't error out because no teams matched
771+ if (Y.one('#teamslist_item') == null) {
772+ return;
773+ }
774+ Y.one('#teamslist_item').prepend('<input type="checkbox" id="checkme" /> ');
775+ Y.one('#checkme').on('change', setTeamStates);
776+ Y.all('#teamslist li').on('change', refreshTeamStates);
777+ refreshTeamStates();
778+
779+ nl = Y.all('#teamslist li input');
780+ checked = 0;
781+ total = nl.size();
782+ for (i = 0; i < nl.size(); i++) {
783+ if (nl.item(i).get('checked')) {
784+ checked ++;
785+ }
786+ }
787+ if (checked == total || checked == 0) {
788+ node = Y.one('#teamslist');
789+ teamslist_height = parseInt(node.getComputedStyle('height'));
790+ node.setStyle('display', 'none');
791+ buf = new Array();
792+ c = node.get('children');
793+ for (i = 0; i < c.size(); i++) {
794+ buf.push(c.item(i).get('text'));
795+ }
796+ node.insert('<span id="reveal_wrapper">' + buf.join(', ') + ' <small style="font-size: 80%;">(<a href="#" id="reveal">details</a>)</small></span>', "before");
797+ Y.one('#reveal').on('click', onClick);
798+ }
799+ });
800+ });
801+
802 </script>
803 </div>
804 <br style="clear: both" />
805
806=== modified file 'identityprovider/tests/test_admin.py'
807--- identityprovider/tests/test_admin.py 2011-01-03 21:58:15 +0000
808+++ identityprovider/tests/test_admin.py 2011-07-05 15:46:33 +0000
809@@ -19,6 +19,7 @@
810 class AdminTestCase(BasicAccountTestCase):
811
812 fixtures = ['admin.json', 'test.json']
813+ pgsql_functions = ['generate_openid_identifier']
814
815 def setUp(self):
816 self.disable_csrf()
817@@ -41,8 +42,10 @@
818 'allowed_sreg': 'fullname',
819 'creation_rationale': '13',
820 }
821- response = self.client.get('/admin/identityprovider/openidrpconfig/add/')
822- response = self.client.post('/admin/identityprovider/openidrpconfig/add/',
823+ response = self.client.get(
824+ '/admin/identityprovider/openidrpconfig/add/')
825+ response = self.client.post(
826+ '/admin/identityprovider/openidrpconfig/add/',
827 data)
828 self.assertEquals(302, response.status_code)
829 # We don't get the ID back, so continue on to the next test to
830
831=== modified file 'identityprovider/tests/test_forms.py'
832--- identityprovider/tests/test_forms.py 2011-02-20 23:49:21 +0000
833+++ identityprovider/tests/test_forms.py 2011-07-05 15:46:33 +0000
834@@ -4,10 +4,20 @@
835 # GNU Affero General Public License version 3 (see the file LICENSE).
836
837 from django.conf import settings
838+from django.http import HttpRequest
839+from django_openid_auth.teams import TeamsRequest
840+from openid.extensions.sreg import SRegRequest
841+
842 from identityprovider.models.account import Account
843-from identityprovider.forms import EditAccountForm, ResetPasswordForm
844-from identityprovider.api10 import forms
845+from identityprovider.forms import (
846+ EditAccountForm,
847+ ResetPasswordForm,
848+ SRegRequestForm,
849+ TeamsRequestForm,
850+)
851 from identityprovider.api10.forms import WebserviceCreateAccountForm
852+from identityprovider.models.openidmodels import OpenIDRPConfig
853+
854 from utils import BasicAccountTestCase, SQLCachedTestCase
855
856
857@@ -51,7 +61,7 @@
858 account = Account.objects.get(openid_identifier='name12_oid')
859 form = EditAccountForm(account=account,
860 data={'password': 'tooweak',
861- 'passwordconfirm' : 'other'})
862+ 'passwordconfirm': 'other'})
863 self.assertFalse(form.is_valid())
864 self.assertEqual(1, len(form.errors['password']))
865 self.assertFalse('passwordconfirm' in form.errors)
866@@ -61,7 +71,7 @@
867 account = Account.objects.get(openid_identifier='name12_oid')
868 form = EditAccountForm(account=account,
869 data={'password': 'tooweak',
870- 'passwordconfirm' : 'tooweak'})
871+ 'passwordconfirm': 'tooweak'})
872 self.assertFalse(form.is_valid())
873 self.assertEqual(1, len(form.errors['password']))
874 self.assertFalse('passwordconfirm' in form.errors)
875@@ -94,3 +104,249 @@
876 self.assertFalse(form.is_valid())
877 self.assertEqual(form.errors['password'][0],
878 'Invalid characters in password')
879+
880+
881+class SRegRequestFormTest(SQLCachedTestCase):
882+ pgsql_functions = ['generate_openid_identifier']
883+
884+ def _get_request_with_post_args(self, args={}):
885+ request = HttpRequest()
886+ request.user = self.test_user
887+ request.POST = args
888+ request.META = {'REQUEST_METHOD': 'POST'}
889+ return request
890+
891+ def setUp(self):
892+ self.test_user = Account.objects.create_account('My name',
893+ 'me@test.com', 'password')
894+ self.rpconfig = OpenIDRPConfig.objects.create(
895+ trust_root='http://localhost/', description="Some description")
896+
897+ def test_no_approved_fields_without_post_request(self):
898+ """The server should not generate a list of approved fields when the
899+ request is not a POST request.
900+ """
901+ request = self._get_request_with_post_args()
902+ request.META['REQUEST_METHOD'] = 'GET'
903+ form = SRegRequestForm(
904+ request,
905+ SRegRequest(required=['fullname', 'email']),
906+ self.rpconfig)
907+ self.assertEqual(len(form.data_approved_by_user), 0)
908+
909+ def test_required_fields_for_trusted_site(self):
910+ """The server should always return values for required fields to trusted
911+ sites, regardless of the state of the checkbox in the UI. Optional
912+ fields should not be returned if the user has unchecked them.
913+ """
914+ form = SRegRequestForm(
915+ self._get_request_with_post_args(),
916+ SRegRequest(required=['fullname'], optional=['email']),
917+ self.rpconfig)
918+ self.assertTrue('fullname' in form.data_approved_by_user)
919+ self.assertFalse('email' in form.data_approved_by_user)
920+
921+ def test_optional_fields_for_trusted_site(self):
922+ """The server should return values for optional fields to trusted
923+ sites only when the user checks the checkbox in the UI.
924+ """
925+ post_args = {'email': 'email'}
926+ form = SRegRequestForm(
927+ self._get_request_with_post_args(post_args),
928+ SRegRequest(optional=['fullname', 'email']),
929+ self.rpconfig)
930+ self.assertFalse('fullname' in form.data_approved_by_user)
931+ self.assertTrue('email' in form.data_approved_by_user)
932+
933+ def test_required_fields_for_untrusted_site(self):
934+ """The server should return values for required fields to untrusted
935+ sites only when the user checks the checkbox in the UI.
936+ """
937+ post_args = {'email': 'email'}
938+ form = SRegRequestForm(
939+ self._get_request_with_post_args(post_args),
940+ SRegRequest(required=['fullname', 'email']),
941+ None)
942+ self.assertFalse('fullname' in form.data_approved_by_user)
943+ self.assertTrue('email' in form.data_approved_by_user)
944+
945+ def test_optional_fields_for_untrusted_site(self):
946+ """The server should return values for optional fields to untrusted
947+ sites only when the user checks the checkbox in the UI.
948+ """
949+ post_args = {'fullname': 'fullname'}
950+ form = SRegRequestForm(
951+ self._get_request_with_post_args(post_args),
952+ SRegRequest(optional=['fullname', 'email']),
953+ None)
954+ self.assertTrue('fullname' in form.data_approved_by_user)
955+ self.assertFalse('email' in form.data_approved_by_user)
956+
957+ def test_checkbox_status_for_trusted_site(self):
958+ """Checkboxes are always checked if the site is trusted
959+ """
960+ form = SRegRequestForm(
961+ self._get_request_with_post_args(),
962+ SRegRequest(required=['fullname'], optional=['email']),
963+ self.rpconfig)
964+ self.assertTrue(form.check_test('fullname'))
965+ self.assertTrue(form.check_test('email'))
966+
967+ def test_checkbox_status_for_trusted_site_with_approved_data(self):
968+ """If the user has previously approved sending data to a trusted site
969+ the same checkbox settings should be returned on the next request unless
970+ those conflict with the required fields.
971+ """
972+ approved_data = {
973+ 'requested': ['fullname', 'email'],
974+ 'approved': ['email']}
975+ form1 = SRegRequestForm(
976+ self._get_request_with_post_args(),
977+ SRegRequest(required=['fullname'], optional=['email']),
978+ self.rpconfig, approved_data=approved_data)
979+ self.assertTrue(form1.check_test('fullname'))
980+ self.assertTrue(form1.check_test('email'))
981+
982+ approved_data['approved'] = []
983+ form2 = SRegRequestForm(
984+ self._get_request_with_post_args(),
985+ SRegRequest(required=['fullname'], optional=['email']),
986+ self.rpconfig, approved_data=approved_data)
987+ self.assertFalse(form2.check_test('email'))
988+
989+ def test_checkbox_status_for_untrusted_site(self):
990+ """Checkboxes are only checked on untrusted site requests if the field
991+ is required
992+ """
993+ form = SRegRequestForm(
994+ self._get_request_with_post_args(),
995+ SRegRequest(required=['fullname'], optional=['email']),
996+ None)
997+ self.assertTrue(form.check_test('fullname'))
998+ self.assertFalse(form.check_test('email'))
999+
1000+ def test_checkbox_status_for_untrusted_site_with_approved_data(self):
1001+ """If the user has previously approved sending data to an untrusted site
1002+ the same checkbox settings should be returned on the next request.
1003+ """
1004+ approved_data = {
1005+ 'requested': ['fullname', 'email'],
1006+ 'approved': ['email']}
1007+ form = SRegRequestForm(
1008+ self._get_request_with_post_args(),
1009+ SRegRequest(required=['fullname'], optional=['email']),
1010+ None, approved_data=approved_data)
1011+ self.assertFalse(form.check_test('fullname'))
1012+ self.assertTrue(form.check_test('email'))
1013+
1014+
1015+class TeamsRequestFormTest(BasicAccountTestCase):
1016+
1017+ fixtures = ["test"]
1018+
1019+ def _get_request_with_post_args(self, args={}):
1020+ request = HttpRequest()
1021+ request.user = self.account
1022+ request.POST = args
1023+ request.META = {'REQUEST_METHOD': 'POST'}
1024+ return request
1025+
1026+ def setUp(self):
1027+ self.account = Account.objects.get(pk=1)
1028+ self.rpconfig = OpenIDRPConfig.objects.create(
1029+ trust_root='http://localhost/', description="Some description",
1030+ can_query_any_team=True)
1031+
1032+ def test_selected_teams_for_trusted_sites(self):
1033+ """If a user checks a requested team in the form for a trusted consumer,
1034+ it should be in the list of teams approved by the user.
1035+ """
1036+ post_args = {'ubuntu-team': 'ubuntu-team'}
1037+ form = TeamsRequestForm(
1038+ self._get_request_with_post_args(post_args),
1039+ TeamsRequest(query_membership=['ubuntu-team']),
1040+ self.rpconfig)
1041+ self.assertTrue('ubuntu-team' in form.teams_approved_by_user)
1042+
1043+ def test_unselected_teams_for_trusted_sites(self):
1044+ """If a user unchecks a requested team in the form for a trusted
1045+ consumer, it should not be in the list of teams approved by the user.
1046+ """
1047+ form = TeamsRequestForm(
1048+ self._get_request_with_post_args(),
1049+ TeamsRequest(query_membership=['ubuntu-team']),
1050+ self.rpconfig)
1051+ self.assertFalse('ubuntu-team' in form.teams_approved_by_user)
1052+
1053+ def test_selected_teams_for_untrusted_sites(self):
1054+ """If a user checks a requested team in the form for an untrusted
1055+ consumer, it should be in the list of teams approved by the user.
1056+ """
1057+ post_args = {'ubuntu-team': 'ubuntu-team'}
1058+ form = TeamsRequestForm(
1059+ self._get_request_with_post_args(post_args),
1060+ TeamsRequest(query_membership=['ubuntu-team']),
1061+ None)
1062+ self.assertTrue('ubuntu-team' in form.teams_approved_by_user)
1063+
1064+ def test_unselected_teams_for_untrusted_sites(self):
1065+ """If a user unchecks a requested team in the form for an untrusted
1066+ consumer, it should not be in the list of teams approved by the user.
1067+ """
1068+ form = TeamsRequestForm(
1069+ self._get_request_with_post_args(),
1070+ TeamsRequest(query_membership=['ubuntu-team']),
1071+ None)
1072+ self.assertFalse('ubuntu-team' in form.teams_approved_by_user)
1073+
1074+ def test_checkbox_status_for_trusted_site(self):
1075+ """Checkboxes should always be checked by default for trusted sites.
1076+ """
1077+ form = TeamsRequestForm(
1078+ self._get_request_with_post_args(),
1079+ TeamsRequest(query_membership=['ubuntu-team']),
1080+ self.rpconfig)
1081+ self.assertTrue(form.check_test('ubuntu-team'))
1082+
1083+ def test_checkbox_status_for_trusted_site_with_approved_data(self):
1084+ """Checkboxes should respect user preferences on trusted sites where
1085+ available.
1086+ """
1087+ approved_data = {
1088+ 'requested': ['ubuntu-team', 'myteam'],
1089+ 'approved': ['myteam']}
1090+ form = TeamsRequestForm(
1091+ self._get_request_with_post_args(),
1092+ TeamsRequest(query_membership=['ubuntu-team', 'myteam']),
1093+ self.rpconfig, approved_data=approved_data)
1094+ self.assertFalse(form.check_test('ubuntu-team'))
1095+ self.assertTrue(form.check_test('myteam'))
1096+
1097+ def test_checkbox_status_for_untrusted_site(self):
1098+ """Checkboxes should always be checked by default for trusted sites.
1099+ """
1100+ form = TeamsRequestForm(
1101+ self._get_request_with_post_args(),
1102+ TeamsRequest(query_membership=['ubuntu-team']),
1103+ None)
1104+ self.assertFalse(form.check_test('ubuntu-team'))
1105+
1106+ def test_checkbox_status_for_untrusted_site_with_approved_data(self):
1107+ """Checkboxes should respect user preferences on untrusted sited where
1108+ available.
1109+ """
1110+ approved_data = {
1111+ 'requested': ['ubuntu-team'],
1112+ 'approved': ['ubuntu-team']}
1113+ form1 = TeamsRequestForm(
1114+ self._get_request_with_post_args(),
1115+ TeamsRequest(query_membership=['ubuntu-team', 'myteam']),
1116+ None, approved_data=approved_data)
1117+ self.assertTrue(form1.check_test('ubuntu-team'))
1118+
1119+ approved_data['approved'] = []
1120+ form2 = TeamsRequestForm(
1121+ self._get_request_with_post_args(),
1122+ TeamsRequest(query_membership=['ubuntu-team', 'myteam']),
1123+ None, approved_data=approved_data)
1124+ self.assertFalse(form2.check_test('ubuntu-team'))
1125
1126=== modified file 'identityprovider/tests/test_models_openidmodels.py'
1127--- identityprovider/tests/test_models_openidmodels.py 2010-07-09 13:44:39 +0000
1128+++ identityprovider/tests/test_models_openidmodels.py 2011-07-05 15:46:33 +0000
1129@@ -158,7 +158,7 @@
1130 self.expires = datetime.now() + timedelta(1)
1131
1132 def create_auth(self):
1133- return OpenIDAuthorization.objects.create( account=self.account,
1134+ return OpenIDAuthorization.objects.create(account=self.account,
1135 client_id=None, date_expires=self.expires,
1136 trust_root=self.trust_root)
1137
1138@@ -219,13 +219,19 @@
1139
1140
1141 class OpenIDRPSummaryTestCase(BasicAccountTestCase):
1142+ approved_data = {
1143+ 'test1': {'one': [1, 2], 'two': [2, 3]},
1144+ 'test2': {'three': [3, 4], 'four': [4, 5]}}
1145+
1146 def setUp(self):
1147 self.account = Account.objects.get_by_email('test@canonical.com')
1148 self.trust_root = 'http://openid.launchpad.dev'
1149
1150 def create_summary(self):
1151- return OpenIDRPSummary.objects.create(account=self.account,
1152+ summary = OpenIDRPSummary.objects.create(account=self.account,
1153 trust_root=self.trust_root, openid_identifier='oid')
1154+ summary.set_approved_data(self.approved_data)
1155+ return summary
1156
1157 def test_record_when_readonly(self):
1158 rm = ReadOnlyManager()
1159@@ -244,6 +250,7 @@
1160 def test_record_with_existing_and_no_openid_identifier(self):
1161 summary1 = self.create_summary()
1162 summary2 = OpenIDRPSummary.objects.record(self.account, self.trust_root)
1163+ summary2.set_approved_data(self.approved_data)
1164 self.assertEqual(summary1.total_logins, 1)
1165 self.assertEqual(summary2.total_logins, 1)
1166 self.assertNotEqual(summary1, summary2)
1167@@ -258,7 +265,8 @@
1168 def test_record_existing_different_oid(self):
1169 summary1 = self.create_summary()
1170 summary2 = OpenIDRPSummary.objects.record(self.account,
1171- self.trust_root, openid_identifier='oid1')
1172+ self.trust_root, openid_identifier='oid1',
1173+ approved_data=self.approved_data)
1174 self.assertEqual(summary1.total_logins, 1)
1175 self.assertEqual(summary2.total_logins, 1)
1176 self.assertNotEqual(summary1, summary2)
1177@@ -267,7 +275,6 @@
1178 summary1 = OpenIDRPSummary.objects.record(self.account, self.trust_root,
1179 openid_identifier='oid')
1180 self.assertEqual(summary1.total_logins, 1)
1181-
1182 summary2 = OpenIDRPSummary.objects.get(account=self.account,
1183 trust_root=self.trust_root, openid_identifier='oid')
1184 self.assertEqual(summary1, summary2)
1185@@ -278,3 +285,10 @@
1186
1187 summary.increment()
1188 self.assertEqual(summary.total_logins, 2)
1189+
1190+ def test_approved_data(self):
1191+ summary = self.create_summary()
1192+ summary.set_approved_data(self.approved_data)
1193+ summary2 = OpenIDRPSummary.objects.get(account=self.account,
1194+ trust_root=self.trust_root, openid_identifier='oid')
1195+ self.assertEqual(summary2.get_approved_data(), self.approved_data)
1196
1197=== modified file 'identityprovider/tests/test_models_team.py'
1198--- identityprovider/tests/test_models_team.py 2010-05-20 21:27:05 +0000
1199+++ identityprovider/tests/test_models_team.py 2011-07-05 15:46:33 +0000
1200@@ -1,9 +1,20 @@
1201 # Copyright 2010 Canonical Ltd. This software is licensed under the
1202 # GNU Affero General Public License version 3 (see the file LICENSE).
1203
1204-from identityprovider.models.person import Person
1205-from identityprovider.models.team import TeamParticipation
1206-from identityprovider.tests.utils import SQLCachedTestCase
1207+from identityprovider.const import (
1208+ PERSON_VISIBILITY_PUBLIC,
1209+ PERSON_VISIBILITY_PRIVATE_MEMBERSHIP,
1210+)
1211+from identityprovider.models import (
1212+ Account,
1213+ Person,
1214+ TeamMembership,
1215+ TeamParticipation,
1216+)
1217+from identityprovider.tests.utils import (
1218+ BasicAccountTestCase,
1219+ SQLCachedTestCase,
1220+)
1221
1222
1223 class TeamParticipationTestCase(SQLCachedTestCase):
1224@@ -12,3 +23,46 @@
1225 person = Person.objects.create(displayname='Person', name='person')
1226 tp = TeamParticipation.objects.create(team=team, person=person)
1227 self.assertEqual(unicode(tp), u'Person in Team')
1228+
1229+
1230+class TeamMembershipTest(BasicAccountTestCase):
1231+
1232+ fixtures = ["test"]
1233+
1234+ def _set_team_to_private_membership(self, team_name):
1235+ self._set_team_privacy(team_name, PERSON_VISIBILITY_PRIVATE_MEMBERSHIP)
1236+
1237+ def _set_team_to_public_membership(self, team_name):
1238+ self._set_team_privacy(team_name, PERSON_VISIBILITY_PUBLIC)
1239+
1240+ def _set_team_privacy(self, team_name, privacy):
1241+ team = Person.objects.get(name=team_name)
1242+ team.visibility = privacy
1243+ team.save()
1244+
1245+ def setUp(self):
1246+ self.account = Account.objects.get(pk=1)
1247+
1248+ def test_get_team_memberships_on_personless_account(self):
1249+ account = Account.objects.create_account('test', 'x@example.com',
1250+ 'password')
1251+ memberships = TeamMembership.get_team_memberships_for_user(
1252+ ['ubuntu-team'], account, False)
1253+ self.assertEquals(memberships, [])
1254+
1255+ def test_get_team_memberships_when_team_is_visible(self):
1256+ memberships = TeamMembership.get_team_memberships_for_user(
1257+ ['ubuntu-team'], self.account, False)
1258+ self.assertEquals(memberships, ['ubuntu-team'])
1259+
1260+ def test_get_team_membership_when_team_is_not_visible(self):
1261+ self._set_team_to_private_membership('ubuntu-team')
1262+ memberships = TeamMembership.get_team_memberships_for_user(
1263+ ['ubuntu-team', 'myteam'], self.account, False)
1264+ self.assertEquals(memberships, [])
1265+
1266+ def test_team_is_private_but_you_can_see_them(self):
1267+ self._set_team_to_private_membership('ubuntu-team')
1268+ memberships = TeamMembership.get_team_memberships_for_user(
1269+ ['ubuntu-team', 'myteam'], self.account, True)
1270+ self.assertEquals(memberships, ['ubuntu-team'])
1271
1272=== modified file 'identityprovider/tests/test_views_server.py'
1273--- identityprovider/tests/test_views_server.py 2011-01-17 15:54:37 +0000
1274+++ identityprovider/tests/test_views_server.py 2011-07-05 15:46:33 +0000
1275@@ -3,14 +3,17 @@
1276
1277 import datetime
1278 import urlparse
1279-
1280 from random import randint
1281+
1282 from django.conf import settings
1283 from django.contrib.auth.models import AnonymousUser
1284+from django.http import HttpRequest
1285 from django.test import TestCase
1286 from openid.extensions import pape
1287 from openid.extensions.sreg import SRegRequest
1288
1289+from mock import patch
1290+
1291 from openid.message import (Message, IDENTIFIER_SELECT, OPENID1_URL_LIMIT,
1292 OPENID2_NS)
1293 from openid.yadis.constants import YADIS_HEADER_NAME
1294@@ -20,16 +23,16 @@
1295 from identityprovider.models import (Account, OpenIDAuthorization,
1296 OpenIDRPConfig, Person)
1297 from identityprovider.models.account import LPOpenIdIdentifier
1298-from identityprovider.models.const import (AccountStatus, EmailStatus,
1299- AccountCreationRationale)
1300+from identityprovider.models.const import (
1301+ AccountStatus,
1302+ AccountCreationRationale,
1303+)
1304 from identityprovider.models.person import PersonLocation
1305 from identityprovider.models.authtoken import create_token
1306
1307 from identityprovider.views import server
1308 from identityprovider.tests.utils import (SQLCachedTestCase,
1309 BasicAccountTestCase, AuthenticatedTestCase, OpenIDProviderTestCase)
1310-from identityprovider.const import PERSON_VISIBILITY_PRIVATE_MEMBERSHIP
1311-
1312
1313
1314 class DummyORequest(object):
1315@@ -41,13 +44,15 @@
1316 def idSelect(self):
1317 return False
1318
1319+
1320 class DummySession(dict):
1321
1322 @property
1323 def session_key(self):
1324 return 'abc'
1325
1326- def flush(self): pass
1327+ def flush(self):
1328+ pass
1329
1330
1331 class DummyRequest(object):
1332@@ -55,7 +60,7 @@
1333 def __init__(self):
1334 self.session = DummySession()
1335 self.COOKIES = {}
1336- self.META ={}
1337+ self.META = {}
1338
1339
1340 class HandleOpenIDErrorTestCase(OpenIDProviderTestCase):
1341@@ -175,6 +180,7 @@
1342 self.assertTrue(r['Location'].endswith('+decide'))
1343 self.assertEqual(openid_referer.value, META['HTTP_REFERER'])
1344
1345+
1346 class MultiLangOpenIDTestCase(TestCase):
1347 def setUp(self):
1348 self.orig_language_code = settings.LANGUAGE_CODE
1349@@ -218,6 +224,7 @@
1350 response = self.client.get('/+openid')
1351 self.assertEqual('de', self.client.session['django_language'])
1352
1353+
1354 class ValidOpenIDTestCase(OpenIDProviderTestCase):
1355
1356 def test_is_valid_openid_idselect(self):
1357@@ -280,6 +287,7 @@
1358
1359 class DecideTestCase(AuthenticatedTestCase, OpenIDProviderTestCase):
1360 fixtures = ['test']
1361+
1362 def _prepare_openid_token(self, param_overrides=None):
1363 request = {'openid.mode': 'checkid_setup',
1364 'openid.trust_root': 'http://localhost/',
1365@@ -363,14 +371,12 @@
1366 self.assertEqual(r.status_code, 200)
1367 self.assertTemplateUsed(r, 'registration/login.html')
1368
1369-
1370 def test_list_of_details_is_complete(self):
1371 self.client.login(username='mark@example.com',
1372 password='test')
1373
1374 # create a trusted rpconfig
1375 rpconfig = OpenIDRPConfig(trust_root='http://localhost/',
1376- allowed_sreg="nickname,email,fullname,timezone,language",
1377 can_query_any_team=True,
1378 description="Some description",
1379 )
1380@@ -386,6 +392,95 @@
1381 self.assertContains(response, "Email address")
1382 self.assertContains(response, "Preferred language")
1383
1384+ def test_state_of_checkboxes_and_data_formats_trusted(self):
1385+ self.client.login(username='mark@example.com',
1386+ password='test')
1387+
1388+ # create a trusted rpconfig
1389+ rpconfig = OpenIDRPConfig(trust_root='http://localhost/',
1390+ can_query_any_team=True,
1391+ description="Some description",
1392+ )
1393+ rpconfig.save()
1394+ param_overrides = {
1395+ 'openid.sreg.required': 'nickname,email,fullname',
1396+ 'openid.sreg.optional': 'language',
1397+ 'openid.lp.query_membership': 'ubuntu-team,launchpad-team,isd-team',
1398+ }
1399+ self._prepare_openid_token(param_overrides=param_overrides)
1400+ response = self.client.get('/%s/+decide' % self.token)
1401+ # checkbox checked and disabled for required fields and label is bold
1402+ username_html = ('<li><input checked="checked" name="nickname" '
1403+ 'value="mark" class="required" disabled="disabled" '
1404+ 'type="checkbox" id="id_nickname" /> <label '
1405+ 'for="id_nickname">Username: mark</label></li>')
1406+ email_html = ('<li><input checked="checked" name="email" '
1407+ 'value="mark@example.com" class="required" '
1408+ 'disabled="disabled" type="checkbox" id="id_email" /> '
1409+ '<label for="id_email">Email address: '
1410+ 'mark@example.com</label></li>')
1411+ # checkbox checked and enabled for optional fields and label is plain
1412+ language_html = ('<li><input checked="checked" type="checkbox" '
1413+ 'name="language" value="en" id="id_language" /> '
1414+ '<label for="id_language">Preferred language: en'
1415+ '</label></li>')
1416+ # team data is enabled and checked, the label is plain
1417+ team_html_1 = ('<li><input checked="checked" type="checkbox" '
1418+ 'name="ubuntu-team" value="ubuntu-team" id="id_'
1419+ 'ubuntu-team" /> <label for="id_ubuntu-team">'
1420+ 'ubuntu-team</label></li>')
1421+ team_html_2 = ('<li><input checked="checked" type="checkbox" '
1422+ 'name="isd-team" value="isd-team" id="id_isd-team" /> '
1423+ '<label for="id_isd-team">isd-team</label></li>')
1424+
1425+ self.assertContains(response, username_html)
1426+ self.assertContains(response, email_html)
1427+ self.assertContains(response, language_html)
1428+ self.assertContains(response, team_html_1)
1429+ self.assertContains(response, team_html_2)
1430+
1431+ def test_state_of_checkboxes_and_data_formats_untrusted(self):
1432+ self.client.login(username='mark@example.com',
1433+ password='test')
1434+
1435+ # create a trusted rpconfig
1436+ rpconfig = OpenIDRPConfig(trust_root='http://untrusted/',
1437+ can_query_any_team=True,
1438+ description="Some description",
1439+ )
1440+ rpconfig.save()
1441+ param_overrides = {
1442+ 'openid.sreg.required': 'nickname,email,fullname',
1443+ 'openid.sreg.optional': 'language',
1444+ 'openid.lp.query_membership': 'ubuntu-team',
1445+ }
1446+ self._prepare_openid_token(param_overrides=param_overrides)
1447+ response = self.client.get('/%s/+decide' % self.token)
1448+ # checkbox checked and *enabled* for required fields and label is bold
1449+ username_html = ('<li><input checked="checked" name="nickname" '
1450+ 'value="mark" class="required" type="checkbox" '
1451+ 'id="id_nickname" /> <label for="id_nickname">'
1452+ 'Username: mark</label></li>')
1453+ email_html = ('<li><input checked="checked" name="email" '
1454+ 'value="mark@example.com" class="required" type='
1455+ '"checkbox" id="id_email" /> <label for="id_email">'
1456+ 'Email address: mark@example.com</label></li>')
1457+ # checkbox *not checked* and enabled for optional fields & label's plain
1458+ language_html = ('<li><input type="checkbox" name="language" value="'
1459+ 'en" id="id_language" /> <label for="id_language">'
1460+ 'Preferred language: en</label></li>')
1461+ # team data is enabled and *not checked*, the label is plain
1462+ team_html = ('<li><input type="checkbox" name="ubuntu-team" '
1463+ 'value="ubuntu-team" id="id_ubuntu-team" /> Team '
1464+ 'membership: <label for="id_ubuntu-team">ubuntu-team'
1465+ '</label></li>')
1466+
1467+ self.assertContains(response, username_html)
1468+ self.assertContains(response, email_html)
1469+ self.assertContains(response, language_html)
1470+ self.assertContains(response, team_html)
1471+
1472+
1473 class PreAuthorizeTestCase(AuthenticatedTestCase):
1474 def setUp(self):
1475 super(PreAuthorizeTestCase, self).setUp(disableCSRF=True)
1476@@ -666,7 +761,7 @@
1477 old_fromOpenIDRequest = pape.Request.fromOpenIDRequest
1478 pape.Request.fromOpenIDRequest = mock_fromOpenIDRequest
1479
1480- r = server._openid_is_authorized(self.request, self.orequest)
1481+ r = server._openid_is_authorized(self.request, self.orequest)
1482 self.assertFalse(r)
1483
1484 pape.Request.fromOpenIDRequest = old_fromOpenIDRequest
1485@@ -832,66 +927,6 @@
1486 self.request = DummyRequest()
1487 self.request.user = self.account
1488
1489- def test_sreg_fields_no_preferredemail(self):
1490- for email in self.account.emailaddress_set.all():
1491- email.status = EmailStatus.NEW
1492- email.save()
1493-
1494- expected = []
1495- result = server._sreg_fields(self.request, self.sreg_request,
1496- self.rpconfig)
1497- self.assertEqual(expected, result)
1498-
1499- def test_sreg_fields_timezone(self):
1500- self.sreg_request.optional = ['timezone']
1501- expected = [('timezone', 'UTC')]
1502- result = server._sreg_fields(self.request, self.sreg_request,
1503- self.rpconfig)
1504- self.assertEqual(result, expected)
1505-
1506- def test_sreg_fields_language(self):
1507- self.sreg_request.optional = ['language']
1508- expected = [('language', 'en')]
1509- result = server._sreg_fields(self.request, self.sreg_request,
1510- self.rpconfig)
1511- self.assertEqual(result, expected)
1512-
1513-class TestGetTeamMemberships(BasicAccountTestCase):
1514-
1515- fixtures = ["test"]
1516-
1517- def setUp(self):
1518- self.account = Account.objects.get(pk=1)
1519-
1520- def test_get_team_memberships_on_personless_account(self):
1521- account = Account.objects.create_account('test', 'x@example.com', 'password')
1522- memberships = server.get_team_memberships(
1523- ["ubuntu-team"], account, True)
1524- self.assertEquals(memberships, [])
1525-
1526- def test_get_team_memberships_when_team_is_visible(self):
1527- memberships = server.get_team_memberships(
1528- ["ubuntu-team"], self.account, True)
1529- self.assertEquals(memberships, ["ubuntu-team"])
1530-
1531- def test_get_team_membership_when_team_is_not_visible(self):
1532- self.set_team_to_private_membership('ubuntu-team')
1533-
1534- memberships = server.get_team_memberships(
1535- ['ubuntu-team', "myteam"], self.account, True)
1536- self.assertEquals(memberships, [])
1537-
1538- def test_team_is_private_but_you_can_see_them(self):
1539- self.set_team_to_private_membership('ubuntu-team')
1540- memberships = server.get_team_memberships(
1541- ['ubuntu-team', "myteam"], self.account, False)
1542- self.assertEquals(memberships, ['ubuntu-team'])
1543-
1544- def set_team_to_private_membership(self, team_name):
1545- team = Person.objects.get(name=team_name)
1546- team.visibility = PERSON_VISIBILITY_PRIVATE_MEMBERSHIP
1547- team.save()
1548-
1549
1550 class MarkupTestCase(SQLCachedTestCase):
1551
1552@@ -908,3 +943,69 @@
1553
1554 r = self.client.get('/%s/+decide' % token)
1555 self.assertContains(r, '<em>not</em>')
1556+
1557+
1558+class ApprovedDataTest(OpenIDProviderTestCase):
1559+ fixtures = ['test']
1560+
1561+ def _get_openid_request(self, with_sreg=True, with_teams=True):
1562+ request = {
1563+ 'openid.mode': 'checkid_setup',
1564+ 'openid.trust_root': 'http://localhost/',
1565+ 'openid.return_to': 'http://localhost/',
1566+ 'openid.identity': IDENTIFIER_SELECT}
1567+ if with_sreg:
1568+ request['openid.sreg.required'] = 'email,fullname'
1569+ if with_teams:
1570+ request['openid.lp.query_membership'] = 'ubuntu-team'
1571+ openid_server = server._get_openid_server()
1572+ return openid_server.decodeRequest(request)
1573+
1574+ def _get_request_with_post_args(self, args={}):
1575+ request = HttpRequest()
1576+ request.user = Account.objects.get(pk=1)
1577+ request.POST = args
1578+ request.META = {'REQUEST_METHOD': 'POST'}
1579+ return request
1580+
1581+ def setUp(self):
1582+ # Ensure that we're restricting RPs for these tests
1583+ self.old_restrict = getattr(settings, 'SSO_RESTRICT_RP', True)
1584+ settings.SSO_RESTRICT_RP = False
1585+
1586+ def tearDown(self):
1587+ settings.SSO_RESTRICT_RP = self.old_restrict
1588+
1589+ def test_approved_data_returns_none_for_no_request(self):
1590+ result = server._get_approved_data(HttpRequest(), None)
1591+ self.assertEqual(result, None)
1592+
1593+ def test_approved_data_for_sreg_only(self):
1594+ post_args = {'email': 'email', 'ubuntu-team': 'ubuntu-team'}
1595+ result = server._get_approved_data(
1596+ self._get_request_with_post_args(post_args),
1597+ self._get_openid_request(True, False))
1598+ self.assertEqual(sorted(result['sreg']['requested']),
1599+ ['email', 'fullname'])
1600+ self.assertEqual(result['sreg']['approved'], ['email'])
1601+ self.assertFalse(result.has_key('teams'))
1602+
1603+ def test_approved_data_for_teams_only(self):
1604+ post_args = {'ubuntu-team': 'ubuntu-team'}
1605+ result = server._get_approved_data(
1606+ self._get_request_with_post_args(post_args),
1607+ self._get_openid_request(False, True))
1608+ self.assertEqual(result['teams']['requested'], ['ubuntu-team'])
1609+ self.assertEqual(result['teams']['approved'], ['ubuntu-team'])
1610+ self.assertFalse(result.has_key('sreg'))
1611+
1612+ def test_approved_data_for_sreg_and_teams(self):
1613+ post_args = {'email': 'email', 'ubuntu-team': 'ubuntu-team'}
1614+ result = server._get_approved_data(
1615+ self._get_request_with_post_args(post_args),
1616+ self._get_openid_request())
1617+ self.assertEqual(sorted(result['sreg']['requested']),
1618+ ['email', 'fullname'])
1619+ self.assertEqual(result['sreg']['approved'], ['email'])
1620+ self.assertEqual(result['teams']['requested'], ['ubuntu-team'])
1621+ self.assertEqual(result['teams']['approved'], ['ubuntu-team'])
1622
1623=== modified file 'identityprovider/tests/utils.py'
1624--- identityprovider/tests/utils.py 2011-06-17 14:22:43 +0000
1625+++ identityprovider/tests/utils.py 2011-07-05 15:46:33 +0000
1626@@ -302,3 +302,54 @@
1627 resp.code = 200
1628 resp.msg = 'OK'
1629 return resp
1630+
1631+
1632+def add_user_to_team(account, teamname):
1633+ """ Note: will not work on the staging or production databases as those are
1634+ replicated from LP and read-only.
1635+ """
1636+ from datetime import date
1637+ from identityprovider.models.person import Person
1638+ from identityprovider.models.account import (Account, LPOpenIdIdentifier)
1639+ from identityprovider.models.team import (TeamMembership, TeamParticipation)
1640+
1641+ memberships = TeamMembership.get_team_memberships_for_user([teamname],
1642+ account, True)
1643+ if len(memberships) > 0:
1644+ return
1645+
1646+ p = account.person
1647+ if p is None:
1648+ p = Person.objects.create(lp_account=account.id,
1649+ displayname=account.displayname,
1650+ name=get_unique_username())
1651+
1652+ try:
1653+ oi = LPOpenIdIdentifier.objects.get(lp_account=account.id)
1654+ except LPOpenIdIdentifier.DoesNotExist:
1655+ oi = LPOpenIdIdentifier.objects.create(
1656+ identifier=account.openid_identifier, lp_account=account.id)
1657+
1658+ try:
1659+ t = Person.objects.get(name=teamname)
1660+ except Person.DoesNotExist:
1661+ t = Person.objects.create(displayname="Team %s" % teamname, teamowner=p,
1662+ name=teamname)
1663+
1664+ now = date.today()
1665+ exp = date(day=now.day, month=now.month, year=now.year+1)
1666+ TeamMembership.objects.create(person=p, date_expires=exp, team=t, status=1)
1667+ TeamParticipation.objects.create(team=t, person=p)
1668+
1669+def get_unique_username():
1670+ from identityprovider.models.person import Person
1671+ i = 0
1672+ while True:
1673+ try:
1674+ username = "user%d" % i
1675+ i += 1
1676+ Person.objects.get(name=username)
1677+ except Person.DoesNotExist:
1678+ break
1679+ return username
1680+
1681
1682=== modified file 'identityprovider/views/server.py'
1683--- identityprovider/views/server.py 2011-05-31 20:29:08 +0000
1684+++ identityprovider/views/server.py 2011-07-05 15:46:33 +0000
1685@@ -6,13 +6,26 @@
1686 import urllib
1687 import urlparse
1688
1689-from datetime import datetime, timedelta
1690+from datetime import (
1691+ datetime,
1692+ timedelta,
1693+)
1694
1695 from openid.extensions import pape
1696-from openid.extensions.sreg import SRegRequest, SRegResponse
1697-from openid.message import IDENTIFIER_SELECT, registerNamespaceAlias
1698-from openid.server.server import (CheckIDRequest, ENCODE_URL, ProtocolError,
1699- Server)
1700+from openid.extensions.sreg import (
1701+ SRegRequest,
1702+ SRegResponse,
1703+)
1704+from openid.message import (
1705+ IDENTIFIER_SELECT,
1706+ registerNamespaceAlias,
1707+)
1708+from openid.server.server import (
1709+ CheckIDRequest,
1710+ ENCODE_URL,
1711+ ProtocolError,
1712+ Server,
1713+)
1714 from openid.server.trustroot import TrustRoot
1715 from openid.urinorm import urinorm
1716 from openid.yadis.constants import YADIS_HEADER_NAME
1717@@ -21,30 +34,47 @@
1718
1719 from django.conf import settings
1720 from django.contrib.auth.decorators import login_required
1721-from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
1722- HttpResponseRedirect)
1723-from django.template import RequestContext, loader
1724-from django.shortcuts import get_object_or_404, render_to_response
1725+from django.http import (
1726+ Http404,
1727+ HttpResponse,
1728+ HttpResponseBadRequest,
1729+ HttpResponseRedirect,
1730+)
1731+from django.template import (
1732+ RequestContext,
1733+ loader,
1734+)
1735+from django.shortcuts import (
1736+ get_object_or_404,
1737+ render_to_response,
1738+)
1739 from django.utils.decorators import decorator_from_middleware
1740 from django.utils import translation
1741
1742 import identityprovider.signed as signed
1743
1744-from identityprovider.const import (
1745- LAUNCHPAD_TEAMS_NS,
1746- PERSON_VISIBILITY_PUBLIC,
1747- SREG_DATA_FIELDS_ORDER,
1748- SREG_LABELS,
1749+from identityprovider.const import LAUNCHPAD_TEAMS_NS
1750+from identityprovider.forms import (
1751+ PreAuthorizeForm,
1752+ SRegRequestForm,
1753+ TeamsRequestForm,
1754 )
1755-from identityprovider.forms import PreAuthorizeForm
1756+
1757 from identityprovider.middleware.xrds import XRDSMiddleware
1758-from identityprovider.models import (Account, DjangoOpenIDStore,
1759- OpenIDAuthorization, OpenIDRPSummary,
1760- Person)
1761+from identityprovider.models import (
1762+ Account,
1763+ DjangoOpenIDStore,
1764+ OpenIDAuthorization,
1765+ OpenIDRPSummary,
1766+ TeamMembership,
1767+)
1768 from identityprovider.models.authtoken import create_token
1769 from identityprovider.teams import TeamsRequest
1770 from identityprovider.utils import is_django_13
1771-from identityprovider.views import ui, utils
1772+from identityprovider.views import (
1773+ ui,
1774+ utils,
1775+)
1776 from identityprovider.views.i18n import set_language_info
1777
1778 if not is_django_13():
1779@@ -56,13 +86,14 @@
1780 registerNamespaceAlias(LAUNCHPAD_TEAMS_NS, 'lp')
1781 logger = logging.getLogger('sso')
1782
1783+
1784 @csrf_exempt
1785 @accept_xrds
1786 def openid_provider(request, lang='en'):
1787 if lang not in settings.SUPPORTED_LANGUAGES:
1788 # Next we check for a primary language.
1789 if lang is not None:
1790- lang = lang.split('_')[0] # e.g. de_CH becomes de.
1791+ lang = lang.split('_')[0] # e.g. de_CH becomes de.
1792 if lang not in settings.SUPPORTED_LANGUAGES:
1793 lang = translation.get_language_from_request(request)
1794 translation.activate(lang)
1795@@ -77,6 +108,7 @@
1796 set_language_info(request, response, lang)
1797 return response
1798
1799+
1800 def _process_openid_request(request, orequest, openid_server):
1801 if not orequest:
1802 context = RequestContext(request)
1803@@ -111,14 +143,15 @@
1804 if orequest.immediate:
1805 rp_config = utils.get_rpconfig(orequest.trust_root)
1806 auto_authorized = _is_auto_authorized_rp(rp_config)
1807- if auto_authorized and request.user.is_authenticated() and _is_identity_owner(request.user, orequest):
1808+ if (auto_authorized and request.user.is_authenticated() and
1809+ _is_identity_owner(request.user, orequest)):
1810 if orequest.idSelect():
1811 oresponse = orequest.answer(True,
1812 identity=request.user.openid_identity_url)
1813 else:
1814 oresponse = orequest.answer(True)
1815 _add_sreg(request, orequest, oresponse)
1816- _check_team_membership(request.user, orequest, oresponse)
1817+ _check_team_membership(request, orequest, oresponse)
1818 response = _django_response(request, oresponse, True)
1819 else:
1820 oresponse = orequest.answer(False)
1821@@ -138,7 +171,7 @@
1822 else:
1823 oresponse = orequest.answer(True)
1824 _add_sreg(request, orequest, oresponse)
1825- _check_team_membership(request.user, orequest, oresponse)
1826+ _check_team_membership(request, orequest, oresponse)
1827 response = _django_response(request, oresponse, True)
1828 elif (request.user.is_authenticated() and not
1829 _is_identity_owner(request.user, orequest)):
1830@@ -196,15 +229,21 @@
1831 return _process_decide(request, orequest, True)
1832 else:
1833 sreg_request = SRegRequest.fromOpenIDRequest(orequest)
1834- sreg_data = _sreg_fields(request, sreg_request, rpconfig)
1835- sreg_data = _humanize_sreg_labels(sreg_data)
1836- teams_data = _teams_data(request.user, orequest, rpconfig)
1837+ teams_request = TeamsRequest.fromOpenIDRequest(orequest)
1838+ try:
1839+ summary = OpenIDRPSummary.objects.get(
1840+ account=request.user, trust_root=orequest.trust_root)
1841+ approved_data = summary.get_approved_data()
1842+ except OpenIDRPSummary.DoesNotExist:
1843+ approved_data = {}
1844 context = RequestContext(request, {
1845 'account': request.user,
1846 'trust_root': orequest.trust_root,
1847 'rpconfig': rpconfig,
1848- 'sreg_data': sreg_data,
1849- 'teams_data': teams_data,
1850+ 'sreg_form': SRegRequestForm(request, sreg_request, rpconfig,
1851+ approved_data=approved_data.get('sreg')),
1852+ 'teams_form': TeamsRequestForm(request, teams_request, rpconfig,
1853+ approved_data=approved_data.get('teams')),
1854 'token': token,
1855 'message': request.session.get('message', None),
1856 'message_style': 'informational',
1857@@ -422,7 +461,7 @@
1858 return user.last_login <= cutoff
1859
1860
1861-def _django_response(request, oresponse, auth_success=False):
1862+def _django_response(request, oresponse, auth_success=False, orequest=None):
1863 """ Convert an OpenID response into a Django HttpResponse """
1864 webresponse = _get_openid_server().encodeResponse(oresponse)
1865 response = HttpResponse(webresponse.body, mimetype="text/plain")
1866@@ -433,54 +472,44 @@
1867 logger.debug("response_body = " + webresponse.body)
1868 if auth_success and isinstance(oresponse.request, CheckIDRequest):
1869 logger.debug("oresponse.fields = " + str(oresponse.fields))
1870- OpenIDRPSummary.objects.record(request.user,
1871- oresponse.request.trust_root)
1872+ approved_data = _get_approved_data(request, orequest)
1873+ OpenIDRPSummary.objects.record(
1874+ request.user,
1875+ oresponse.request.trust_root,
1876+ None,
1877+ approved_data)
1878 return response
1879
1880
1881-def _sreg_fields(request, sreg_request, rpconfig):
1882- account = request.user
1883- field_names = set(sreg_request.required + sreg_request.optional)
1884- if rpconfig is None:
1885- # The nickname field is permitted by default.
1886- field_names.intersection_update(['nickname'])
1887- elif rpconfig.allowed_sreg is not None:
1888- field_names.intersection_update(rpconfig.allowed_sreg.split(','))
1889- sreg_field_names = [name for name in SREG_DATA_FIELDS_ORDER
1890- if name in field_names]
1891- # Collect registration values
1892- values = {}
1893- values['fullname'] = account.displayname
1894- if account.preferredemail is not None:
1895- values['email'] = account.preferredemail.email
1896- if account.person is not None:
1897- values['nickname'] = account.person.name
1898- if account.person.time_zone is not None:
1899- values['timezone'] = account.person.time_zone
1900- if account.preferredlanguage is not None:
1901- values['language'] = account.preferredlanguage
1902- else:
1903- values['language'] = translation.get_language_from_request(request)
1904- logger.debug("values (sreg_fields) = " + str(values))
1905- return [(field, values[field])
1906- for field in sreg_field_names if field in values]
1907-
1908-
1909-def _humanize_sreg_labels(sreg_data):
1910- new_data = []
1911- for k, v in sreg_data:
1912- k = SREG_LABELS.get(k, k)
1913- new_data.append((k, v))
1914- return new_data
1915-
1916-
1917-def _teams_data(account, orequest, rpconfig):
1918- teams_request = TeamsRequest.fromOpenIDRequest(orequest)
1919- restrict_teams = True
1920- if rpconfig is not None:
1921- restrict_teams = not rpconfig.can_query_any_team
1922- return get_team_memberships(teams_request.allRequestedTeams(),
1923- account, restrict_teams)
1924+def _get_approved_data(request, orequest):
1925+ """Given an HTTP request and an OpenID request, return a nested dict of
1926+ values requested in the request and approved by the user.
1927+ """
1928+ if not orequest:
1929+ return None
1930+
1931+ approved_data = {}
1932+
1933+ sreg_request = SRegRequest.fromOpenIDRequest(orequest)
1934+ rpconfig = utils.get_rpconfig(orequest.trust_root)
1935+ sreg_form = SRegRequestForm(request, sreg_request, rpconfig)
1936+ if len(sreg_form.data.keys()) > 0:
1937+ approved_data['sreg'] = {
1938+ 'requested': sreg_form.data.keys(),
1939+ 'approved': sreg_form.data_approved_by_user.keys()}
1940+
1941+ args = orequest.message.getArgs(LAUNCHPAD_TEAMS_NS)
1942+ team_names = args.get('query_membership')
1943+ if team_names:
1944+ team_names = team_names.split(',')
1945+ teams_form = TeamsRequestForm(request,
1946+ TeamsRequest.fromOpenIDRequest(orequest),
1947+ utils.get_rpconfig(orequest.trust_root))
1948+ approved_data['teams'] = {
1949+ 'requested': team_names,
1950+ 'approved': teams_form.teams_approved_by_user}
1951+
1952+ return approved_data
1953
1954
1955 def _is_identity_owner(user, openid_request):
1956@@ -495,11 +524,11 @@
1957 def _add_sreg(request, openid_request, openid_response):
1958 # Add sreg result data
1959 sreg_request = SRegRequest.fromOpenIDRequest(openid_request)
1960- sreg_fields = _sreg_fields(request, sreg_request,
1961- utils.get_rpconfig(openid_request.trust_root))
1962- if sreg_fields:
1963+ rpconfig = utils.get_rpconfig(openid_request.trust_root)
1964+ form = SRegRequestForm(request, sreg_request, rpconfig)
1965+ if form.data_approved_by_user:
1966 sreg_response = SRegResponse.extractResponse(
1967- sreg_request, dict(sreg_fields))
1968+ sreg_request, dict(form.data_approved_by_user))
1969 openid_response.addExtension(sreg_response)
1970
1971
1972@@ -522,8 +551,8 @@
1973 datetime.now(),
1974 request.session.session_key)
1975 _add_sreg(request, orequest, oresponse)
1976- _check_team_membership(request.user, orequest, oresponse)
1977- r = _django_response(request, oresponse, decision)
1978+ _check_team_membership(request, orequest, oresponse, False)
1979+ r = _django_response(request, oresponse, decision, orequest)
1980 if r.content:
1981 # Only user-visible content is generated from this view. Wrap
1982 # it up and set the Content-Type.
1983@@ -543,47 +572,43 @@
1984 return openid_server
1985
1986
1987-def _check_team_membership(account, openid_request, openid_response):
1988+def _check_team_membership(request, orequest, oresponse, immediate=True):
1989 """Perform team membership checks.
1990
1991 If any team membership checks have been requested as part of
1992 the OpenID request, annotate the response with the list of
1993 teams the user is actually a member of.
1994 """
1995- assert account is not None, (
1996+ assert request.user is not None, (
1997 'Must be logged in to calculate team membership')
1998- if account.person is None:
1999+ if request.user.person is None:
2000 return
2001- args = openid_request.message.getArgs(LAUNCHPAD_TEAMS_NS)
2002+ args = orequest.message.getArgs(LAUNCHPAD_TEAMS_NS)
2003 team_names = args.get('query_membership')
2004 if not team_names:
2005 return
2006 team_names = team_names.split(',')
2007- only_public_teams = (
2008- utils.get_rpconfig(openid_request.trust_root) is None or not
2009- utils.get_rpconfig(openid_request.trust_root).can_query_any_team)
2010- memberships = get_team_memberships(
2011- team_names, account, only_public_teams)
2012- openid_response.fields.namespaces.addAlias(LAUNCHPAD_TEAMS_NS, 'lp')
2013- openid_response.fields.setArg(
2014- LAUNCHPAD_TEAMS_NS, 'is_member', ','.join(memberships))
2015-
2016-
2017-def get_team_memberships(team_names, account, only_public_teams):
2018- memberships = []
2019- for team_name in team_names:
2020+ if immediate:
2021 try:
2022- team = Person.objects.get(name=team_name)
2023- except Person.DoesNotExist:
2024- team = None
2025- if team is None or not team.is_team():
2026- continue
2027- # Control access to private teams
2028- if (team.visibility != PERSON_VISIBILITY_PUBLIC and only_public_teams):
2029- continue
2030- if account.person_in_team(team):
2031- memberships.append(team_name)
2032- return memberships
2033+ summary = OpenIDRPSummary.objects.get(
2034+ account=request.user, trust_root=orequest.trust_root)
2035+ approved_data = summary.get_approved_data()
2036+ except OpenIDRPSummary.DoesNotExist:
2037+ approved_data = {}
2038+ if 'teams' in approved_data:
2039+ teams = ','.join(approved_data['teams'].get('approved', []))
2040+ else:
2041+ rpconfig = utils.get_rpconfig(orequest.trust_root)
2042+ teams = ','.join(TeamMembership.get_team_memberships_for_user(
2043+ team_names, request.user,
2044+ rpconfig and rpconfig.can_query_any_team))
2045+ else:
2046+ form = TeamsRequestForm(request,
2047+ TeamsRequest.fromOpenIDRequest(orequest),
2048+ utils.get_rpconfig(orequest.trust_root))
2049+ teams = ','.join(form.teams_approved_by_user)
2050+ oresponse.fields.namespaces.addAlias(LAUNCHPAD_TEAMS_NS, 'lp')
2051+ oresponse.fields.setArg(LAUNCHPAD_TEAMS_NS, 'is_member', teams)
2052
2053
2054 def untrusted(request, token):
2055
2056=== modified file 'identityprovider/views/ui.py'
2057--- identityprovider/views/ui.py 2011-05-31 15:44:44 +0000
2058+++ identityprovider/views/ui.py 2011-07-05 15:46:33 +0000
2059@@ -206,6 +206,7 @@
2060 })
2061 return render_to_response('enter_token.html', context)
2062
2063+
2064 def _redirect_to_enter_token(confirmation_code=None, email=None):
2065 params = {}
2066 if confirmation_code is not None:
2067@@ -214,6 +215,7 @@
2068 params['email'] = email
2069 return HttpResponseRedirect('/+enter_token?%s' % urlencode(params))
2070
2071+
2072 def claim_token(request, authtoken):
2073 email = request.GET.get('email') or request.session.get('token_email')
2074 if email is None:
2075@@ -221,6 +223,7 @@
2076 token_type = get_type_of_token(authtoken)
2077 return _handle_confirmation(token_type, authtoken, email, request)
2078
2079+
2080 # As soon as _finish_account_creation doesn't need `request`, neither
2081 # does this.
2082 def _handle_confirmation(confirmation_type, confirmation_code, email, request,
2083@@ -258,7 +261,8 @@
2084 return HttpResponseRedirect(redirection_url)
2085
2086 view = old_confirm_account
2087- if 'token' in args: del args['token']
2088+ if 'token' in args:
2089+ del args['token']
2090
2091 elif confirmation_type == LoginTokenType.VALIDATEEMAIL:
2092 view = confirm_email
2093@@ -572,9 +576,11 @@
2094 })
2095 return render_to_response('registration/bad_token.html', context)
2096
2097+
2098 def logout_to_confirm(request):
2099 return render_to_response('registration/logout_to_confirm.html')
2100
2101+
2102 def deactivated(request):
2103 context = RequestContext(request, {
2104 'message': request.session.pop('message', None),
2105
2106=== modified file 'payload/__init__.py'
2107--- payload/__init__.py 2011-06-23 19:51:28 +0000
2108+++ payload/__init__.py 2011-07-05 15:46:33 +0000
2109@@ -210,4 +210,7 @@
2110 print 'Nothing to do; translations are up to date.'
2111
2112
2113-
2114+def manage(command, *args):
2115+ """Run manage.py command"""
2116+ virtualenv("python django_project/manage.py {0} {1}".format(
2117+ command, " ".join(args)), capture=False,)