Merge lp:~mfoord/canonical-identity-provider/keep-passwords into lp:canonical-identity-provider/release

Proposed by Michael Foord
Status: Rejected
Rejected by: Natalia Bidart
Proposed branch: lp:~mfoord/canonical-identity-provider/keep-passwords
Merge into: lp:canonical-identity-provider/release
Diff against target: 620 lines (+326/-54)
11 files modified
src/identityprovider/admin.py (+5/-0)
src/identityprovider/management/commands/populate.py (+3/-2)
src/identityprovider/migrations/0017_keep_passwords.py (+236/-0)
src/identityprovider/models/account.py (+28/-10)
src/identityprovider/schema.py (+1/-0)
src/identityprovider/signals.py (+1/-13)
src/identityprovider/templates/nexus/admin/password_change_form.html (+0/-1)
src/identityprovider/tests/test_admin.py (+7/-7)
src/identityprovider/tests/test_models_account.py (+7/-0)
src/identityprovider/tests/test_signals.py (+2/-20)
src/identityprovider/validators.py (+36/-1)
To merge this branch: bzr merge lp:~mfoord/canonical-identity-provider/keep-passwords
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+196209@code.launchpad.net
To post a comment you must log in.
1098. By Michael Foord

Remove the invalidate tokens signal

1099. By Michael Foord

Merge

1100. By Michael Foord

Clear oauth tokens when password changes

1101. By Michael Foord

Remove and move old tests

1102. By Michael Foord

Merge

1103. By Michael Foord

PCI compliance validator

1104. By Michael Foord

Fixes to the PCI validator

1105. By Michael Foord

Fix circular import

1106. By Michael Foord

Add PCI group setting

1107. By Michael Foord

Merge

1108. By Michael Foord

Merge

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Hi! I'm doing some cleanup on SSO MPs, I assume this is no longer valid, will set as Rejected.

Please shange the global status if this is still current. Thanks!

Unmerged revisions

1108. By Michael Foord

Merge

1107. By Michael Foord

Merge

1106. By Michael Foord

Add PCI group setting

1105. By Michael Foord

Fix circular import

1104. By Michael Foord

Fixes to the PCI validator

1103. By Michael Foord

PCI compliance validator

1102. By Michael Foord

Merge

1101. By Michael Foord

Remove and move old tests

1100. By Michael Foord

Clear oauth tokens when password changes

1099. By Michael Foord

