Merge lp:~mfoord/canonical-identity-provider/keep-passwords into lp:canonical-identity-provider/release
- keep-passwords
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical ISD hackers | Pending | ||
Review via email: mp+196209@code.launchpad.net |
Commit message
Description of the change
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 : | # |
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): |
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!