Merge lp:~jamestait/canonical-identity-provider/add-ax-fetch into lp:canonical-identity-provider/release
- add-ax-fetch
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Ricardo Kirkner |
Approved revision: | no longer in the source branch. |
Merged at revision: | 764 |
Proposed branch: | lp:~jamestait/canonical-identity-provider/add-ax-fetch |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
1492 lines (+1023/-109) 12 files modified
identityprovider/admin.py (+9/-1) identityprovider/const.py (+26/-0) identityprovider/forms.py (+152/-25) identityprovider/migrations/0007_add_allowed_ax.py (+212/-0) identityprovider/models/openidmodels.py (+1/-0) identityprovider/templates/server/decide.html (+15/-7) identityprovider/templates/server/openidapplication-xrds.xml (+1/-0) identityprovider/tests/openid_server/test_discovery.py (+1/-0) identityprovider/tests/test_admin.py (+73/-8) identityprovider/tests/test_forms.py (+199/-2) identityprovider/tests/test_views_server.py (+296/-61) identityprovider/views/server.py (+38/-5) |
To merge this branch: | bzr merge lp:~jamestait/canonical-identity-provider/add-ax-fetch |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ricardo Kirkner (community) | Approve | ||
Review via email: mp+155200@code.launchpad.net |
Commit message
Added handling of OpenID Attribute Exchange.
Description of the change
Add support for OpenID Attribute Exchange 1.0 Fetch Requests
=======
This is a fairly large change to add support for OpenID Attribute Exchange 1.0 Fetch Requests [0]. AX is like Simple Registration (SReg, [1]) on steroids. It allows a Relying Party to request certain attributes about the user from the OpenID Provider at login time. Where SReg has a handful of fixed, predefined properties that can be requested, defined by short keywords, AX is extensible, with attributes being identified by a URI, and having a symbolic alias attached.
Taking advantage of this extensibility, as well as returning the same properties as SReg, by way of an SReg compatibility schema for AX, we also return the email verification status. This isn't defined in a spec anywhere, so I've assigned this attribute our own URI in the ns.launchpad.net namespace.
Points to Note
--------------
0) I've added the allowable AX properties for a given RP as a separate coluumn in the RPConfig (allowed_ax) from the allowable SReg properties (allowed_sreg). I initially did this because I was going to store the URIs in the RPConfig, but I later switched to storing the alias for the properties that we can provide. Maybe these could be amalgamated into a single column and the forms could DTRT to avoid people futzing with the SReg requests and getting attributes they shouldn't.
1) I'm not sure about AX_DATA_FIELDS as a NamespaceMap in identityprovide
2) I'm a little unhappy with the complexity in places - lots of mapping using AX_DATA_FIELDS which makes the code somewhat ungainly. I switched from using the URIs to avoid form fields in the AXFetchRequestForm on the decide page with URIs for names, but maybe there's a middle ground where we can use symbolic names just for rendering the form and use the URIs everywhere else.
3) Maybe we don't want to use ns.launchpad.net for the email_verified attribute.
4) Maybe the label for email_verified could be improved.
5) Currently we do nothing with AX Store Requests.
6) If a request has both SReg and AX requests, the SReg request will be ignored and return empty results. This isn't a problem for django-openid-auth, which starts with the SReg results but uses the AX results in preference; other libraries may vary.
James Tait (jamestait) wrote : | # |
> - missing commit message
Fixed. :)
> - instead of *_EMAIL_VERIFIED I'd use *_ACCOUNT_VERIFIED (as we should care
> about the status of the account, not the email; if the account is verified,
> the returned email is also verified).
This came about as a result of a misunderstanding on my part about the difference between a verified account and an account with a verified e-mail address. Happy to fix this.
> - l. 96-99: I'd extract this into a helper method for readability
> - l. 101-105: ditto
Yes, that would clean things up nicely.
> - l. 114-135: since these values (except email_verified) are the same for AX
> or SREG, why not have a common helper function to get them (DRY)?
Ditto.
> - l. 462-463: you can use {% elif ... %} here I believe?
Alas, not in Django 1.3. ;)
> - l. 516: why not get the entry again and confirm it has the fields you just
> posted?
Yes, that should work. I'll look into adding that extra verification, and maybe do the same for the SReg tests.
> - l. 531: this could match any checkbox in the form? why not rather add a
> specific test to verify email is listed as an ax field?
Indeed, this should be made more robust. I'll shore this up and again look to do the same for the SReg tests.
> - l. 613-614: you could also use assertIn, assertNotIn
Good catch!
> - l. 818-819,
> pyquery for asserting against the DOM
I'll look into this. I have to admit I was cringing when writing the tests, so I'm grateful for the suggestion of a better method.
> - l. 1166: can we call _get_openid_request with named params? it's hard to
> understand what each param means otherwise
Yes, that would be an improvement.
> - l. 1250: why not just do: if ax_form.data: ?
> - l. 1250-1253: or maybe add a helper to the form to get the values without
> having to know that data is a dict?
I prefer the latter approach. It had occurred to me to do something like this, but I wanted to get eyes on the code before I made any further changes.
Thanks for the comments!
Ricardo Kirkner (ricardokirkner) wrote : | # |
LGTM. Awesome stuff!
Preview Diff
1 | === modified file 'identityprovider/admin.py' |
2 | --- identityprovider/admin.py 2013-03-21 17:03:32 +0000 |
3 | +++ identityprovider/admin.py 2013-04-03 14:40:35 +0000 |
4 | @@ -6,7 +6,10 @@ |
5 | from django.core.urlresolvers import reverse |
6 | from django.template.defaultfilters import force_escape |
7 | |
8 | -from identityprovider.const import SREG_LABELS |
9 | +from identityprovider.const import ( |
10 | + AX_DATA_LABELS, |
11 | + SREG_LABELS, |
12 | +) |
13 | from identityprovider.fields import CommaSeparatedField |
14 | from identityprovider.models import ( |
15 | Account, |
16 | @@ -42,6 +45,11 @@ |
17 | displayname = forms.CharField(label="Display name") |
18 | trust_root = forms.URLField(verify_exists=False) |
19 | logo = forms.CharField(required=False) |
20 | + allowed_ax = CommaSeparatedField( |
21 | + choices=AX_DATA_LABELS.items(), |
22 | + required=False, |
23 | + widget=CommaSeparatedWidget, |
24 | + ) |
25 | allowed_sreg = CommaSeparatedField( |
26 | choices=SREG_LABELS.items(), |
27 | required=False, |
28 | |
29 | === modified file 'identityprovider/const.py' |
30 | --- identityprovider/const.py 2012-10-12 21:23:31 +0000 |
31 | +++ identityprovider/const.py 2013-04-03 14:40:35 +0000 |
32 | @@ -1,6 +1,8 @@ |
33 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
34 | # GNU Affero General Public License version 3 (see the file LICENSE). |
35 | |
36 | +from openid.message import NamespaceMap |
37 | + |
38 | LAUNCHPAD_TEAMS_NS = 'http://ns.launchpad.net/2007/openid-teams' |
39 | |
40 | PERSON_VISIBILITY_PUBLIC = 1 |
41 | @@ -20,3 +22,27 @@ |
42 | 'timezone': 'Time zone', |
43 | 'language': 'Preferred language', |
44 | } |
45 | + |
46 | +AX_URI_FULL_NAME = 'http://axschema.org/namePerson' |
47 | +AX_URI_NICKNAME = 'http://axschema.org/namePerson/friendly' |
48 | +AX_URI_EMAIL = 'http://axschema.org/contact/email' |
49 | +AX_URI_TIMEZONE = 'http://axschema.org/timezone' |
50 | +AX_URI_LANGUAGE = 'http://axschema.org/language/pref' |
51 | +AX_URI_ACCOUNT_VERIFIED = 'http://ns.login.ubuntu.com/2013/validation/account' |
52 | + |
53 | +AX_DATA_FIELDS = NamespaceMap() |
54 | +AX_DATA_FIELDS.addAlias(AX_URI_FULL_NAME, 'fullname') |
55 | +AX_DATA_FIELDS.addAlias(AX_URI_NICKNAME, 'nickname') |
56 | +AX_DATA_FIELDS.addAlias(AX_URI_EMAIL, 'email') |
57 | +AX_DATA_FIELDS.addAlias(AX_URI_TIMEZONE, 'timezone') |
58 | +AX_DATA_FIELDS.addAlias(AX_URI_LANGUAGE, 'language') |
59 | +AX_DATA_FIELDS.addAlias(AX_URI_ACCOUNT_VERIFIED, 'account_verified') |
60 | + |
61 | +AX_DATA_LABELS = { |
62 | + 'fullname': 'Full name', |
63 | + 'nickname': 'Username', |
64 | + 'email': 'Email address', |
65 | + 'timezone': 'Time zone', |
66 | + 'language': 'Preferred language', |
67 | + 'account_verified': 'Account verified', |
68 | +} |
69 | |
70 | === modified file 'identityprovider/forms.py' |
71 | --- identityprovider/forms.py 2013-03-27 14:30:56 +0000 |
72 | +++ identityprovider/forms.py 2013-04-03 14:40:35 +0000 |
73 | @@ -10,6 +10,8 @@ |
74 | from django.utils.translation import ugettext as _ |
75 | |
76 | from identityprovider.const import ( |
77 | + AX_DATA_FIELDS, |
78 | + AX_DATA_LABELS, |
79 | SREG_DATA_FIELDS_ORDER, |
80 | SREG_LABELS, |
81 | ) |
82 | @@ -340,6 +342,31 @@ |
83 | callback = forms.CharField(error_messages=default_errors) |
84 | |
85 | |
86 | +def _get_data_for_user(request, fields, with_verified=False): |
87 | + """Get the data to ask about in the form based on the user's |
88 | + account record. |
89 | + """ |
90 | + values = {} |
91 | + user = request.user |
92 | + values['fullname'] = user.displayname |
93 | + if user.preferredemail is not None: |
94 | + values['email'] = user.preferredemail.email |
95 | + if with_verified: |
96 | + values['account_verified'] = ( |
97 | + 'token_via_email' if user.is_verified else 'no') |
98 | + if user.person is not None: |
99 | + values['nickname'] = user.person.name |
100 | + if user.person.time_zone is not None: |
101 | + values['timezone'] = user.person.time_zone |
102 | + if user.preferredlanguage is not None: |
103 | + values['language'] = user.preferredlanguage |
104 | + else: |
105 | + values['language'] = translation.get_language_from_request(request) |
106 | + logger.debug("values (sreg_fields) = " + str(values)) |
107 | + |
108 | + return dict([(f, values[f]) for f in fields if f in values]) |
109 | + |
110 | + |
111 | class SRegRequestForm(Form): |
112 | """A form object for user control over OpenID sreg data. |
113 | """ |
114 | @@ -351,6 +378,11 @@ |
115 | return dict([(f, self.data[f]) for f in self.data |
116 | if self.field_approved(f)]) |
117 | |
118 | + @property |
119 | + def has_data(self): |
120 | + """Helper property to check if this form has any data.""" |
121 | + return len(self.data) > 0 |
122 | + |
123 | def __init__(self, request, sreg_request, rpconfig, approved_data=None): |
124 | self.request = request |
125 | self.request_method = request.META.get('REQUEST_METHOD') |
126 | @@ -368,32 +400,10 @@ |
127 | fields = set() |
128 | else: |
129 | fields = sreg_fields |
130 | - self.data = self._get_sreg_data_for_user(request.user, fields) |
131 | + self.data = _get_data_for_user(request, fields, with_verified=False) |
132 | |
133 | super(SRegRequestForm, self).__init__(self.data) |
134 | self._init_fields(self.data) |
135 | - self.should_display = len(self.data) > 0 |
136 | - |
137 | - def _get_sreg_data_for_user(self, user, sreg_fields): |
138 | - """Get the sreg data to ask about in the form based on the user's |
139 | - account record. |
140 | - """ |
141 | - values = {} |
142 | - values['fullname'] = user.displayname |
143 | - if user.preferredemail is not None: |
144 | - values['email'] = user.preferredemail.email |
145 | - if user.person is not None: |
146 | - values['nickname'] = user.person.name |
147 | - if user.person.time_zone is not None: |
148 | - values['timezone'] = user.person.time_zone |
149 | - if user.preferredlanguage is not None: |
150 | - values['language'] = user.preferredlanguage |
151 | - else: |
152 | - values['language'] = translation.get_language_from_request( |
153 | - self.request) |
154 | - logger.debug("values (sreg_fields) = " + str(values)) |
155 | - |
156 | - return dict([(f, values[f]) for f in sreg_fields if f in values]) |
157 | |
158 | def _init_fields(self, data): |
159 | """Initialises form fields for the user's sreg data. |
160 | @@ -444,6 +454,114 @@ |
161 | return field in approved |
162 | |
163 | |
164 | +class AXFetchRequestForm(Form): |
165 | + """A form object for user control over OpenID Attribute Exchange.""" |
166 | + |
167 | + def __init__(self, request, ax_request, rpconfig, approved_data=None): |
168 | + self.request = request |
169 | + self.request_method = request.META.get('REQUEST_METHOD') |
170 | + self.ax_request = ax_request |
171 | + self.rpconfig = rpconfig |
172 | + self.approved_data = approved_data |
173 | + # generate initial form data |
174 | + ax_fields = self._get_requested_field_aliases() |
175 | + if rpconfig is not None: |
176 | + ax_fields = self._filter_allowed_fields(ax_fields) |
177 | + self.data = _get_data_for_user(request, ax_fields, with_verified=True) |
178 | + |
179 | + super(AXFetchRequestForm, self).__init__(self.data) |
180 | + self._init_fields(self.data) |
181 | + |
182 | + def _get_requested_field_aliases(self): |
183 | + return [AX_DATA_FIELDS.getAlias(f) |
184 | + for f in AX_DATA_FIELDS.iterNamespaceURIs() |
185 | + if f in set(self.ax_request.requested_attributes.keys())] |
186 | + |
187 | + def _filter_allowed_fields(self, requested_fields): |
188 | + if self.rpconfig.allowed_ax: |
189 | + return set(requested_fields).intersection( |
190 | + set(self.rpconfig.allowed_ax.split(','))) |
191 | + return set() |
192 | + |
193 | + def _init_fields(self, data): |
194 | + """Initialises form fields for the user's ax data. |
195 | + """ |
196 | + for key, val in data.items(): |
197 | + label = "%s: %s" % (AX_DATA_LABELS.get(key, key), val) |
198 | + attrs = {} |
199 | + if (AX_DATA_FIELDS.getNamespaceURI(key) in |
200 | + self.ax_request.getRequiredAttrs()): |
201 | + attrs['class'] = 'required' |
202 | + if self.rpconfig is not None: |
203 | + attrs['disabled'] = 'disabled' |
204 | + self.fields[key] = fields.BooleanField( |
205 | + label=label, |
206 | + widget=forms.CheckboxInput(attrs=attrs, |
207 | + check_test=self.check_test), |
208 | + ) |
209 | + |
210 | + def check_test(self, value): |
211 | + """Determines if a checkbox should be pre-checked based on previously |
212 | + approved user data, openid request and relying party type. |
213 | + """ |
214 | + for k, v in self.data.items(): |
215 | + if value == v: |
216 | + value = k |
217 | + break |
218 | + |
219 | + if self.rpconfig: |
220 | + # Trusted site, check required fields |
221 | + if (AX_DATA_FIELDS.getNamespaceURI(value) in |
222 | + self.ax_request.getRequiredAttrs()): |
223 | + return True |
224 | + # If we have previous (dis)approval for this site, use it |
225 | + if self.approved_data: |
226 | + return (value in self.approved_data.get('requested', []) and |
227 | + value in self.approved_data.get('approved', [])) |
228 | + # Otherwise, default to True |
229 | + return (AX_DATA_FIELDS.getNamespaceURI(value) in |
230 | + self.ax_request.requested_attributes) |
231 | + else: |
232 | + # If we have previous (dis)approval for this site, use it |
233 | + if self.approved_data: |
234 | + return (value in self.approved_data.get('requested', []) and |
235 | + value in self.approved_data.get('approved', [])) |
236 | + # No previous (dis)approval, check required and leave the rest |
237 | + if (AX_DATA_FIELDS.getNamespaceURI(value) in |
238 | + self.ax_request.getRequiredAttrs()): |
239 | + return True |
240 | + # Otherwise default to False |
241 | + return False |
242 | + |
243 | + def field_approved(self, field): |
244 | + """Check if the field should be returned in the response based on user |
245 | + preferences and overridden for trusted relying parties. |
246 | + """ |
247 | + approved = set([AX_DATA_FIELDS.getNamespaceURI(f) |
248 | + for f in self.request.POST.keys()]) |
249 | + if self.rpconfig is not None: |
250 | + if self.rpconfig.auto_authorize: |
251 | + ax_fields = set(self.ax_request.requested_attributes.keys()) |
252 | + else: |
253 | + ax_fields = set(self.ax_request.getRequiredAttrs()) |
254 | + approved.update(ax_fields) |
255 | + return field in approved |
256 | + |
257 | + @property |
258 | + def data_approved_for_request(self): |
259 | + """Return the list of ax data approved for the request.""" |
260 | + if self.request_method == 'POST': |
261 | + return dict( |
262 | + [(f, self.data[f]) for f in self.data |
263 | + if self.field_approved(AX_DATA_FIELDS.getNamespaceURI(f))]) |
264 | + return {} |
265 | + |
266 | + @property |
267 | + def has_data(self): |
268 | + """Helper property to check if this form has any data.""" |
269 | + return len(self.data) > 0 |
270 | + |
271 | + |
272 | class TeamsRequestForm(Form): |
273 | """A form object for user control over OpenID teams data. |
274 | """ |
275 | @@ -471,7 +589,6 @@ |
276 | super(TeamsRequestForm, self).__init__(self.memberships) |
277 | |
278 | self._init_fields(self.memberships) |
279 | - self.should_display = len(self.memberships) > 0 |
280 | |
281 | def _get_teams_for_user(self, user, include_private=False): |
282 | """Get the list of teams to ask about in the form based on the user's |
283 | @@ -485,9 +602,14 @@ |
284 | def _init_fields(self, form_data): |
285 | """Initialises form fields for the user's team memberships. |
286 | """ |
287 | + if len(form_data) == 1: |
288 | + label_format = 'Team membership: %s' |
289 | + else: |
290 | + label_format = '%s' |
291 | for team in form_data: |
292 | + label = label_format % team |
293 | self.fields[team] = fields.BooleanField( |
294 | - label=team, widget=forms.CheckboxInput( |
295 | + label=label, widget=forms.CheckboxInput( |
296 | check_test=self.check_test)) |
297 | |
298 | def check_test(self, value): |
299 | @@ -500,6 +622,11 @@ |
300 | else: |
301 | return self.rpconfig is not None |
302 | |
303 | + @property |
304 | + def has_data(self): |
305 | + """Helper property to check if this form has any data.""" |
306 | + return len(self.memberships) > 0 |
307 | + |
308 | |
309 | class HOTPDeviceForm(Form): |
310 | name = fields.CharField() |
311 | |
312 | === added file 'identityprovider/migrations/0007_add_allowed_ax.py' |
313 | --- identityprovider/migrations/0007_add_allowed_ax.py 1970-01-01 00:00:00 +0000 |
314 | +++ identityprovider/migrations/0007_add_allowed_ax.py 2013-04-03 14:40:35 +0000 |
315 | @@ -0,0 +1,212 @@ |
316 | +# -*- coding: utf-8 -*- |
317 | +import datetime |
318 | +from south.db import db |
319 | +from south.v2 import SchemaMigration |
320 | +from django.db import models |
321 | + |
322 | + |
323 | +class Migration(SchemaMigration): |
324 | + |
325 | + def forwards(self, orm): |
326 | + # Adding field 'OpenIDRPConfig.allowed_ax' |
327 | + db.add_column('ssoopenidrpconfig', 'allowed_ax', |
328 | + self.gf('django.db.models.fields.TextField')(null=True, blank=True), |
329 | + keep_default=False) |
330 | + |
331 | + |
332 | + def backwards(self, orm): |
333 | + # Deleting field 'OpenIDRPConfig.allowed_ax' |
334 | + db.delete_column('ssoopenidrpconfig', 'allowed_ax') |
335 | + |
336 | + |
337 | + models = { |
338 | + 'identityprovider.account': { |
339 | + 'Meta': {'object_name': 'Account', 'db_table': "u'account'"}, |
340 | + 'creation_rationale': ('django.db.models.fields.IntegerField', [], {}), |
341 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), |
342 | + 'date_status_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), |
343 | + 'displayname': ('identityprovider.models.account.DisplaynameField', [], {}), |
344 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
345 | + 'old_openid_identifier': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), |
346 | + 'openid_identifier': ('django.db.models.fields.TextField', [], {'default': "u'XJm6fny'", 'unique': 'True'}), |
347 | + 'preferredlanguage': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
348 | + 'status': ('django.db.models.fields.IntegerField', [], {}), |
349 | + 'status_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
350 | + 'twofactor_attempts': ('django.db.models.fields.SmallIntegerField', [], {'default': '0', 'null': 'True'}), |
351 | + 'twofactor_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
352 | + 'warn_about_backup_device': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) |
353 | + }, |
354 | + 'identityprovider.accountpassword': { |
355 | + 'Meta': {'object_name': 'AccountPassword', 'db_table': "u'accountpassword'"}, |
356 | + 'account': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['identityprovider.Account']", 'unique': 'True', 'db_column': "'account'"}), |
357 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
358 | + 'password': ('identityprovider.models.account.PasswordField', [], {}) |
359 | + }, |
360 | + 'identityprovider.apiuser': { |
361 | + 'Meta': {'object_name': 'APIUser', 'db_table': "'api_user'"}, |
362 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
363 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
364 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}), |
365 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
366 | + 'username': ('django.db.models.fields.CharField', [], {'max_length': '256'}) |
367 | + }, |
368 | + 'identityprovider.authenticationdevice': { |
369 | + 'Meta': {'ordering': "('id',)", 'object_name': 'AuthenticationDevice'}, |
370 | + 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'devices'", 'to': "orm['identityprovider.Account']"}), |
371 | + 'counter': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
372 | + 'device_type': ('django.db.models.fields.TextField', [], {'null': 'True'}), |
373 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
374 | + 'key': ('django.db.models.fields.TextField', [], {}), |
375 | + 'name': ('django.db.models.fields.TextField', [], {}) |
376 | + }, |
377 | + 'identityprovider.authtoken': { |
378 | + 'Meta': {'object_name': 'AuthToken', 'db_table': "u'authtoken'"}, |
379 | + 'date_consumed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), |
380 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'db_index': 'True', 'blank': 'True'}), |
381 | + 'displayname': ('identityprovider.models.account.DisplaynameField', [], {'null': 'True', 'blank': 'True'}), |
382 | + 'email': ('django.db.models.fields.TextField', [], {'db_index': 'True'}), |
383 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
384 | + 'password': ('identityprovider.models.account.PasswordField', [], {'null': 'True', 'blank': 'True'}), |
385 | + 'redirection_url': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
386 | + 'requester': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'requester'", 'blank': 'True'}), |
387 | + 'requester_email': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
388 | + 'token': ('django.db.models.fields.TextField', [], {'unique': 'True'}), |
389 | + 'token_type': ('django.db.models.fields.IntegerField', [], {}) |
390 | + }, |
391 | + 'identityprovider.emailaddress': { |
392 | + 'Meta': {'object_name': 'EmailAddress', 'db_table': "u'emailaddress'"}, |
393 | + 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'account'", 'blank': 'True'}), |
394 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}), |
395 | + 'email': ('django.db.models.fields.TextField', [], {}), |
396 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
397 | + 'lp_person': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'person'", 'blank': 'True'}), |
398 | + 'status': ('django.db.models.fields.IntegerField', [], {}) |
399 | + }, |
400 | + 'identityprovider.invalidatedemailaddress': { |
401 | + 'Meta': {'object_name': 'InvalidatedEmailAddress', 'db_table': "u'invalidated_emailaddress'"}, |
402 | + 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'account'", 'blank': 'True'}), |
403 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}), |
404 | + 'email': ('django.db.models.fields.TextField', [], {}), |
405 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) |
406 | + }, |
407 | + 'identityprovider.lpopenididentifier': { |
408 | + 'Meta': {'object_name': 'LPOpenIdIdentifier', 'db_table': "u'lp_openididentifier'"}, |
409 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.date.today'}), |
410 | + 'identifier': ('django.db.models.fields.TextField', [], {'unique': 'True', 'primary_key': 'True'}), |
411 | + 'lp_account': ('django.db.models.fields.IntegerField', [], {'db_column': "'account'", 'db_index': 'True'}) |
412 | + }, |
413 | + 'identityprovider.openidassociation': { |
414 | + 'Meta': {'unique_together': "(('server_url', 'handle'),)", 'object_name': 'OpenIDAssociation', 'db_table': "u'openidassociation'"}, |
415 | + 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
416 | + 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), |
417 | + 'issued': ('django.db.models.fields.IntegerField', [], {}), |
418 | + 'lifetime': ('django.db.models.fields.IntegerField', [], {}), |
419 | + 'secret': ('django.db.models.fields.TextField', [], {}), |
420 | + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'}) |
421 | + }, |
422 | + 'identityprovider.openidauthorization': { |
423 | + 'Meta': {'object_name': 'OpenIDAuthorization', 'db_table': "u'openidauthorization'"}, |
424 | + 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}), |
425 | + 'client_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
426 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}), |
427 | + 'date_expires': ('django.db.models.fields.DateTimeField', [], {}), |
428 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
429 | + 'trust_root': ('django.db.models.fields.TextField', [], {}) |
430 | + }, |
431 | + 'identityprovider.openidnonce': { |
432 | + 'Meta': {'unique_together': "(('server_url', 'timestamp', 'salt'),)", 'object_name': 'OpenIDNonce', 'db_table': "'openidnonce'"}, |
433 | + 'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}), |
434 | + 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047', 'primary_key': 'True'}), |
435 | + 'timestamp': ('django.db.models.fields.IntegerField', [], {}) |
436 | + }, |
437 | + 'identityprovider.openidrpconfig': { |
438 | + 'Meta': {'object_name': 'OpenIDRPConfig', 'db_table': "'ssoopenidrpconfig'"}, |
439 | + 'allow_unverified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
440 | + 'allowed_ax': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
441 | + 'allowed_sreg': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
442 | + 'auto_authorize': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
443 | + 'can_query_any_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
444 | + 'creation_rationale': ('django.db.models.fields.IntegerField', [], {'default': '13'}), |
445 | + 'description': ('django.db.models.fields.TextField', [], {}), |
446 | + 'displayname': ('django.db.models.fields.TextField', [], {}), |
447 | + 'flag_twofactor': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), |
448 | + 'ga_snippet': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
449 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
450 | + 'logo': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
451 | + 'prefer_canonical_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
452 | + 'require_two_factor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
453 | + 'trust_root': ('django.db.models.fields.TextField', [], {'unique': 'True'}) |
454 | + }, |
455 | + 'identityprovider.openidrpsummary': { |
456 | + 'Meta': {'unique_together': "(('account', 'trust_root', 'openid_identifier'),)", 'object_name': 'OpenIDRPSummary', 'db_table': "u'openidrpsummary'"}, |
457 | + 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}), |
458 | + 'approved_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), |
459 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}), |
460 | + 'date_last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}), |
461 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
462 | + 'openid_identifier': ('django.db.models.fields.TextField', [], {'db_index': 'True'}), |
463 | + 'total_logins': ('django.db.models.fields.IntegerField', [], {'default': '1'}), |
464 | + 'trust_root': ('django.db.models.fields.TextField', [], {'db_index': 'True'}) |
465 | + }, |
466 | + 'identityprovider.person': { |
467 | + 'Meta': {'object_name': 'Person', 'db_table': "u'lp_person'"}, |
468 | + 'addressline1': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
469 | + 'addressline2': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
470 | + 'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
471 | + 'country': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'country'", 'blank': 'True'}), |
472 | + 'creation_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
473 | + 'creation_rationale': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
474 | + 'datecreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), |
475 | + 'defaultmembershipperiod': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
476 | + 'defaultrenewalperiod': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
477 | + 'displayname': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
478 | + 'fti': ('django.db.models.fields.TextField', [], {'null': 'True'}), |
479 | + 'hide_email_addresses': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), |
480 | + 'homepage_content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
481 | + 'icon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'icon'", 'blank': 'True'}), |
482 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
483 | + 'language': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'language'", 'blank': 'True'}), |
484 | + 'logo': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'logo'", 'blank': 'True'}), |
485 | + 'lp_account': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'null': 'True', 'db_column': "'account'"}), |
486 | + 'mail_resumption_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), |
487 | + 'mailing_list_auto_subscribe_policy': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}), |
488 | + 'mailing_list_receive_duplicates': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'}), |
489 | + 'merged': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'merged'", 'blank': 'True'}), |
490 | + 'mugshot': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'mugshot'", 'blank': 'True'}), |
491 | + 'name': ('django.db.models.fields.TextField', [], {'unique': 'True', 'null': 'True'}), |
492 | + 'organization': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
493 | + 'personal_standing': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True'}), |
494 | + 'personal_standing_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
495 | + 'phone': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
496 | + 'postcode': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
497 | + 'province': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
498 | + 'registrant': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'registrant'", 'blank': 'True'}), |
499 | + 'renewal_policy': ('django.db.models.fields.IntegerField', [], {'default': '10', 'null': 'True'}), |
500 | + 'subscriptionpolicy': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}), |
501 | + 'teamdescription': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
502 | + 'teamowner': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'teamowner'", 'blank': 'True'}), |
503 | + 'verbose_bugnotifications': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), |
504 | + 'visibility': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}) |
505 | + }, |
506 | + 'identityprovider.personlocation': { |
507 | + 'Meta': {'object_name': 'PersonLocation', 'db_table': "u'lp_personlocation'"}, |
508 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), |
509 | + 'date_last_modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), |
510 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
511 | + 'last_modified_by': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'last_modified_by'"}), |
512 | + 'latitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), |
513 | + 'locked': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}), |
514 | + 'longitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), |
515 | + 'person': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['identityprovider.Person']", 'unique': 'True', 'null': 'True', 'db_column': "'person'"}), |
516 | + 'time_zone': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
517 | + 'visible': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'}) |
518 | + }, |
519 | + 'identityprovider.teamparticipation': { |
520 | + 'Meta': {'unique_together': "(('team', 'person'),)", 'object_name': 'TeamParticipation', 'db_table': "u'lp_teamparticipation'"}, |
521 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
522 | + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Person']", 'null': 'True', 'db_column': "'person'"}), |
523 | + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'team_participations'", 'null': 'True', 'db_column': "'team'", 'to': "orm['identityprovider.Person']"}) |
524 | + } |
525 | + } |
526 | + |
527 | + complete_apps = ['identityprovider'] |
528 | \ No newline at end of file |
529 | |
530 | === modified file 'identityprovider/models/openidmodels.py' |
531 | --- identityprovider/models/openidmodels.py 2013-03-05 18:08:29 +0000 |
532 | +++ identityprovider/models/openidmodels.py 2013-04-03 14:40:35 +0000 |
533 | @@ -154,6 +154,7 @@ |
534 | displayname = models.TextField() |
535 | description = models.TextField() |
536 | logo = models.TextField(blank=True, null=True) |
537 | + allowed_ax = models.TextField(blank=True, null=True) |
538 | allowed_sreg = models.TextField(blank=True, null=True) |
539 | creation_rationale = models.IntegerField( |
540 | default=13, choices=AccountCreationRationale._get_choices()) |
541 | |
542 | === modified file 'identityprovider/templates/server/decide.html' |
543 | --- identityprovider/templates/server/decide.html 2013-02-14 17:12:13 +0000 |
544 | +++ identityprovider/templates/server/decide.html 2013-04-03 14:40:35 +0000 |
545 | @@ -31,15 +31,23 @@ |
546 | <form action="{{ action }}" method="POST" name="decideform"> |
547 | {% csrf_token %} |
548 | <div class="info-items"> |
549 | - {% if sreg_form.should_display or teams_form.should_display %} |
550 | + {% if sreg_form.has_data or teams_form.has_data or ax_form.has_data %} |
551 | <ul class="list"> |
552 | - {% for field in sreg_form %} |
553 | + {% if ax_form.has_data %} |
554 | + {% for field in ax_form %} |
555 | + <li class="ax">{{ field|safe }} {{ field.label_tag }}</li> |
556 | + {% endfor %} |
557 | + {% else %} |
558 | + {% if sreg_form.has_data %} |
559 | + {% for field in sreg_form %} |
560 | + <li class="sreg">{{ field|safe }} {{ field.label_tag }}</li> |
561 | + {% endfor %} |
562 | + {% endif %} |
563 | + {% endif %} |
564 | + {% if teams_form.has_data %} |
565 | + {% ifequal teams_form.fields|length 1 %} |
566 | + {% for field in teams_form %} |
567 | <li>{{ field|safe }} {{ field.label_tag }}</li> |
568 | - {% endfor %} |
569 | - {% if teams_form.should_display %} |
570 | - {% ifequal teams_form.fields|length 1 %} |
571 | - {% for field in teams_form %} |
572 | - <li>{{ field|safe }} <label for="id_{{ field.html_name }}">{% trans "Team membership:"%}</label> {{ field.label_tag }}</li> |
573 | {% endfor %} |
574 | {% else %} |
575 | <li id="teamslist_item"> |
576 | |
577 | === modified file 'identityprovider/templates/server/openidapplication-xrds.xml' |
578 | --- identityprovider/templates/server/openidapplication-xrds.xml 2012-12-04 18:51:42 +0000 |
579 | +++ identityprovider/templates/server/openidapplication-xrds.xml 2013-04-03 14:40:35 +0000 |
580 | @@ -5,6 +5,7 @@ |
581 | <XRD> |
582 | <Service priority="0"> |
583 | <Type>http://specs.openid.net/auth/2.0/server</Type> |
584 | + <Type>http://openid.net/srv/ax/1.0</Type> |
585 | <Type>http://openid.net/extensions/sreg/1.1</Type> |
586 | <Type>http://ns.launchpad.net/2007/openid-teams</Type> |
587 | <URI>{{ endpoint_url }}</URI> |
588 | |
589 | === modified file 'identityprovider/tests/openid_server/test_discovery.py' |
590 | --- identityprovider/tests/openid_server/test_discovery.py 2013-04-01 16:17:07 +0000 |
591 | +++ identityprovider/tests/openid_server/test_discovery.py 2013-04-03 14:40:35 +0000 |
592 | @@ -54,6 +54,7 @@ |
593 | 'server_url': self.base_openid_url, |
594 | 'supports': [ |
595 | 'http://specs.openid.net/auth/2.0/server', |
596 | + 'http://openid.net/srv/ax/1.0', |
597 | 'http://openid.net/extensions/sreg/1.1', |
598 | 'http://ns.launchpad.net/2007/openid-teams', |
599 | ], |
600 | |
601 | === modified file 'identityprovider/tests/test_admin.py' |
602 | --- identityprovider/tests/test_admin.py 2013-03-29 20:36:42 +0000 |
603 | +++ identityprovider/tests/test_admin.py 2013-04-03 14:40:35 +0000 |
604 | @@ -53,18 +53,33 @@ |
605 | self.assertRaises(NotRegistered, admin.site.unregister, model) |
606 | |
607 | def test_openidrpconfig_allowed_sreg_checkboxes_postable(self): |
608 | - data = {'trust_root': 'http://localhost/bla/', |
609 | - 'displayname': 'My Test RP', |
610 | - 'description': 'Bla', |
611 | - 'allowed_sreg': ['fullname', 'email'], |
612 | - 'creation_rationale': '13', |
613 | + trust_root = 'http://localhost/bla/' |
614 | + displayname = 'My Test RP' |
615 | + description = 'Bla' |
616 | + allowed_sreg = ['fullname', 'email'] |
617 | + creation_rationale = 13 |
618 | + |
619 | + data = {'trust_root': trust_root, |
620 | + 'displayname': displayname, |
621 | + 'description': description, |
622 | + 'allowed_sreg': allowed_sreg, |
623 | + 'creation_rationale': creation_rationale, |
624 | } |
625 | add_view = reverse('admin:identityprovider_openidrpconfig_add') |
626 | response = self.client.get(add_view) |
627 | response = self.client.post(add_view, data) |
628 | self.assertEqual(302, response.status_code) |
629 | - # We don't get the ID back, so continue on to the next test to |
630 | - # verify that the checkbox appears on the model's screen. |
631 | + # We don't get the ID back, so ensure we only have one entity and |
632 | + # assume it's the correct one. This is racy, but the alternative is |
633 | + # another request to the list screen to scrape the ID from there. |
634 | + self.assertEqual(OpenIDRPConfig.objects.count(), 1) |
635 | + rpconfig = OpenIDRPConfig.objects.get() |
636 | + self.assertEqual(rpconfig.trust_root, trust_root) |
637 | + self.assertEqual(rpconfig.displayname, displayname) |
638 | + self.assertEqual(rpconfig.description, description) |
639 | + self.assertEqual(sorted(rpconfig.allowed_sreg.split(',')), |
640 | + sorted(allowed_sreg)) |
641 | + self.assertEqual(rpconfig.creation_rationale, creation_rationale) |
642 | |
643 | def test_openidrpconfig_allowed_sreg_checkboxes_getable(self): |
644 | data = {'trust_root': 'http://localhost/bla/', |
645 | @@ -78,7 +93,57 @@ |
646 | 'admin:identityprovider_openidrpconfig_change', |
647 | args=(rpconfig.id,)) |
648 | response = self.client.get(change_view) |
649 | - self.assertContains(response, 'checked') |
650 | + dom = PyQuery(response.content) |
651 | + checked = dom.find('input[checked=checked]') |
652 | + self.assertEqual(len(checked), 1) |
653 | + self.assertEqual(checked[0].value, 'fullname') |
654 | + |
655 | + def test_openidrpconfig_allowed_ax_checkboxes_postable(self): |
656 | + trust_root = 'http://localhost/bla/' |
657 | + displayname = 'My Test RP' |
658 | + description = 'Bla' |
659 | + allowed_ax = ['fullname', 'email'] |
660 | + creation_rationale = 13 |
661 | + |
662 | + data = {'trust_root': trust_root, |
663 | + 'displayname': displayname, |
664 | + 'description': description, |
665 | + 'allowed_ax': allowed_ax, |
666 | + 'creation_rationale': creation_rationale, |
667 | + } |
668 | + add_view = reverse('admin:identityprovider_openidrpconfig_add') |
669 | + response = self.client.get(add_view) |
670 | + response = self.client.post(add_view, data) |
671 | + self.assertEqual(302, response.status_code) |
672 | + # We don't get the ID back, so ensure we only have one entity and |
673 | + # assume it's the correct one. This is racy, but the alternative is |
674 | + # another request to the list screen to scrape the ID from there. |
675 | + self.assertEqual(OpenIDRPConfig.objects.count(), 1) |
676 | + rpconfig = OpenIDRPConfig.objects.get() |
677 | + self.assertEqual(rpconfig.trust_root, trust_root) |
678 | + self.assertEqual(rpconfig.displayname, displayname) |
679 | + self.assertEqual(rpconfig.description, description) |
680 | + self.assertEqual(sorted(rpconfig.allowed_ax.split(',')), |
681 | + sorted(allowed_ax)) |
682 | + self.assertEqual(rpconfig.creation_rationale, creation_rationale) |
683 | + |
684 | + def test_openidrpconfig_allowed_ax_checkboxes_getable(self): |
685 | + data = {'trust_root': 'http://localhost/bla/', |
686 | + 'displayname': 'My Test RP', |
687 | + 'description': 'Bla', |
688 | + 'allowed_ax': 'email', |
689 | + 'creation_rationale': '13', |
690 | + } |
691 | + rpconfig = OpenIDRPConfig(**data) |
692 | + rpconfig.save() |
693 | + change_view = reverse( |
694 | + 'admin:identityprovider_openidrpconfig_change', |
695 | + args=(rpconfig.id,)) |
696 | + response = self.client.get(change_view) |
697 | + dom = PyQuery(response.content) |
698 | + checked = dom.find('input[checked=checked]') |
699 | + self.assertEqual(len(checked), 1) |
700 | + self.assertEqual(checked[0].value, 'email') |
701 | |
702 | def test_inline_models(self): |
703 | expected_inlines = [AccountPasswordInline, EmailAddressInline, |
704 | |
705 | === modified file 'identityprovider/tests/test_forms.py' |
706 | --- identityprovider/tests/test_forms.py 2013-03-29 21:15:13 +0000 |
707 | +++ identityprovider/tests/test_forms.py 2013-04-03 14:40:35 +0000 |
708 | @@ -6,11 +6,20 @@ |
709 | from django.http import HttpRequest |
710 | from django_openid_auth.teams import TeamsRequest |
711 | from mock import patch |
712 | +from openid.extensions.ax import ( |
713 | + AttrInfo, |
714 | + FetchRequest, |
715 | +) |
716 | from openid.extensions.sreg import SRegRequest |
717 | |
718 | from identityprovider.models.account import Account |
719 | from identityprovider.models.const import EmailStatus |
720 | +from identityprovider.const import ( |
721 | + AX_URI_FULL_NAME, |
722 | + AX_URI_EMAIL, |
723 | +) |
724 | from identityprovider.forms import ( |
725 | + AXFetchRequestForm, |
726 | DeviceRenameForm, |
727 | EditAccountForm, |
728 | LoginForm, |
729 | @@ -280,8 +289,8 @@ |
730 | self._get_request_with_post_args(), |
731 | SRegRequest(required=['fullname'], optional=['email']), |
732 | self.rpconfig) |
733 | - self.assertTrue('fullname' in form.data_approved_for_request) |
734 | - self.assertFalse('email' in form.data_approved_for_request) |
735 | + self.assertIn('fullname', form.data_approved_for_request) |
736 | + self.assertNotIn('email', form.data_approved_for_request) |
737 | |
738 | def test_optional_fields_for_trusted_site(self): |
739 | """The server should return values for optional fields to trusted |
740 | @@ -378,6 +387,194 @@ |
741 | self.assertTrue(form.check_test('email')) |
742 | |
743 | |
744 | +class AXFetchRequestFormTestCase(SSOBaseTestCase): |
745 | + """AX Fetch Request form tests. |
746 | + |
747 | + This is all functionally very similar to Simple Registration, except that |
748 | + the extensibility of AX adds an extra layer of complexity. |
749 | + """ |
750 | + |
751 | + def _get_request_with_post_args(self, args={}): |
752 | + request = HttpRequest() |
753 | + request.user = self.test_user |
754 | + request.POST = args |
755 | + request.META = {'REQUEST_METHOD': 'POST'} |
756 | + return request |
757 | + |
758 | + def setUp(self): |
759 | + super(AXFetchRequestFormTestCase, self).setUp() |
760 | + self.test_user = Account.objects.create_account( |
761 | + 'My name', 'me@test.com', DEFAULT_USER_PASSWORD) |
762 | + self.rpconfig = OpenIDRPConfig.objects.create( |
763 | + trust_root='http://localhost/', description="Some description") |
764 | + |
765 | + def test_no_approved_fields_without_post_request(self): |
766 | + """The server should not generate a list of approved fields when the |
767 | + request is not a POST request. |
768 | + """ |
769 | + request = self._get_request_with_post_args() |
770 | + request.META['REQUEST_METHOD'] = 'GET' |
771 | + fetch_request = FetchRequest() |
772 | + for (attr, alias) in [ |
773 | + (AX_URI_FULL_NAME, 'fullname'), |
774 | + (AX_URI_EMAIL, 'email')]: |
775 | + fetch_request.add(AttrInfo(attr, alias=alias, required=True)) |
776 | + form = AXFetchRequestForm(request, fetch_request, self.rpconfig) |
777 | + self.assertEqual(len(form.data_approved_for_request), 0) |
778 | + |
779 | + def test_required_fields_for_trusted_site(self): |
780 | + """The server should always return values for required fields to |
781 | + trusted sites, regardless of the state of the checkbox in the UI. |
782 | + Optional fields should not be returned if the user has unchecked them. |
783 | + """ |
784 | + self.rpconfig.allowed_ax = 'fullname,email' |
785 | + fetch_request = FetchRequest() |
786 | + fetch_request.add( |
787 | + AttrInfo(AX_URI_FULL_NAME, alias='fullname', required=True)) |
788 | + fetch_request.add( |
789 | + AttrInfo(AX_URI_EMAIL, alias='email', required=False)) |
790 | + form = AXFetchRequestForm(self._get_request_with_post_args(), |
791 | + fetch_request, self.rpconfig) |
792 | + self.assertTrue('fullname' in form.data_approved_for_request) |
793 | + self.assertFalse('email' in form.data_approved_for_request) |
794 | + |
795 | + def test_optional_fields_for_trusted_site(self): |
796 | + """The server should return values for optional fields to trusted |
797 | + sites only when the user checks the checkbox in the UI. |
798 | + """ |
799 | + self.rpconfig.allowed_ax = 'fullname,email' |
800 | + post_args = {'email': 'email'} |
801 | + fetch_request = FetchRequest() |
802 | + for (attr, alias) in [ |
803 | + (AX_URI_FULL_NAME, 'fullname'), |
804 | + (AX_URI_EMAIL, 'email')]: |
805 | + fetch_request.add(AttrInfo(attr, alias=alias, required=False)) |
806 | + form = AXFetchRequestForm(self._get_request_with_post_args(post_args), |
807 | + fetch_request, self.rpconfig) |
808 | + self.assertFalse('fullname' in form.data_approved_for_request) |
809 | + self.assertTrue('email' in form.data_approved_for_request) |
810 | + |
811 | + def test_required_fields_for_untrusted_site(self): |
812 | + """The server should return values for required fields to untrusted |
813 | + sites only when the user checks the checkbox in the UI. |
814 | + """ |
815 | + post_args = {'email': 'email'} |
816 | + fetch_request = FetchRequest() |
817 | + for (attr, alias) in [ |
818 | + (AX_URI_FULL_NAME, 'fullname'), |
819 | + (AX_URI_EMAIL, 'email')]: |
820 | + fetch_request.add(AttrInfo(attr, alias=alias, required=True)) |
821 | + form = AXFetchRequestForm(self._get_request_with_post_args(post_args), |
822 | + fetch_request, None) |
823 | + self.assertFalse('fullname' in form.data_approved_for_request) |
824 | + self.assertTrue('email' in form.data_approved_for_request) |
825 | + |
826 | + def test_optional_fields_for_untrusted_site(self): |
827 | + """The server should return values for optional fields to untrusted |
828 | + sites only when the user checks the checkbox in the UI. |
829 | + """ |
830 | + post_args = {'fullname': 'fullname'} |
831 | + fetch_request = FetchRequest() |
832 | + for (attr, alias) in [ |
833 | + (AX_URI_FULL_NAME, 'fullname'), |
834 | + (AX_URI_EMAIL, 'email')]: |
835 | + fetch_request.add(AttrInfo(attr, alias=alias, required=False)) |
836 | + form = AXFetchRequestForm(self._get_request_with_post_args(post_args), |
837 | + fetch_request, None) |
838 | + self.assertTrue('fullname' in form.data_approved_for_request) |
839 | + self.assertFalse('email' in form.data_approved_for_request) |
840 | + |
841 | + def test_checkbox_status_for_trusted_site(self): |
842 | + """Checkboxes are always checked if the site is trusted""" |
843 | + fetch_request = FetchRequest() |
844 | + fetch_request.add( |
845 | + AttrInfo(AX_URI_FULL_NAME, alias='fullname', required=True)) |
846 | + fetch_request.add( |
847 | + AttrInfo(AX_URI_EMAIL, alias='email', required=False)) |
848 | + form = AXFetchRequestForm(self._get_request_with_post_args(), |
849 | + fetch_request, self.rpconfig) |
850 | + # True because fullname required |
851 | + self.assertTrue(form.check_test('fullname')) |
852 | + # True because trusted site and no previous disapproval |
853 | + self.assertTrue(form.check_test('email')) |
854 | + # Throw in an unrequested field for good measure |
855 | + self.assertFalse(form.check_test('language')) |
856 | + |
857 | + def test_checkbox_status_for_trusted_site_with_approved_data(self): |
858 | + """If the user has previously approved sending data to a trusted site |
859 | + the same checkbox settings should be returned on the next request |
860 | + unless those conflict with the required fields. |
861 | + """ |
862 | + approved_data = { |
863 | + 'requested': ['fullname', 'email'], |
864 | + 'approved': ['email', 'language']} |
865 | + fetch_request = FetchRequest() |
866 | + fetch_request.add( |
867 | + AttrInfo(AX_URI_FULL_NAME, alias='fullname', required=True)) |
868 | + fetch_request.add( |
869 | + AttrInfo(AX_URI_EMAIL, alias='email', required=False)) |
870 | + form1 = AXFetchRequestForm(self._get_request_with_post_args(), |
871 | + fetch_request, self.rpconfig, |
872 | + approved_data=approved_data) |
873 | + # True because fullname required |
874 | + self.assertTrue(form1.check_test('fullname')) |
875 | + # True because email previously approved |
876 | + self.assertTrue(form1.check_test('email')) |
877 | + # Throw in an unrequested, previously-approved field for good measure |
878 | + self.assertFalse(form1.check_test('language')) |
879 | + |
880 | + approved_data['approved'] = [] |
881 | + form2 = AXFetchRequestForm(self._get_request_with_post_args(), |
882 | + fetch_request, self.rpconfig, |
883 | + approved_data=approved_data) |
884 | + # True because fullname required |
885 | + self.assertTrue(form1.check_test('fullname')) |
886 | + # False because email previously disapproved |
887 | + self.assertFalse(form2.check_test('email')) |
888 | + # Throw in an unrequested field for good measure |
889 | + self.assertFalse(form2.check_test('language')) |
890 | + |
891 | + def test_checkbox_status_for_untrusted_site(self): |
892 | + """Checkboxes are only checked on untrusted site requests if the field |
893 | + is required |
894 | + """ |
895 | + fetch_request = FetchRequest() |
896 | + fetch_request.add( |
897 | + AttrInfo(AX_URI_FULL_NAME, alias='fullname', required=True)) |
898 | + fetch_request.add( |
899 | + AttrInfo(AX_URI_EMAIL, alias='email', required=False)) |
900 | + form = AXFetchRequestForm(self._get_request_with_post_args(), |
901 | + fetch_request, None) |
902 | + # True because fullname required |
903 | + self.assertTrue(form.check_test('fullname')) |
904 | + # False because untrusted site and no previous approval |
905 | + self.assertFalse(form.check_test('email')) |
906 | + # Throw in an unrequested field for good measure |
907 | + self.assertFalse(form.check_test('language')) |
908 | + |
909 | + def test_checkbox_status_for_untrusted_site_with_approved_data(self): |
910 | + """If the user has previously approved sending data to an untrusted |
911 | + site the same checkbox settings should be returned on the next request. |
912 | + """ |
913 | + approved_data = { |
914 | + 'requested': ['fullname', 'email'], |
915 | + 'approved': ['email', 'language']} |
916 | + fetch_request = FetchRequest() |
917 | + fetch_request.add( |
918 | + AttrInfo(AX_URI_FULL_NAME, alias='fullname', required=True)) |
919 | + fetch_request.add( |
920 | + AttrInfo(AX_URI_EMAIL, alias='email', required=False)) |
921 | + form = AXFetchRequestForm(self._get_request_with_post_args(), |
922 | + fetch_request, None, |
923 | + approved_data=approved_data) |
924 | + # False because untrusted site and previously disapproved |
925 | + self.assertFalse(form.check_test('fullname')) |
926 | + # True because previously approved |
927 | + self.assertTrue(form.check_test('email')) |
928 | + # Throw in an unrequested, previously-approved field for good measure |
929 | + self.assertFalse(form.check_test('language')) |
930 | + |
931 | + |
932 | class TeamsRequestFormTestCase(SSOBaseTestCase): |
933 | |
934 | def _get_request_with_post_args(self, args={}): |
935 | |
936 | === modified file 'identityprovider/tests/test_views_server.py' |
937 | --- identityprovider/tests/test_views_server.py 2013-03-29 20:36:42 +0000 |
938 | +++ identityprovider/tests/test_views_server.py 2013-04-03 14:40:35 +0000 |
939 | @@ -15,6 +15,10 @@ |
940 | from django.http import HttpRequest |
941 | from gargoyle.testutils import switches |
942 | from mock import Mock, patch |
943 | +from openid.extensions.ax import ( |
944 | + AXMessage, |
945 | + FetchRequest, |
946 | +) |
947 | from openid.extensions.sreg import SRegRequest |
948 | from openid.message import ( |
949 | IDENTIFIER_SELECT, |
950 | @@ -26,6 +30,12 @@ |
951 | from pyquery import PyQuery |
952 | |
953 | import identityprovider.signed as signed |
954 | +from identityprovider.const import ( |
955 | + AX_URI_ACCOUNT_VERIFIED, |
956 | + AX_URI_EMAIL, |
957 | + AX_URI_FULL_NAME, |
958 | + AX_URI_LANGUAGE, |
959 | +) |
960 | from identityprovider.models import ( |
961 | Account, |
962 | OpenIDAuthorization, |
963 | @@ -509,7 +519,40 @@ |
964 | # no extra check is needed |
965 | server._check_team_membership(request, self.orequest, oresponse) |
966 | |
967 | - def test_list_of_details_is_complete(self): |
968 | + def test_only_ax_or_sreg_form_is_displayed(self): |
969 | + """Even if both SReg and AX requests are present, only display the AX |
970 | + form.""" |
971 | + team_name = 'ubuntu-team' |
972 | + team = self.factory.make_team(name=team_name) |
973 | + self.factory.add_account_to_team(self.account, team) |
974 | + |
975 | + # create a trusted rpconfig |
976 | + rpconfig = OpenIDRPConfig( |
977 | + trust_root='http://localhost/', |
978 | + allowed_sreg='fullname,email', |
979 | + allowed_ax='fullname,email,account_verified', |
980 | + can_query_any_team=True, |
981 | + description="Some description", |
982 | + ) |
983 | + rpconfig.save() |
984 | + param_overrides = { |
985 | + 'openid.sreg.required': 'nickname,email,fullname,language', |
986 | + 'openid.ns.ax': AXMessage.ns_uri, |
987 | + 'openid.ax.mode': FetchRequest.mode, |
988 | + 'openid.ax.type.fullname': AX_URI_FULL_NAME, |
989 | + 'openid.ax.type.email': AX_URI_EMAIL, |
990 | + 'openid.ax.type.account_verified': AX_URI_ACCOUNT_VERIFIED, |
991 | + 'openid.ax.type.language': AX_URI_LANGUAGE, |
992 | + 'openid.ax.required': 'fullname,email,account_verified,language', |
993 | + 'openid.lp.query_membership': team_name, |
994 | + } |
995 | + self._prepare_openid_token(param_overrides=param_overrides) |
996 | + response = self.client.get('/%s/+decide' % self.token) |
997 | + dom = PyQuery(response.content) |
998 | + self.assertEqual(len(dom.find('li.ax')), 3) |
999 | + self.assertEqual(len(dom.find('li.sreg')), 0) |
1000 | + |
1001 | + def test_list_of_details_is_complete_with_sreg(self): |
1002 | team_name = 'ubuntu-team' |
1003 | team = self.factory.make_team(name=team_name) |
1004 | self.factory.add_account_to_team(self.account, team) |
1005 | @@ -532,7 +575,91 @@ |
1006 | self.assertContains(response, "Email address") |
1007 | self.assertContains(response, "Preferred language") |
1008 | |
1009 | - def test_state_of_checkboxes_and_data_formats_trusted(self): |
1010 | + def test_list_of_details_is_complete_with_ax(self): |
1011 | + team_name = 'ubuntu-team' |
1012 | + team = self.factory.make_team(name=team_name) |
1013 | + self.factory.add_account_to_team(self.account, team) |
1014 | + |
1015 | + # create a trusted rpconfig |
1016 | + rpconfig = OpenIDRPConfig( |
1017 | + trust_root='http://localhost/', |
1018 | + allowed_ax='fullname,email,account_verified', |
1019 | + can_query_any_team=True, |
1020 | + description="Some description", |
1021 | + ) |
1022 | + rpconfig.save() |
1023 | + param_overrides = { |
1024 | + 'openid.ns.ax': AXMessage.ns_uri, |
1025 | + 'openid.ax.mode': FetchRequest.mode, |
1026 | + 'openid.ax.type.fullname': AX_URI_FULL_NAME, |
1027 | + 'openid.ax.type.email': AX_URI_EMAIL, |
1028 | + 'openid.ax.type.account_verified': AX_URI_ACCOUNT_VERIFIED, |
1029 | + 'openid.ax.type.language': AX_URI_LANGUAGE, |
1030 | + 'openid.ax.required': 'fullname,email,account_verified,language', |
1031 | + 'openid.lp.query_membership': team_name, |
1032 | + } |
1033 | + self._prepare_openid_token(param_overrides=param_overrides) |
1034 | + response = self.client.get('/%s/+decide' % self.token) |
1035 | + self.assertContains(response, "Team membership") |
1036 | + self.assertContains(response, "Full name") |
1037 | + self.assertContains(response, "Email address") |
1038 | + self.assertContains(response, "Account verified") |
1039 | + |
1040 | + def _test_state_of_checkboxes_and_data_formats( |
1041 | + self, dom, field, label=None, value=None, required=False, |
1042 | + disabled=False, checked=False): |
1043 | + elem = dom.find('#id_%s' % field) |
1044 | + self.assertEqual(len(elem), 1) |
1045 | + self.assertEqual(elem[0].get('type'), 'checkbox') |
1046 | + if required: |
1047 | + self.assertEqual(elem[0].get('class'), 'required') |
1048 | + else: |
1049 | + self.assertIsNone(elem[0].get('class')) |
1050 | + if checked: |
1051 | + self.assertEqual(elem[0].get('checked'), 'checked') |
1052 | + else: |
1053 | + self.assertIsNone(elem[0].get('checked')) |
1054 | + if disabled: |
1055 | + self.assertEqual(elem[0].get('disabled'), 'disabled') |
1056 | + else: |
1057 | + self.assertIsNone(elem[0].get('disabled')) |
1058 | + self.assertEqual(elem[0].get('value'), value) |
1059 | + elem = dom.find('label[for=id_%s]' % field) |
1060 | + self.assertEqual(len(elem), 1) |
1061 | + self.assertEqual( |
1062 | + elem[0].text, '%s: %s' % (label, value) if label else value) |
1063 | + |
1064 | + def _test_required_trusted_field(self, dom, field, label=None, value=None): |
1065 | + """Required fields for trusted RPs *should* be checked, *should* be |
1066 | + disabled and *should* be required.""" |
1067 | + self._test_state_of_checkboxes_and_data_formats( |
1068 | + dom, field=field, label=label, value=value, |
1069 | + required=True, disabled=True, checked=True) |
1070 | + |
1071 | + def _test_optional_trusted_field(self, dom, field, label=None, value=None): |
1072 | + """Optional fields for trusted RPs *should* be checked, *should not* be |
1073 | + disabled and *should not* be required.""" |
1074 | + self._test_state_of_checkboxes_and_data_formats( |
1075 | + dom, field=field, label=label, value=value, checked=True, |
1076 | + disabled=False, required=False) |
1077 | + |
1078 | + def _test_required_untrusted_field(self, dom, field, label=None, |
1079 | + value=None): |
1080 | + """Required fields for untrusted RPs *should* be checked, *should not* |
1081 | + be disabled and *should* be required.""" |
1082 | + self._test_state_of_checkboxes_and_data_formats( |
1083 | + dom, field=field, label=label, value=value, checked=True, |
1084 | + disabled=False, required=True) |
1085 | + |
1086 | + def _test_optional_untrusted_field(self, dom, field, label=None, |
1087 | + value=None): |
1088 | + """Optional fields for untrusted RPs *should not* be checked, |
1089 | + *should not* be disabled and *should not* be required.""" |
1090 | + self._test_state_of_checkboxes_and_data_formats( |
1091 | + dom, field=field, label=label, value=value, required=False, |
1092 | + checked=False, disabled=False) |
1093 | + |
1094 | + def test_state_of_checkboxes_and_data_formats_trusted_sreg(self): |
1095 | teams = ('ubuntu-team', 'launchpad-team', 'isd-team') |
1096 | for team_name in teams: |
1097 | team = self.factory.make_team(name=team_name) |
1098 | @@ -552,39 +679,68 @@ |
1099 | } |
1100 | self._prepare_openid_token(param_overrides=param_overrides) |
1101 | response = self.client.get(self.url) |
1102 | - # checkbox checked and disabled for required fields and label is bold |
1103 | + dom = PyQuery(response.content) |
1104 | + self.assertEqual(len(dom.find('li.sreg')), 3) |
1105 | + |
1106 | nickname = self.account.person.name |
1107 | - username_html = ('<li><input checked="checked" name="nickname" ' |
1108 | - 'value="%s" class="required" disabled="disabled" ' |
1109 | - 'type="checkbox" id="id_nickname" /> <label ' |
1110 | - 'for="id_nickname">Username: %s</label></li>' % |
1111 | - (nickname, nickname)) |
1112 | - email_html = ('<li><input checked="checked" name="email" ' |
1113 | - 'value="%s" class="required" ' |
1114 | - 'disabled="disabled" type="checkbox" id="id_email" /> ' |
1115 | - '<label for="id_email">Email address: %s</label></li>' % |
1116 | - (self.login_email, self.login_email)) |
1117 | - # checkbox checked and enabled for optional fields and label is plain |
1118 | - language_html = ('<li><input checked="checked" type="checkbox" ' |
1119 | - 'name="language" value="en" id="id_language" /> ' |
1120 | - '<label for="id_language">Preferred language: en' |
1121 | - '</label></li>') |
1122 | - # team data is enabled and checked, the label is plain |
1123 | - team_html_1 = ('<li><input checked="checked" type="checkbox" ' |
1124 | - 'name="ubuntu-team" value="ubuntu-team" id="id_' |
1125 | - 'ubuntu-team" /> <label for="id_ubuntu-team">' |
1126 | - 'ubuntu-team</label></li>') |
1127 | - team_html_2 = ('<li><input checked="checked" type="checkbox" ' |
1128 | - 'name="isd-team" value="isd-team" id="id_isd-team" /> ' |
1129 | - '<label for="id_isd-team">isd-team</label></li>') |
1130 | - |
1131 | - self.assertContains(response, username_html) |
1132 | - self.assertContains(response, email_html) |
1133 | - self.assertContains(response, language_html) |
1134 | - self.assertContains(response, team_html_1) |
1135 | - self.assertContains(response, team_html_2) |
1136 | - |
1137 | - def test_state_of_checkboxes_and_data_formats_untrusted(self): |
1138 | + self._test_required_trusted_field(dom, field='nickname', |
1139 | + label='Username', value=nickname) |
1140 | + self._test_required_trusted_field(dom, field='email', |
1141 | + label='Email address', |
1142 | + value=self.login_email) |
1143 | + |
1144 | + self._test_optional_trusted_field(dom, field='language', |
1145 | + label='Preferred language', |
1146 | + value='en') |
1147 | + |
1148 | + for team in teams: |
1149 | + self._test_optional_trusted_field(dom, field=team, value=team) |
1150 | + |
1151 | + def test_state_of_checkboxes_and_data_formats_trusted_ax(self): |
1152 | + teams = ('ubuntu-team', 'launchpad-team', 'isd-team') |
1153 | + for team_name in teams: |
1154 | + team = self.factory.make_team(name=team_name) |
1155 | + self.factory.add_account_to_team(self.account, team) |
1156 | + |
1157 | + # create a trusted rpconfig |
1158 | + rpconfig = OpenIDRPConfig( |
1159 | + trust_root='http://localhost/', |
1160 | + allowed_ax='nickname,email,language,account_verified', |
1161 | + can_query_any_team=True, |
1162 | + description="Some description", |
1163 | + ) |
1164 | + rpconfig.save() |
1165 | + param_overrides = { |
1166 | + 'openid.ns.ax': AXMessage.ns_uri, |
1167 | + 'openid.ax.mode': FetchRequest.mode, |
1168 | + 'openid.ax.type.fullname': AX_URI_FULL_NAME, |
1169 | + 'openid.ax.type.email': AX_URI_EMAIL, |
1170 | + 'openid.ax.type.account_verified': AX_URI_ACCOUNT_VERIFIED, |
1171 | + 'openid.ax.type.language': AX_URI_LANGUAGE, |
1172 | + 'openid.ax.required': 'fullname,email,account_verified', |
1173 | + 'openid.ax.if_available': 'language', |
1174 | + 'openid.lp.query_membership': ','.join(teams), |
1175 | + } |
1176 | + self._prepare_openid_token(param_overrides=param_overrides) |
1177 | + response = self.client.get('/%s/+decide' % self.token) |
1178 | + dom = PyQuery(response.content) |
1179 | + self.assertEqual(len(dom.find('li.ax')), 3) |
1180 | + |
1181 | + self._test_required_trusted_field(dom, field='email', |
1182 | + label='Email address', |
1183 | + value=self.login_email) |
1184 | + self._test_required_trusted_field(dom, field='account_verified', |
1185 | + label='Account verified', |
1186 | + value='token_via_email') |
1187 | + |
1188 | + self._test_optional_trusted_field(dom, field='language', |
1189 | + label='Preferred language', |
1190 | + value='en') |
1191 | + |
1192 | + for team in teams: |
1193 | + self._test_optional_trusted_field(dom, field=team, value=team) |
1194 | + |
1195 | + def test_state_of_checkboxes_and_data_formats_untrusted_sreg(self): |
1196 | team_name = 'ubuntu-team' |
1197 | team = self.factory.make_team(name=team_name) |
1198 | self.factory.add_account_to_team(self.account, team) |
1199 | @@ -602,30 +758,71 @@ |
1200 | } |
1201 | self._prepare_openid_token(param_overrides=param_overrides) |
1202 | response = self.client.get(self.url) |
1203 | + dom = PyQuery(response.content) |
1204 | + self.assertEqual(len(dom.find('li.sreg')), 4) |
1205 | + |
1206 | nickname = self.account.person.name |
1207 | - # checkbox checked and *enabled* for required fields and label is bold |
1208 | - username_html = ('<li><input checked="checked" name="nickname" ' |
1209 | - 'value="%s" class="required" type="checkbox" ' |
1210 | - 'id="id_nickname" /> <label for="id_nickname">' |
1211 | - 'Username: %s</label></li>' % (nickname, nickname)) |
1212 | - email_html = ('<li><input checked="checked" name="email" ' |
1213 | - 'value="%s" class="required" type=' |
1214 | - '"checkbox" id="id_email" /> <label for="id_email">' |
1215 | - 'Email address: %s</label></li>' % |
1216 | - (self.login_email, self.login_email)) |
1217 | - # checkbox *not checked* and enabled for optional fields & plain label |
1218 | - language_html = ('<li><input type="checkbox" name="language" value="' |
1219 | - 'en" id="id_language" /> <label for="id_language">' |
1220 | - 'Preferred language: en</label></li>') |
1221 | - # team data is enabled and *not checked*, the label is plain |
1222 | - team_html = ('<li><input type="checkbox" name="ubuntu-team" ' |
1223 | - 'value="ubuntu-team" id="id_ubuntu-team" /> ' |
1224 | - '<label for="id_ubuntu-team">Team membership:</label> ' |
1225 | - '<label for="id_ubuntu-team">ubuntu-team</label></li>') |
1226 | - self.assertContains(response, username_html) |
1227 | - self.assertContains(response, email_html) |
1228 | - self.assertContains(response, language_html) |
1229 | - self.assertContains(response, team_html) |
1230 | + self._test_required_untrusted_field(dom, field='nickname', |
1231 | + label='Username', value=nickname) |
1232 | + self._test_required_untrusted_field(dom, field='email', |
1233 | + label='Email address', |
1234 | + value=self.login_email) |
1235 | + |
1236 | + self._test_optional_untrusted_field(dom, field='language', |
1237 | + label='Preferred language', |
1238 | + value='en') |
1239 | + |
1240 | + self._test_optional_untrusted_field(dom, field=team_name, |
1241 | + label='Team membership', |
1242 | + value=team_name) |
1243 | + |
1244 | + def test_state_of_checkboxes_and_data_formats_untrusted_ax(self): |
1245 | + team_name = 'ubuntu-team' |
1246 | + team = self.factory.make_team(name=team_name) |
1247 | + self.factory.add_account_to_team(self.account, team) |
1248 | + |
1249 | + # create a trusted rpconfig |
1250 | + trust_root = 'http://untrusted/' |
1251 | + rpconfig = OpenIDRPConfig( |
1252 | + trust_root=trust_root, |
1253 | + can_query_any_team=True, |
1254 | + description="Some description", |
1255 | + ) |
1256 | + |
1257 | + delete_rpconfig_cache_entry("http://localhost/") |
1258 | + |
1259 | + rpconfig.save() |
1260 | + param_overrides = { |
1261 | + 'openid.ns.ax': AXMessage.ns_uri, |
1262 | + 'openid.ax.mode': FetchRequest.mode, |
1263 | + 'openid.ax.type.fullname': AX_URI_FULL_NAME, |
1264 | + 'openid.ax.type.email': AX_URI_EMAIL, |
1265 | + 'openid.ax.type.account_verified': AX_URI_ACCOUNT_VERIFIED, |
1266 | + 'openid.ax.type.language': AX_URI_LANGUAGE, |
1267 | + 'openid.ax.required': 'fullname,email,account_verified', |
1268 | + 'openid.ax.if_available': 'language', |
1269 | + 'openid.lp.query_membership': 'ubuntu-team', |
1270 | + } |
1271 | + self._prepare_openid_token(param_overrides=param_overrides) |
1272 | + response = self.client.get('/%s/+decide' % self.token) |
1273 | + dom = PyQuery(response.content) |
1274 | + self.assertEqual(len(dom.find('li.ax')), 4) |
1275 | + |
1276 | + fullname = self.account.get_full_name() |
1277 | + self._test_required_untrusted_field(dom, field='fullname', |
1278 | + label='Full name', |
1279 | + value=fullname) |
1280 | + self._test_required_untrusted_field(dom, field='email', |
1281 | + label='Email address', |
1282 | + value=self.login_email) |
1283 | + |
1284 | + self._test_optional_untrusted_field(dom, field='language', |
1285 | + label='Preferred language', |
1286 | + value='en') |
1287 | + |
1288 | + self._test_optional_untrusted_field(dom, field=team_name, |
1289 | + label='Team membership', |
1290 | + value=team_name) |
1291 | |
1292 | |
1293 | class DecideUserUnverifiedTestCase(DecideBaseTestCase): |
1294 | @@ -1353,7 +1550,8 @@ |
1295 | |
1296 | class ApprovedDataTestCase(SSOBaseTestCase): |
1297 | |
1298 | - def _get_openid_request(self, with_sreg=True, with_teams=True): |
1299 | + def _get_openid_request( |
1300 | + self, with_sreg=True, with_ax=True, with_teams=True): |
1301 | request = { |
1302 | 'openid.mode': 'checkid_setup', |
1303 | 'openid.trust_root': 'http://localhost/', |
1304 | @@ -1361,6 +1559,12 @@ |
1305 | 'openid.identity': IDENTIFIER_SELECT} |
1306 | if with_sreg: |
1307 | request['openid.sreg.required'] = 'email,fullname' |
1308 | + if with_ax: |
1309 | + request['openid.ns.ax'] = AXMessage.ns_uri |
1310 | + request['openid.ax.mode'] = FetchRequest.mode |
1311 | + request['openid.ax.type.fullname'] = AX_URI_FULL_NAME |
1312 | + request['openid.ax.type.email'] = AX_URI_EMAIL |
1313 | + request['openid.ax.required'] = 'email,fullname' |
1314 | if with_teams: |
1315 | request['openid.lp.query_membership'] = 'ubuntu-team' |
1316 | openid_server = server._get_openid_server() |
1317 | @@ -1391,31 +1595,62 @@ |
1318 | post_args = {'email': 'email', 'ubuntu-team': 'ubuntu-team'} |
1319 | result = server._get_approved_data( |
1320 | self._get_request_with_post_args(post_args), |
1321 | - self._get_openid_request(True, False)) |
1322 | + self._get_openid_request(with_sreg=True, with_ax=False, |
1323 | + with_teams=False)) |
1324 | self.assertEqual(sorted(result['sreg']['requested']), |
1325 | ['email', 'fullname']) |
1326 | self.assertEqual(result['sreg']['approved'], ['email']) |
1327 | self.assertNotIn('teams', result) |
1328 | + self.assertNotIn('ax', result) |
1329 | + |
1330 | + def test_approved_data_for_ax_only(self): |
1331 | + post_args = {'email': 'email', 'ubuntu-team': 'ubuntu-team'} |
1332 | + result = server._get_approved_data( |
1333 | + self._get_request_with_post_args(post_args), |
1334 | + self._get_openid_request(with_sreg=False, with_ax=True, |
1335 | + with_teams=False)) |
1336 | + self.assertEqual(sorted(result['ax']['requested']), |
1337 | + ['email', 'fullname']) |
1338 | + self.assertEqual(result['ax']['approved'], ['email']) |
1339 | + self.assertNotIn('sreg', result) |
1340 | + self.assertNotIn('teams', result) |
1341 | |
1342 | def test_approved_data_for_teams_only(self): |
1343 | post_args = {'ubuntu-team': 'ubuntu-team'} |
1344 | result = server._get_approved_data( |
1345 | self._get_request_with_post_args(post_args), |
1346 | - self._get_openid_request(False, True)) |
1347 | + self._get_openid_request(with_sreg=False, with_ax=False, |
1348 | + with_teams=True)) |
1349 | self.assertEqual(result['teams']['requested'], ['ubuntu-team']) |
1350 | self.assertEqual(result['teams']['approved'], ['ubuntu-team']) |
1351 | + self.assertNotIn('ax', result) |
1352 | self.assertNotIn('sreg', result) |
1353 | |
1354 | def test_approved_data_for_sreg_and_teams(self): |
1355 | post_args = {'email': 'email', 'ubuntu-team': 'ubuntu-team'} |
1356 | result = server._get_approved_data( |
1357 | self._get_request_with_post_args(post_args), |
1358 | - self._get_openid_request()) |
1359 | + self._get_openid_request(with_sreg=True, with_ax=False, |
1360 | + with_teams=True)) |
1361 | self.assertEqual(sorted(result['sreg']['requested']), |
1362 | ['email', 'fullname']) |
1363 | self.assertEqual(result['sreg']['approved'], ['email']) |
1364 | self.assertEqual(result['teams']['requested'], ['ubuntu-team']) |
1365 | self.assertEqual(result['teams']['approved'], ['ubuntu-team']) |
1366 | + self.assertNotIn('ax', result) |
1367 | + |
1368 | + def test_approved_data_for_ax_and_teams(self): |
1369 | + post_args = {'email': 'email', 'ubuntu-team': 'ubuntu-team'} |
1370 | + result = server._get_approved_data( |
1371 | + self._get_request_with_post_args(post_args), |
1372 | + self._get_openid_request(with_sreg=False, with_ax=True, |
1373 | + with_teams=True)) |
1374 | + self.assertEqual(sorted(result['ax']['requested']), |
1375 | + ['email', 'fullname']) |
1376 | + self.assertEqual(result['ax']['approved'], ['email']) |
1377 | + self.assertEqual(result['teams']['requested'], ['ubuntu-team']) |
1378 | + self.assertEqual(result['teams']['approved'], ['ubuntu-team']) |
1379 | + self.assertNotIn('sreg', result) |
1380 | |
1381 | |
1382 | class TokenLoginTestCase(SSOBaseTestCase): |
1383 | |
1384 | === modified file 'identityprovider/views/server.py' |
1385 | --- identityprovider/views/server.py 2013-03-28 17:20:52 +0000 |
1386 | +++ identityprovider/views/server.py 2013-04-03 14:40:35 +0000 |
1387 | @@ -12,7 +12,10 @@ |
1388 | timedelta, |
1389 | ) |
1390 | |
1391 | -from openid.extensions import pape |
1392 | +from openid.extensions import ( |
1393 | + ax, |
1394 | + pape, |
1395 | +) |
1396 | from openid.extensions.sreg import ( |
1397 | SRegRequest, |
1398 | SRegResponse, |
1399 | @@ -59,8 +62,12 @@ |
1400 | |
1401 | import identityprovider.signed as signed |
1402 | |
1403 | -from identityprovider.const import LAUNCHPAD_TEAMS_NS |
1404 | +from identityprovider.const import ( |
1405 | + AX_DATA_FIELDS, |
1406 | + LAUNCHPAD_TEAMS_NS, |
1407 | +) |
1408 | from identityprovider.forms import ( |
1409 | + AXFetchRequestForm, |
1410 | PreAuthorizeForm, |
1411 | SRegRequestForm, |
1412 | TeamsRequestForm, |
1413 | @@ -278,6 +285,7 @@ |
1414 | return _process_decide(request, orequest, decision=True) |
1415 | |
1416 | sreg_request = SRegRequest.fromOpenIDRequest(orequest) |
1417 | + ax_request = ax.FetchRequest.fromOpenIDRequest(orequest) |
1418 | teams_request = TeamsRequest.fromOpenIDRequest(orequest) |
1419 | try: |
1420 | summary = OpenIDRPSummary.objects.get( |
1421 | @@ -287,6 +295,9 @@ |
1422 | except OpenIDRPSummary.DoesNotExist: |
1423 | approved_data = {} |
1424 | |
1425 | + ax_form = (AXFetchRequestForm( |
1426 | + request, ax_request, rpconfig, approved_data=approved_data.get('ax')) |
1427 | + if ax_request else None) |
1428 | sreg_form = SRegRequestForm(request, sreg_request, rpconfig, |
1429 | approved_data=approved_data.get('sreg')) |
1430 | teams_form = TeamsRequestForm(request, teams_request, rpconfig, |
1431 | @@ -295,6 +306,7 @@ |
1432 | 'account': request.user, |
1433 | 'trust_root': orequest.trust_root, |
1434 | 'rpconfig': rpconfig, |
1435 | + 'ax_form': ax_form, |
1436 | 'sreg_form': sreg_form, |
1437 | 'teams_form': teams_form, |
1438 | 'token': token, |
1439 | @@ -541,15 +553,23 @@ |
1440 | return None |
1441 | |
1442 | approved_data = {} |
1443 | - |
1444 | - sreg_request = SRegRequest.fromOpenIDRequest(orequest) |
1445 | rpconfig = utils.get_rpconfig(orequest.trust_root) |
1446 | + |
1447 | + sreg_request = SRegRequest.fromOpenIDRequest(orequest) |
1448 | sreg_form = SRegRequestForm(request, sreg_request, rpconfig) |
1449 | - if len(sreg_form.data.keys()) > 0: |
1450 | + if sreg_form.has_data: |
1451 | approved_data['sreg'] = { |
1452 | 'requested': sreg_form.data.keys(), |
1453 | 'approved': sreg_form.data_approved_for_request.keys()} |
1454 | |
1455 | + ax_request = ax.FetchRequest.fromOpenIDRequest(orequest) |
1456 | + if ax_request: |
1457 | + ax_form = AXFetchRequestForm(request, ax_request, rpconfig) |
1458 | + if ax_form.has_data: |
1459 | + approved_data['ax'] = { |
1460 | + 'requested': ax_form.data.keys(), |
1461 | + 'approved': ax_form.data_approved_for_request.keys()} |
1462 | + |
1463 | args = orequest.message.getArgs(LAUNCHPAD_TEAMS_NS) |
1464 | team_names = args.get('query_membership') |
1465 | if team_names: |
1466 | @@ -586,6 +606,18 @@ |
1467 | openid_response.addExtension(sreg_response) |
1468 | |
1469 | |
1470 | +def _add_ax(request, openid_request, openid_response): |
1471 | + ax_request = ax.FetchRequest.fromOpenIDRequest(openid_request) |
1472 | + if ax_request: |
1473 | + rpconfig = utils.get_rpconfig(openid_request.trust_root) |
1474 | + form = AXFetchRequestForm(request, ax_request, rpconfig) |
1475 | + if form.data_approved_for_request: |
1476 | + ax_response = ax.FetchResponse(ax_request) |
1477 | + for k, v in form.data_approved_for_request.iteritems(): |
1478 | + ax_response.addValue(AX_DATA_FIELDS.getNamespaceURI(k), v) |
1479 | + openid_response.addExtension(ax_response) |
1480 | + |
1481 | + |
1482 | def _process_decide(request, orequest, decision): |
1483 | oresponse = orequest.answer( |
1484 | decision, identity=request.user.openid_identity_url) |
1485 | @@ -603,6 +635,7 @@ |
1486 | orequest.trust_root, |
1487 | client_id=request.session.session_key) |
1488 | _add_sreg(request, orequest, oresponse) |
1489 | + _add_ax(request, orequest, oresponse) |
1490 | # if there's no submitted POST data, this is an auto-authorized |
1491 | # (immediate) request |
1492 | immediate = not request.POST |
- missing commit message 870-894, 930-954, 988-1024, 1065-1086: we now tend to prefer using pyquery for asserting against the DOM
- instead of *_EMAIL_VERIFIED I'd use *_ACCOUNT_VERIFIED (as we should care about the status of the account, not the email; if the account is verified, the returned email is also verified).
- l. 96-99: I'd extract this into a helper method for readability
- l. 101-105: ditto
- l. 114-135: since these values (except email_verified) are the same for AX or SREG, why not have a common helper function to get them (DRY)?
- l. 462-463: you can use {% elif ... %} here I believe?
- l. 516: why not get the entry again and confirm it has the fields you just posted?
- l. 531: this could match any checkbox in the form? why not rather add a specific test to verify email is listed as an ax field?
- l. 613-614: you could also use assertIn, assertNotIn
- l. 818-819,
- l. 1166: can we call _get_openid_request with named params? it's hard to understand what each param means otherwise
- l. 1250: why not just do: if ax_form.data: ?
- l. 1250-1253: or maybe add a helper to the form to get the values without having to know that data is a dict?
Despite some desired changes, I must say I'm impressed with the clarity of the work done. It was very easy to understand the changes and follow the new logic.
Well done!