Merge

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/identityprovider/admin.py'
2--- src/identityprovider/admin.py 2013-11-26 15:40:55 +0000
3+++ src/identityprovider/admin.py 2013-11-27 18:08:05 +0000
4@@ -1,6 +1,8 @@
5 # Copyright 2010,2013 Canonical Ltd. This software is licensed under the
6 # GNU Affero General Public License version 3 (see the file LICENSE).
7
8+from datetime import datetime
9+
10 from django import forms
11 from django.contrib import admin
12 from django.contrib.auth.admin import UserAdmin
13@@ -134,6 +136,7 @@
14
15 class Meta:
16 model = AccountPassword
17+ exclude = ['when_created']
18
19 def __init__(self, *args, **kwargs):
20 initial = kwargs.get('initial', {})
21@@ -143,6 +146,7 @@
22
23
24 class AccountPasswordFormset(forms.models.BaseInlineFormSet):
25+
26 def __init__(self, *args, **kwargs):
27 super(AccountPasswordFormset, self).__init__(*args, **kwargs)
28 self.can_delete = False
29@@ -260,6 +264,7 @@
30 instance = instances[0]
31 encrypted_password = make_password(password)
32 instance.password = encrypted_password
33+ instance.when_created = datetime.utcnow()
34 instance.save()
35 else:
36 super(AccountAdmin, self).save_formset(
37
38=== modified file 'src/identityprovider/management/commands/populate.py'
39--- src/identityprovider/management/commands/populate.py 2013-11-19 18:45:46 +0000
40+++ src/identityprovider/management/commands/populate.py 2013-11-27 18:08:05 +0000
41@@ -167,14 +167,15 @@
42 def gen_accountpassword(self, id):
43 """Return a sequence representing an AccountPassword.
44
45- Currently (id, account, password)
46+ Currently (id, account, password, when_created)
47 """
48 account_id = (id - self.ranges['accountpassword'][0] +
49 self.ranges['account'][0])
50 plaintext = self.random_string(size=8)
51 self.passwords.append(plaintext)
52 password = make_password(plaintext)
53- return [str(id), str(account_id), password]
54+ when_created = self.random_date()
55+ return [str(id), str(account_id), password, when_created]
56
57 def gen_lp_openididentifier(self, id):
58 """ Returns a sequence representing an LPOpenIdIdentfier.
59
60=== added file 'src/identityprovider/migrations/0017_keep_passwords.py'
61--- src/identityprovider/migrations/0017_keep_passwords.py 1970-01-01 00:00:00 +0000
62+++ src/identityprovider/migrations/0017_keep_passwords.py 2013-11-27 18:08:05 +0000
63@@ -0,0 +1,236 @@
64+# -*- coding: utf-8 -*-
65+import datetime
66+from south.db import db
67+from south.v2 import SchemaMigration
68+from django.db import models
69+
70+
71+class Migration(SchemaMigration):
72+
73+ def forwards(self, orm):
74+ # Removing unique constraint on 'AccountPassword', fields ['account']
75+ db.delete_unique(u'accountpassword', ['account'])
76+
77+ # Adding field 'AccountPassword.when_created'
78+ db.add_column(u'accountpassword', 'when_created',
79+ self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now),
80+ keep_default=False)
81+
82+
83+ # Changing field 'AccountPassword.account'
84+ db.alter_column(u'accountpassword', 'account', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['identityprovider.Account'], db_column='account'))
85+
86+ def backwards(self, orm):
87+ # Deleting field 'AccountPassword.when_created'
88+ db.delete_column(u'accountpassword', 'when_created')
89+
90+
91+ # Changing field 'AccountPassword.account'
92+ db.alter_column(u'accountpassword', 'account', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['identityprovider.Account'], unique=True, db_column='account'))
93+ # Adding unique constraint on 'AccountPassword', fields ['account']
94+ db.create_unique(u'accountpassword', ['account'])
95+
96+
97+ models = {
98+ 'identityprovider.account': {
99+ 'Meta': {'object_name': 'Account', 'db_table': "u'account'"},
100+ 'creation_rationale': ('django.db.models.fields.IntegerField', [], {}),
101+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}),
102+ 'date_status_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}),
103+ 'displayname': ('identityprovider.models.account.DisplaynameField', [], {}),
104+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
105+ 'old_openid_identifier': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
106+ 'openid_identifier': ('django.db.models.fields.TextField', [], {'default': "u'XrCnJdc'", 'unique': 'True'}),
107+ 'preferredlanguage': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
108+ 'status': ('django.db.models.fields.IntegerField', [], {}),
109+ 'status_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
110+ 'twofactor_attempts': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
111+ 'twofactor_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
112+ 'warn_about_backup_device': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
113+ },
114+ 'identityprovider.accountpassword': {
115+ 'Meta': {'ordering': "['-when_created']", 'object_name': 'AccountPassword', 'db_table': "u'accountpassword'"},
116+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}),
117+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
118+ 'password': ('identityprovider.models.account.PasswordField', [], {}),
119+ 'when_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
120+ },
121+ 'identityprovider.apiuser': {
122+ 'Meta': {'object_name': 'APIUser', 'db_table': "'api_user'"},
123+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
124+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
125+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
126+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
127+ 'username': ('django.db.models.fields.CharField', [], {'max_length': '256'})
128+ },
129+ 'identityprovider.authenticationdevice': {
130+ 'Meta': {'ordering': "('id',)", 'object_name': 'AuthenticationDevice'},
131+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'devices'", 'to': "orm['identityprovider.Account']"}),
132+ 'counter': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
133+ 'device_type': ('django.db.models.fields.TextField', [], {'null': 'True'}),
134+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
135+ 'key': ('django.db.models.fields.TextField', [], {}),
136+ 'name': ('django.db.models.fields.TextField', [], {})
137+ },
138+ 'identityprovider.authtoken': {
139+ 'Meta': {'object_name': 'AuthToken', 'db_table': "u'authtoken'"},
140+ 'date_consumed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
141+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'db_index': 'True', 'blank': 'True'}),
142+ 'displayname': ('identityprovider.models.account.DisplaynameField', [], {'null': 'True', 'blank': 'True'}),
143+ 'email': ('django.db.models.fields.TextField', [], {'db_index': 'True'}),
144+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
145+ 'password': ('identityprovider.models.account.PasswordField', [], {'null': 'True', 'blank': 'True'}),
146+ 'redirection_url': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
147+ 'requester': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'requester'", 'blank': 'True'}),
148+ 'requester_email': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
149+ 'token': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
150+ 'token_type': ('django.db.models.fields.IntegerField', [], {})
151+ },
152+ 'identityprovider.emailaddress': {
153+ 'Meta': {'object_name': 'EmailAddress', 'db_table': "u'emailaddress'"},
154+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'account'", 'blank': 'True'}),
155+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
156+ 'email': ('django.db.models.fields.TextField', [], {}),
157+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
158+ 'lp_person': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'person'", 'blank': 'True'}),
159+ 'status': ('django.db.models.fields.IntegerField', [], {})
160+ },
161+ 'identityprovider.invalidatedemailaddress': {
162+ 'Meta': {'object_name': 'InvalidatedEmailAddress', 'db_table': "u'invalidated_emailaddress'"},
163+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'account'", 'blank': 'True'}),
164+ 'account_notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
165+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
166+ 'date_invalidated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'null': 'True', 'blank': 'True'}),
167+ 'email': ('django.db.models.fields.TextField', [], {}),
168+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
169+ },
170+ 'identityprovider.leakedcredential': {
171+ 'Meta': {'unique_together': "(('email', 'source'),)", 'object_name': 'LeakedCredential'},
172+ 'date_leaked': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
173+ 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '254'}),
174+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
175+ 'password': ('django.db.models.fields.TextField', [], {'default': 'None'}),
176+ 'source': ('django.db.models.fields.TextField', [], {'default': 'None'})
177+ },
178+ 'identityprovider.lpopenididentifier': {
179+ 'Meta': {'object_name': 'LPOpenIdIdentifier', 'db_table': "u'lp_openididentifier'"},
180+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.date.today'}),
181+ 'identifier': ('django.db.models.fields.TextField', [], {'unique': 'True', 'primary_key': 'True'}),
182+ 'lp_account': ('django.db.models.fields.IntegerField', [], {'db_column': "'account'", 'db_index': 'True'})
183+ },
184+ 'identityprovider.openidassociation': {
185+ 'Meta': {'unique_together': "(('server_url', 'handle'),)", 'object_name': 'OpenIDAssociation', 'db_table': "u'openidassociation'"},
186+ 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
187+ 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
188+ 'issued': ('django.db.models.fields.IntegerField', [], {}),
189+ 'lifetime': ('django.db.models.fields.IntegerField', [], {}),
190+ 'secret': ('django.db.models.fields.TextField', [], {}),
191+ 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'})
192+ },
193+ 'identityprovider.openidauthorization': {
194+ 'Meta': {'object_name': 'OpenIDAuthorization', 'db_table': "u'openidauthorization'"},
195+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}),
196+ 'client_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
197+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
198+ 'date_expires': ('django.db.models.fields.DateTimeField', [], {}),
199+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
200+ 'trust_root': ('django.db.models.fields.TextField', [], {})
201+ },
202+ 'identityprovider.openidnonce': {
203+ 'Meta': {'unique_together': "(('server_url', 'timestamp', 'salt'),)", 'object_name': 'OpenIDNonce', 'db_table': "'openidnonce'"},
204+ 'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
205+ 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047', 'primary_key': 'True'}),
206+ 'timestamp': ('django.db.models.fields.IntegerField', [], {})
207+ },
208+ 'identityprovider.openidrpconfig': {
209+ 'Meta': {'object_name': 'OpenIDRPConfig', 'db_table': "'ssoopenidrpconfig'"},
210+ 'allow_unverified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
211+ 'allowed_ax': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
212+ 'allowed_sreg': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
213+ 'allowed_user_attribs': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
214+ 'auto_authorize': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
215+ 'can_query_any_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
216+ 'creation_rationale': ('django.db.models.fields.IntegerField', [], {'default': '13'}),
217+ 'description': ('django.db.models.fields.TextField', [], {}),
218+ 'displayname': ('django.db.models.fields.TextField', [], {}),
219+ 'flag_twofactor': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
220+ 'ga_snippet': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
221+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
222+ 'logo': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
223+ 'prefer_canonical_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
224+ 'require_two_factor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
225+ 'trust_root': ('django.db.models.fields.TextField', [], {'unique': 'True'})
226+ },
227+ 'identityprovider.openidrpsummary': {
228+ 'Meta': {'unique_together': "(('account', 'trust_root', 'openid_identifier'),)", 'object_name': 'OpenIDRPSummary', 'db_table': "u'openidrpsummary'"},
229+ 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}),
230+ 'approved_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
231+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
232+ 'date_last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
233+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
234+ 'openid_identifier': ('django.db.models.fields.TextField', [], {'db_index': 'True'}),
235+ 'total_logins': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
236+ 'trust_root': ('django.db.models.fields.TextField', [], {'db_index': 'True'})
237+ },
238+ 'identityprovider.person': {
239+ 'Meta': {'object_name': 'Person', 'db_table': "u'lp_person'"},
240+ 'addressline1': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
241+ 'addressline2': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
242+ 'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
243+ 'country': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'country'", 'blank': 'True'}),
244+ 'creation_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
245+ 'creation_rationale': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
246+ 'datecreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
247+ 'defaultmembershipperiod': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
248+ 'defaultrenewalperiod': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
249+ 'displayname': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
250+ 'fti': ('django.db.models.fields.TextField', [], {'null': 'True'}),
251+ 'hide_email_addresses': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
252+ 'homepage_content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
253+ 'icon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'icon'", 'blank': 'True'}),
254+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
255+ 'language': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'language'", 'blank': 'True'}),
256+ 'logo': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'logo'", 'blank': 'True'}),
257+ 'lp_account': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'null': 'True', 'db_column': "'account'"}),
258+ 'mail_resumption_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
259+ 'mailing_list_auto_subscribe_policy': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}),
260+ 'mailing_list_receive_duplicates': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'}),
261+ 'merged': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'merged'", 'blank': 'True'}),
262+ 'mugshot': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'mugshot'", 'blank': 'True'}),
263+ 'name': ('django.db.models.fields.TextField', [], {'unique': 'True', 'null': 'True'}),
264+ 'organization': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
265+ 'personal_standing': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True'}),
266+ 'personal_standing_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
267+ 'phone': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
268+ 'postcode': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
269+ 'province': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
270+ 'registrant': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'registrant'", 'blank': 'True'}),
271+ 'renewal_policy': ('django.db.models.fields.IntegerField', [], {'default': '10', 'null': 'True'}),
272+ 'subscriptionpolicy': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}),
273+ 'teamdescription': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
274+ 'teamowner': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'teamowner'", 'blank': 'True'}),
275+ 'verbose_bugnotifications': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
276+ 'visibility': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'})
277+ },
278+ 'identityprovider.personlocation': {
279+ 'Meta': {'object_name': 'PersonLocation', 'db_table': "u'lp_personlocation'"},
280+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
281+ 'date_last_modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
282+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
283+ 'last_modified_by': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'last_modified_by'"}),
284+ 'latitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
285+ 'locked': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
286+ 'longitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
287+ 'person': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['identityprovider.Person']", 'unique': 'True', 'null': 'True', 'db_column': "'person'"}),
288+ 'time_zone': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
289+ 'visible': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'})
290+ },
291+ 'identityprovider.teamparticipation': {
292+ 'Meta': {'unique_together': "(('team', 'person'),)", 'object_name': 'TeamParticipation', 'db_table': "u'lp_teamparticipation'"},
293+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
294+ 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Person']", 'null': 'True', 'db_column': "'person'"}),
295+ 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'team_participations'", 'null': 'True', 'db_column': "'team'", 'to': "orm['identityprovider.Person']"})
296+ }
297+ }
298+
299+ complete_apps = ['identityprovider']
300\ No newline at end of file
301
302=== modified file 'src/identityprovider/models/account.py'
303--- src/identityprovider/models/account.py 2013-11-21 11:56:12 +0000
304+++ src/identityprovider/models/account.py 2013-11-27 18:08:05 +0000
305@@ -120,7 +120,8 @@
306 email.save()
307 password = AccountPassword.objects.create(
308 account=account,
309- password=password)
310+ password=password,
311+ when_created=datetime.utcnow())
312 return account
313
314 def get_by_email(self, email):
315@@ -166,6 +167,10 @@
316
317 objects = AccountManager.for_queryset_class(AccountQuerySet)()
318
319+ @property
320+ def accountpassword(self):
321+ return self.accountpassword_set.latest()
322+
323 class Meta:
324 app_label = 'identityprovider'
325 db_table = u'account'
326@@ -235,10 +240,14 @@
327 def set_deleted(self):
328 self.status = AccountStatus.DELETED
329 self.emailaddress_set.delete()
330- self.accountpassword.password = 'invalid'
331- self.accountpassword.save()
332 self.save()
333
334+ for old_password in self.accountpassword_set.all()[1:]:
335+ old_password.delete()
336+ latest_password = self.accountpassword
337+ latest_password.password = 'invalid'
338+ latest_password.save()
339+
340 user, _ = User.objects.get_or_create(username=self.openid_identifier)
341 user.is_active = False
342 user.save()
343@@ -407,16 +416,21 @@
344
345 def set_password(self, password, salt=None):
346 validate_password_policy(password, self)
347- try:
348- accountpassword = self.accountpassword
349- except AccountPassword.DoesNotExist:
350- accountpassword = AccountPassword(account=self, password='invalid')
351+ accountpassword = AccountPassword(
352+ account=self, password='invalid', when_created=datetime.utcnow())
353 accountpassword.password = make_password(password, salt=salt)
354 accountpassword.save()
355 # clear the password reset flag
356 self.need_password_reset = False
357 self.save()
358
359+ # clear all oauth tokens as password has changed
360+ self.invalidate_oauth_tokens()
361+
362+ # delete all but the current and four previous
363+ for old_password in self.accountpassword_set.all()[5:]:
364+ old_password.delete()
365+
366 def get_and_delete_messages(self):
367 return []
368
369@@ -523,8 +537,9 @@
370 try:
371 # by setting a plain text value we make sure its not going to
372 # match against anything
373- self.accountpassword.password = 'invalid'
374- self.accountpassword.save()
375+ accountpassword = self.accountpassword
376+ accountpassword.password = 'invalid'
377+ accountpassword.save()
378 except AccountPassword.DoesNotExist:
379 # no password, so nothing to reset
380 pass
381@@ -557,14 +572,17 @@
382
383
384 class AccountPassword(models.Model):
385- account = models.OneToOneField(Account, db_column='account')
386+ account = models.ForeignKey(Account, db_column='account')
387 password = PasswordField()
388+ when_created = models.DateTimeField(default=datetime.now)
389
390 class Meta:
391 app_label = 'identityprovider'
392 db_table = u'accountpassword'
393 verbose_name = _('account password')
394 verbose_name_plural = _('account passwords')
395+ ordering = ['-when_created']
396+ get_latest_by = 'when_created'
397
398 def __unicode__(self):
399 return _("Password for %s") % unicode(self.account)
400
401=== modified file 'src/identityprovider/schema.py'
402--- src/identityprovider/schema.py 2013-11-12 13:44:06 +0000
403+++ src/identityprovider/schema.py 2013-11-27 18:08:05 +0000
404@@ -172,6 +172,7 @@
405 max_password_reset_tokens = IntOption(default=5)
406 combo_url = StringOption(default="/combo/")
407 combine = BoolOption(default=False)
408+ pci_compliance_group = StringOption(default="pci-compliance")
409
410 class static_urls(Section):
411 support_form_url = StringOption()
412
413=== modified file 'src/identityprovider/signals.py'
414--- src/identityprovider/signals.py 2013-05-16 13:24:00 +0000
415+++ src/identityprovider/signals.py 2013-11-27 18:08:05 +0000
416@@ -4,13 +4,12 @@
417 from django.dispatch import Signal
418 from django.conf import settings
419 from django.contrib.auth.signals import user_logged_in
420-from django.db.models.signals import post_save
421
422 from oauth.oauth import OAuthRequest
423 from oauth_backend.models import Token
424
425 from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME
426-from identityprovider.models import Account, AccountPassword
427+from identityprovider.models import Account
428 from identityprovider.utils import http_request_with_timeout
429
430
431@@ -39,17 +38,6 @@
432 application_token_invalidated.connect(account_change_notify)
433
434
435-def invalidate_account_oauth_tokens(sender, instance, created, **kwargs):
436- if not created:
437- # invalidate oauth tokens on password change
438- instance.account.invalidate_oauth_tokens()
439-
440-
441-post_save.connect(
442- invalidate_account_oauth_tokens, sender=AccountPassword,
443- dispatch_uid='identityprovider.AccountPassword.post_save')
444-
445-
446 def set_session_oauth_token(sender, user, request, **kwargs):
447 # user is an Account instance here
448
449
450=== modified file 'src/identityprovider/templates/nexus/admin/password_change_form.html'
451--- src/identityprovider/templates/nexus/admin/password_change_form.html 2013-05-15 13:39:40 +0000
452+++ src/identityprovider/templates/nexus/admin/password_change_form.html 2013-11-27 18:08:05 +0000
453@@ -45,7 +45,6 @@
454 <div class="submit-row">
455 <input type="submit" value="{% trans 'Change my password' %}" class="default" />
456 </div>
457-
458 <script type="text/javascript">document.getElementById("id_old_password").focus();</script>
459 </div>
460 </form>
461
462=== modified file 'src/identityprovider/tests/test_admin.py'
463--- src/identityprovider/tests/test_admin.py 2013-10-22 19:23:13 +0000
464+++ src/identityprovider/tests/test_admin.py 2013-11-27 18:08:05 +0000
465@@ -123,10 +123,10 @@
466 'status': str(self.account.status),
467 'displayname': self.account.displayname,
468 'openid_identifier': self.account.openid_identifier,
469- 'accountpassword-TOTAL_FORMS': '1',
470- 'accountpassword-INITIAL_FORMS': '1',
471- 'accountpassword-0-id': str(account_id),
472- 'accountpassword-0-account': str(account_id),
473+ 'accountpassword_set-TOTAL_FORMS': '1',
474+ 'accountpassword_set-INITIAL_FORMS': '1',
475+ 'accountpassword_set-0-id': str(account_id),
476+ 'accountpassword_set-0-account': str(account_id),
477 'emailaddress_set-TOTAL_FORMS': '1',
478 'emailaddress_set-INITIAL_FORMS': '1',
479 'emailaddress_set-0-id': str(email.id),
480@@ -150,7 +150,7 @@
481 new_password = 'blah'
482 parameters = {
483 'emailaddress_set-0-email': new_email,
484- 'accountpassword-0-password': new_password,
485+ 'accountpassword_set-0-password': new_password,
486 }
487 self.post_account_change(**parameters)
488
489@@ -244,8 +244,8 @@
490 self.assertEqual(r.status_code, 200)
491 self.assertContains(r,
492 '<input type="password" '
493- 'name="accountpassword-0-password" '
494- 'id="id_accountpassword-0-password">',
495+ 'name="accountpassword_set-0-password" '
496+ 'id="id_accountpassword_set-0-password">',
497 html=True)
498
499 def test_add_api_user(self):
500
501=== modified file 'src/identityprovider/tests/test_models_account.py'
502--- src/identityprovider/tests/test_models_account.py 2013-11-21 13:15:42 +0000
503+++ src/identityprovider/tests/test_models_account.py 2013-11-27 18:08:05 +0000
504@@ -425,6 +425,13 @@
505 self.assertNotEqual(account.accountpassword.password,
506 original_password)
507
508+ def test_password_change_deletes_tokns(self):
509+ account = self.factory.make_account()
510+ self.factory.make_oauth_token(account=account)
511+
512+ account.set_password('fooBar123')
513+ self.assertEqual(account.oauth_tokens().count(), 0)
514+
515 def test_sets_password_with_django_cypher(self):
516 password = 'Some password from 2013'
517 account = self.factory.make_account()
518
519=== modified file 'src/identityprovider/tests/test_signals.py'
520--- src/identityprovider/tests/test_signals.py 2013-08-16 15:12:35 +0000
521+++ src/identityprovider/tests/test_signals.py 2013-11-27 18:08:05 +0000
522@@ -5,15 +5,12 @@
523
524 from django.contrib.auth.signals import user_logged_in
525 from django.core.urlresolvers import reverse
526-from django.db.models.signals import post_save
527
528 from oauth_backend.models import Token
529
530 from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME
531-from identityprovider.signals import (
532- invalidate_account_oauth_tokens,
533- set_session_oauth_token,
534-)
535+from identityprovider.signals import set_session_oauth_token
536+
537 from identityprovider.readonly import ReadOnlyManager
538 from identityprovider.tests import DEFAULT_USER_PASSWORD
539 from identityprovider.tests.utils import (
540@@ -77,18 +74,3 @@
541 self.client.login(username=self.email, password=DEFAULT_USER_PASSWORD)
542 self.assertEqual(
543 self.client.session.get(SESSION_TOKEN_KEY), token.token)
544-
545-
546-class InvalidateOauthTokenOnPasswordChangeTestCase(SSOBaseTestCase):
547-
548- def test_signal_connected(self):
549- # r[1] is a weak ref
550- registered_functions = [r[1]() for r in post_save.receivers]
551- self.assertIn(invalidate_account_oauth_tokens, registered_functions)
552-
553- def test_listener_on_password_change(self):
554- account = self.factory.make_account()
555- self.factory.make_oauth_token(account=account)
556-
557- account.set_password('fooBar123')
558- self.assertEqual(account.oauth_tokens().count(), 0)
559
560=== modified file 'src/identityprovider/validators.py'
561--- src/identityprovider/validators.py 2013-09-12 14:45:21 +0000
562+++ src/identityprovider/validators.py 2013-11-27 18:08:05 +0000
563@@ -1,6 +1,7 @@
564 from contextlib import contextmanager
565 import re
566
567+from django.conf import settings
568 from django.core.exceptions import ValidationError
569 from django.utils.translation import ugettext_lazy as _
570
571@@ -11,6 +12,10 @@
572 "Password must consist of lower- and upper-case characters, and at "
573 "least one digit."
574 )
575+PCI_PASSWORD_POLICY_ERROR = _(
576+ "Your password matches a previous passowrd. PCI compliance rules "
577+ "forbid the reuse of recent passwords."
578+)
579
580
581 class BasicValidator(object):
582@@ -88,7 +93,37 @@
583 raise ValidationError(CANONICAL_PASSWORD_POLICY_ERROR)
584
585
586-_validators = [CanonicalValidator(), BasicValidator()]
587+class PCIValidator(BasicValidator):
588+ """Apply PCI compliance password rules"""
589+
590+ def match(self, account):
591+ """Return True if the account is in the 'PCI' team."""
592+ # store the account
593+ self.account = account
594+ return (account is not None and
595+ account.person_in_team(settings.PCI_COMLIANCE_GROUP))
596+
597+ def validate(self, password):
598+ """Check the new password doesn't match any of the last four
599+ passwords."""
600+ super(CanonicalValidator, self).validate(password)
601+
602+ # import here to avoid circular imports
603+ from identityprovider.auth import LaunchpadBackend
604+
605+ account = self.account
606+ backend = LaunchpadBackend()
607+ for accountpassword in account.accountpassword_set.all()[1:]:
608+ try:
609+ backend._validate_raw_password(
610+ account.accountpassword, password, update=False)
611+ except ValidationError:
612+ pass
613+ else:
614+ raise ValidationError(PCI_PASSWORD_POLICY_ERROR)
615+
616+
617+_validators = [CanonicalValidator(), BasicValidator(), PCIValidator()]
618
619
620 def validate_password_policy(password, account=None):