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
=== modified file 'src/identityprovider/admin.py'
--- src/identityprovider/admin.py 2013-11-26 15:40:55 +0000
+++ src/identityprovider/admin.py 2013-11-27 18:08:05 +0000
@@ -1,6 +1,8 @@
1# Copyright 2010,2013 Canonical Ltd. This software is licensed under the1# Copyright 2010,2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from datetime import datetime
5
4from django import forms6from django import forms
5from django.contrib import admin7from django.contrib import admin
6from django.contrib.auth.admin import UserAdmin8from django.contrib.auth.admin import UserAdmin
@@ -134,6 +136,7 @@
134136
135 class Meta:137 class Meta:
136 model = AccountPassword138 model = AccountPassword
139 exclude = ['when_created']
137140
138 def __init__(self, *args, **kwargs):141 def __init__(self, *args, **kwargs):
139 initial = kwargs.get('initial', {})142 initial = kwargs.get('initial', {})
@@ -143,6 +146,7 @@
143146
144147
145class AccountPasswordFormset(forms.models.BaseInlineFormSet):148class AccountPasswordFormset(forms.models.BaseInlineFormSet):
149
146 def __init__(self, *args, **kwargs):150 def __init__(self, *args, **kwargs):
147 super(AccountPasswordFormset, self).__init__(*args, **kwargs)151 super(AccountPasswordFormset, self).__init__(*args, **kwargs)
148 self.can_delete = False152 self.can_delete = False
@@ -260,6 +264,7 @@
260 instance = instances[0]264 instance = instances[0]
261 encrypted_password = make_password(password)265 encrypted_password = make_password(password)
262 instance.password = encrypted_password266 instance.password = encrypted_password
267 instance.when_created = datetime.utcnow()
263 instance.save()268 instance.save()
264 else:269 else:
265 super(AccountAdmin, self).save_formset(270 super(AccountAdmin, self).save_formset(
266271
=== modified file 'src/identityprovider/management/commands/populate.py'
--- src/identityprovider/management/commands/populate.py 2013-11-19 18:45:46 +0000
+++ src/identityprovider/management/commands/populate.py 2013-11-27 18:08:05 +0000
@@ -167,14 +167,15 @@
167 def gen_accountpassword(self, id):167 def gen_accountpassword(self, id):
168 """Return a sequence representing an AccountPassword.168 """Return a sequence representing an AccountPassword.
169169
170 Currently (id, account, password)170 Currently (id, account, password, when_created)
171 """171 """
172 account_id = (id - self.ranges['accountpassword'][0] +172 account_id = (id - self.ranges['accountpassword'][0] +
173 self.ranges['account'][0])173 self.ranges['account'][0])
174 plaintext = self.random_string(size=8)174 plaintext = self.random_string(size=8)
175 self.passwords.append(plaintext)175 self.passwords.append(plaintext)
176 password = make_password(plaintext)176 password = make_password(plaintext)
177 return [str(id), str(account_id), password]177 when_created = self.random_date()
178 return [str(id), str(account_id), password, when_created]
178179
179 def gen_lp_openididentifier(self, id):180 def gen_lp_openididentifier(self, id):
180 """ Returns a sequence representing an LPOpenIdIdentfier.181 """ Returns a sequence representing an LPOpenIdIdentfier.
181182
=== added file 'src/identityprovider/migrations/0017_keep_passwords.py'
--- src/identityprovider/migrations/0017_keep_passwords.py 1970-01-01 00:00:00 +0000
+++ src/identityprovider/migrations/0017_keep_passwords.py 2013-11-27 18:08:05 +0000
@@ -0,0 +1,236 @@
1# -*- coding: utf-8 -*-
2import datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Removing unique constraint on 'AccountPassword', fields ['account']
12 db.delete_unique(u'accountpassword', ['account'])
13
14 # Adding field 'AccountPassword.when_created'
15 db.add_column(u'accountpassword', 'when_created',
16 self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now),
17 keep_default=False)
18
19
20 # Changing field 'AccountPassword.account'
21 db.alter_column(u'accountpassword', 'account', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['identityprovider.Account'], db_column='account'))
22
23 def backwards(self, orm):
24 # Deleting field 'AccountPassword.when_created'
25 db.delete_column(u'accountpassword', 'when_created')
26
27
28 # Changing field 'AccountPassword.account'
29 db.alter_column(u'accountpassword', 'account', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['identityprovider.Account'], unique=True, db_column='account'))
30 # Adding unique constraint on 'AccountPassword', fields ['account']
31 db.create_unique(u'accountpassword', ['account'])
32
33
34 models = {
35 'identityprovider.account': {
36 'Meta': {'object_name': 'Account', 'db_table': "u'account'"},
37 'creation_rationale': ('django.db.models.fields.IntegerField', [], {}),
38 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}),
39 'date_status_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}),
40 'displayname': ('identityprovider.models.account.DisplaynameField', [], {}),
41 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42 'old_openid_identifier': ('django.db.models.fields.TextField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
43 'openid_identifier': ('django.db.models.fields.TextField', [], {'default': "u'XrCnJdc'", 'unique': 'True'}),
44 'preferredlanguage': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
45 'status': ('django.db.models.fields.IntegerField', [], {}),
46 'status_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
47 'twofactor_attempts': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
48 'twofactor_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
49 'warn_about_backup_device': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
50 },
51 'identityprovider.accountpassword': {
52 'Meta': {'ordering': "['-when_created']", 'object_name': 'AccountPassword', 'db_table': "u'accountpassword'"},
53 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}),
54 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55 'password': ('identityprovider.models.account.PasswordField', [], {}),
56 'when_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
57 },
58 'identityprovider.apiuser': {
59 'Meta': {'object_name': 'APIUser', 'db_table': "'api_user'"},
60 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
61 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
63 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
64 'username': ('django.db.models.fields.CharField', [], {'max_length': '256'})
65 },
66 'identityprovider.authenticationdevice': {
67 'Meta': {'ordering': "('id',)", 'object_name': 'AuthenticationDevice'},
68 'account': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'devices'", 'to': "orm['identityprovider.Account']"}),
69 'counter': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
70 'device_type': ('django.db.models.fields.TextField', [], {'null': 'True'}),
71 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
72 'key': ('django.db.models.fields.TextField', [], {}),
73 'name': ('django.db.models.fields.TextField', [], {})
74 },
75 'identityprovider.authtoken': {
76 'Meta': {'object_name': 'AuthToken', 'db_table': "u'authtoken'"},
77 'date_consumed': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
78 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'db_index': 'True', 'blank': 'True'}),
79 'displayname': ('identityprovider.models.account.DisplaynameField', [], {'null': 'True', 'blank': 'True'}),
80 'email': ('django.db.models.fields.TextField', [], {'db_index': 'True'}),
81 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82 'password': ('identityprovider.models.account.PasswordField', [], {'null': 'True', 'blank': 'True'}),
83 'redirection_url': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
84 'requester': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'requester'", 'blank': 'True'}),
85 'requester_email': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
86 'token': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
87 'token_type': ('django.db.models.fields.IntegerField', [], {})
88 },
89 'identityprovider.emailaddress': {
90 'Meta': {'object_name': 'EmailAddress', 'db_table': "u'emailaddress'"},
91 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'account'", 'blank': 'True'}),
92 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
93 'email': ('django.db.models.fields.TextField', [], {}),
94 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95 'lp_person': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'person'", 'blank': 'True'}),
96 'status': ('django.db.models.fields.IntegerField', [], {})
97 },
98 'identityprovider.invalidatedemailaddress': {
99 'Meta': {'object_name': 'InvalidatedEmailAddress', 'db_table': "u'invalidated_emailaddress'"},
100 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'null': 'True', 'db_column': "'account'", 'blank': 'True'}),
101 'account_notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
102 'date_created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
103 'date_invalidated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'null': 'True', 'blank': 'True'}),
104 'email': ('django.db.models.fields.TextField', [], {}),
105 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
106 },
107 'identityprovider.leakedcredential': {
108 'Meta': {'unique_together': "(('email', 'source'),)", 'object_name': 'LeakedCredential'},
109 'date_leaked': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
110 'email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '254'}),
111 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112 'password': ('django.db.models.fields.TextField', [], {'default': 'None'}),
113 'source': ('django.db.models.fields.TextField', [], {'default': 'None'})
114 },
115 'identityprovider.lpopenididentifier': {
116 'Meta': {'object_name': 'LPOpenIdIdentifier', 'db_table': "u'lp_openididentifier'"},
117 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.date.today'}),
118 'identifier': ('django.db.models.fields.TextField', [], {'unique': 'True', 'primary_key': 'True'}),
119 'lp_account': ('django.db.models.fields.IntegerField', [], {'db_column': "'account'", 'db_index': 'True'})
120 },
121 'identityprovider.openidassociation': {
122 'Meta': {'unique_together': "(('server_url', 'handle'),)", 'object_name': 'OpenIDAssociation', 'db_table': "u'openidassociation'"},
123 'assoc_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
124 'handle': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
125 'issued': ('django.db.models.fields.IntegerField', [], {}),
126 'lifetime': ('django.db.models.fields.IntegerField', [], {}),
127 'secret': ('django.db.models.fields.TextField', [], {}),
128 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'})
129 },
130 'identityprovider.openidauthorization': {
131 'Meta': {'object_name': 'OpenIDAuthorization', 'db_table': "u'openidauthorization'"},
132 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}),
133 'client_id': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
134 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
135 'date_expires': ('django.db.models.fields.DateTimeField', [], {}),
136 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
137 'trust_root': ('django.db.models.fields.TextField', [], {})
138 },
139 'identityprovider.openidnonce': {
140 'Meta': {'unique_together': "(('server_url', 'timestamp', 'salt'),)", 'object_name': 'OpenIDNonce', 'db_table': "'openidnonce'"},
141 'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
142 'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047', 'primary_key': 'True'}),
143 'timestamp': ('django.db.models.fields.IntegerField', [], {})
144 },
145 'identityprovider.openidrpconfig': {
146 'Meta': {'object_name': 'OpenIDRPConfig', 'db_table': "'ssoopenidrpconfig'"},
147 'allow_unverified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
148 'allowed_ax': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
149 'allowed_sreg': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
150 'allowed_user_attribs': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
151 'auto_authorize': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
152 'can_query_any_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
153 'creation_rationale': ('django.db.models.fields.IntegerField', [], {'default': '13'}),
154 'description': ('django.db.models.fields.TextField', [], {}),
155 'displayname': ('django.db.models.fields.TextField', [], {}),
156 'flag_twofactor': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
157 'ga_snippet': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
158 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
159 'logo': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
160 'prefer_canonical_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
161 'require_two_factor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
162 'trust_root': ('django.db.models.fields.TextField', [], {'unique': 'True'})
163 },
164 'identityprovider.openidrpsummary': {
165 'Meta': {'unique_together': "(('account', 'trust_root', 'openid_identifier'),)", 'object_name': 'OpenIDRPSummary', 'db_table': "u'openidrpsummary'"},
166 'account': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Account']", 'db_column': "'account'"}),
167 'approved_data': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
168 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
169 'date_last_used': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'blank': 'True'}),
170 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
171 'openid_identifier': ('django.db.models.fields.TextField', [], {'db_index': 'True'}),
172 'total_logins': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
173 'trust_root': ('django.db.models.fields.TextField', [], {'db_index': 'True'})
174 },
175 'identityprovider.person': {
176 'Meta': {'object_name': 'Person', 'db_table': "u'lp_person'"},
177 'addressline1': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
178 'addressline2': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
179 'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
180 'country': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'country'", 'blank': 'True'}),
181 'creation_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
182 'creation_rationale': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
183 'datecreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
184 'defaultmembershipperiod': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
185 'defaultrenewalperiod': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
186 'displayname': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
187 'fti': ('django.db.models.fields.TextField', [], {'null': 'True'}),
188 'hide_email_addresses': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
189 'homepage_content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
190 'icon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'icon'", 'blank': 'True'}),
191 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
192 'language': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'language'", 'blank': 'True'}),
193 'logo': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'logo'", 'blank': 'True'}),
194 'lp_account': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'null': 'True', 'db_column': "'account'"}),
195 'mail_resumption_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
196 'mailing_list_auto_subscribe_policy': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}),
197 'mailing_list_receive_duplicates': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'}),
198 'merged': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'merged'", 'blank': 'True'}),
199 'mugshot': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'mugshot'", 'blank': 'True'}),
200 'name': ('django.db.models.fields.TextField', [], {'unique': 'True', 'null': 'True'}),
201 'organization': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
202 'personal_standing': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True'}),
203 'personal_standing_reason': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
204 'phone': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
205 'postcode': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
206 'province': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
207 'registrant': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'registrant'", 'blank': 'True'}),
208 'renewal_policy': ('django.db.models.fields.IntegerField', [], {'default': '10', 'null': 'True'}),
209 'subscriptionpolicy': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'}),
210 'teamdescription': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
211 'teamowner': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'teamowner'", 'blank': 'True'}),
212 'verbose_bugnotifications': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
213 'visibility': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True'})
214 },
215 'identityprovider.personlocation': {
216 'Meta': {'object_name': 'PersonLocation', 'db_table': "u'lp_personlocation'"},
217 'date_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
218 'date_last_modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
219 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
220 'last_modified_by': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_column': "'last_modified_by'"}),
221 'latitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
222 'locked': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
223 'longitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
224 'person': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['identityprovider.Person']", 'unique': 'True', 'null': 'True', 'db_column': "'person'"}),
225 'time_zone': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
226 'visible': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'})
227 },
228 'identityprovider.teamparticipation': {
229 'Meta': {'unique_together': "(('team', 'person'),)", 'object_name': 'TeamParticipation', 'db_table': "u'lp_teamparticipation'"},
230 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
231 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['identityprovider.Person']", 'null': 'True', 'db_column': "'person'"}),
232 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'team_participations'", 'null': 'True', 'db_column': "'team'", 'to': "orm['identityprovider.Person']"})
233 }
234 }
235
236 complete_apps = ['identityprovider']
0\ No newline at end of file237\ No newline at end of file
1238
=== modified file 'src/identityprovider/models/account.py'
--- src/identityprovider/models/account.py 2013-11-21 11:56:12 +0000
+++ src/identityprovider/models/account.py 2013-11-27 18:08:05 +0000
@@ -120,7 +120,8 @@
120 email.save()120 email.save()
121 password = AccountPassword.objects.create(121 password = AccountPassword.objects.create(
122 account=account,122 account=account,
123 password=password)123 password=password,
124 when_created=datetime.utcnow())
124 return account125 return account
125126
126 def get_by_email(self, email):127 def get_by_email(self, email):
@@ -166,6 +167,10 @@
166167
167 objects = AccountManager.for_queryset_class(AccountQuerySet)()168 objects = AccountManager.for_queryset_class(AccountQuerySet)()
168169
170 @property
171 def accountpassword(self):
172 return self.accountpassword_set.latest()
173
169 class Meta:174 class Meta:
170 app_label = 'identityprovider'175 app_label = 'identityprovider'
171 db_table = u'account'176 db_table = u'account'
@@ -235,10 +240,14 @@
235 def set_deleted(self):240 def set_deleted(self):
236 self.status = AccountStatus.DELETED241 self.status = AccountStatus.DELETED
237 self.emailaddress_set.delete()242 self.emailaddress_set.delete()
238 self.accountpassword.password = 'invalid'
239 self.accountpassword.save()
240 self.save()243 self.save()
241244
245 for old_password in self.accountpassword_set.all()[1:]:
246 old_password.delete()
247 latest_password = self.accountpassword
248 latest_password.password = 'invalid'
249 latest_password.save()
250
242 user, _ = User.objects.get_or_create(username=self.openid_identifier)251 user, _ = User.objects.get_or_create(username=self.openid_identifier)
243 user.is_active = False252 user.is_active = False
244 user.save()253 user.save()
@@ -407,16 +416,21 @@
407416
408 def set_password(self, password, salt=None):417 def set_password(self, password, salt=None):
409 validate_password_policy(password, self)418 validate_password_policy(password, self)
410 try:419 accountpassword = AccountPassword(
411 accountpassword = self.accountpassword420 account=self, password='invalid', when_created=datetime.utcnow())
412 except AccountPassword.DoesNotExist:
413 accountpassword = AccountPassword(account=self, password='invalid')
414 accountpassword.password = make_password(password, salt=salt)421 accountpassword.password = make_password(password, salt=salt)
415 accountpassword.save()422 accountpassword.save()
416 # clear the password reset flag423 # clear the password reset flag
417 self.need_password_reset = False424 self.need_password_reset = False
418 self.save()425 self.save()
419426
427 # clear all oauth tokens as password has changed
428 self.invalidate_oauth_tokens()
429
430 # delete all but the current and four previous
431 for old_password in self.accountpassword_set.all()[5:]:
432 old_password.delete()
433
420 def get_and_delete_messages(self):434 def get_and_delete_messages(self):
421 return []435 return []
422436
@@ -523,8 +537,9 @@
523 try:537 try:
524 # by setting a plain text value we make sure its not going to538 # by setting a plain text value we make sure its not going to
525 # match against anything539 # match against anything
526 self.accountpassword.password = 'invalid'540 accountpassword = self.accountpassword
527 self.accountpassword.save()541 accountpassword.password = 'invalid'
542 accountpassword.save()
528 except AccountPassword.DoesNotExist:543 except AccountPassword.DoesNotExist:
529 # no password, so nothing to reset544 # no password, so nothing to reset
530 pass545 pass
@@ -557,14 +572,17 @@
557572
558573
559class AccountPassword(models.Model):574class AccountPassword(models.Model):
560 account = models.OneToOneField(Account, db_column='account')575 account = models.ForeignKey(Account, db_column='account')
561 password = PasswordField()576 password = PasswordField()
577 when_created = models.DateTimeField(default=datetime.now)
562578
563 class Meta:579 class Meta:
564 app_label = 'identityprovider'580 app_label = 'identityprovider'
565 db_table = u'accountpassword'581 db_table = u'accountpassword'
566 verbose_name = _('account password')582 verbose_name = _('account password')
567 verbose_name_plural = _('account passwords')583 verbose_name_plural = _('account passwords')
584 ordering = ['-when_created']
585 get_latest_by = 'when_created'
568586
569 def __unicode__(self):587 def __unicode__(self):
570 return _("Password for %s") % unicode(self.account)588 return _("Password for %s") % unicode(self.account)
571589
=== modified file 'src/identityprovider/schema.py'
--- src/identityprovider/schema.py 2013-11-12 13:44:06 +0000
+++ src/identityprovider/schema.py 2013-11-27 18:08:05 +0000
@@ -172,6 +172,7 @@
172 max_password_reset_tokens = IntOption(default=5)172 max_password_reset_tokens = IntOption(default=5)
173 combo_url = StringOption(default="/combo/")173 combo_url = StringOption(default="/combo/")
174 combine = BoolOption(default=False)174 combine = BoolOption(default=False)
175 pci_compliance_group = StringOption(default="pci-compliance")
175176
176 class static_urls(Section):177 class static_urls(Section):
177 support_form_url = StringOption()178 support_form_url = StringOption()
178179
=== modified file 'src/identityprovider/signals.py'
--- src/identityprovider/signals.py 2013-05-16 13:24:00 +0000
+++ src/identityprovider/signals.py 2013-11-27 18:08:05 +0000
@@ -4,13 +4,12 @@
4from django.dispatch import Signal4from django.dispatch import Signal
5from django.conf import settings5from django.conf import settings
6from django.contrib.auth.signals import user_logged_in6from django.contrib.auth.signals import user_logged_in
7from django.db.models.signals import post_save
87
9from oauth.oauth import OAuthRequest8from oauth.oauth import OAuthRequest
10from oauth_backend.models import Token9from oauth_backend.models import Token
1110
12from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME11from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME
13from identityprovider.models import Account, AccountPassword12from identityprovider.models import Account
14from identityprovider.utils import http_request_with_timeout13from identityprovider.utils import http_request_with_timeout
1514
1615
@@ -39,17 +38,6 @@
39application_token_invalidated.connect(account_change_notify)38application_token_invalidated.connect(account_change_notify)
4039
4140
42def invalidate_account_oauth_tokens(sender, instance, created, **kwargs):
43 if not created:
44 # invalidate oauth tokens on password change
45 instance.account.invalidate_oauth_tokens()
46
47
48post_save.connect(
49 invalidate_account_oauth_tokens, sender=AccountPassword,
50 dispatch_uid='identityprovider.AccountPassword.post_save')
51
52
53def set_session_oauth_token(sender, user, request, **kwargs):41def set_session_oauth_token(sender, user, request, **kwargs):
54 # user is an Account instance here42 # user is an Account instance here
5543
5644
=== modified file 'src/identityprovider/templates/nexus/admin/password_change_form.html'
--- src/identityprovider/templates/nexus/admin/password_change_form.html 2013-05-15 13:39:40 +0000
+++ src/identityprovider/templates/nexus/admin/password_change_form.html 2013-11-27 18:08:05 +0000
@@ -45,7 +45,6 @@
45 <div class="submit-row">45 <div class="submit-row">
46 <input type="submit" value="{% trans 'Change my password' %}" class="default" />46 <input type="submit" value="{% trans 'Change my password' %}" class="default" />
47 </div>47 </div>
48
49 <script type="text/javascript">document.getElementById("id_old_password").focus();</script>48 <script type="text/javascript">document.getElementById("id_old_password").focus();</script>
50 </div>49 </div>
51 </form>50 </form>
5251
=== modified file 'src/identityprovider/tests/test_admin.py'
--- src/identityprovider/tests/test_admin.py 2013-10-22 19:23:13 +0000
+++ src/identityprovider/tests/test_admin.py 2013-11-27 18:08:05 +0000
@@ -123,10 +123,10 @@
123 'status': str(self.account.status),123 'status': str(self.account.status),
124 'displayname': self.account.displayname,124 'displayname': self.account.displayname,
125 'openid_identifier': self.account.openid_identifier,125 'openid_identifier': self.account.openid_identifier,
126 'accountpassword-TOTAL_FORMS': '1',126 'accountpassword_set-TOTAL_FORMS': '1',
127 'accountpassword-INITIAL_FORMS': '1',127 'accountpassword_set-INITIAL_FORMS': '1',
128 'accountpassword-0-id': str(account_id),128 'accountpassword_set-0-id': str(account_id),
129 'accountpassword-0-account': str(account_id),129 'accountpassword_set-0-account': str(account_id),
130 'emailaddress_set-TOTAL_FORMS': '1',130 'emailaddress_set-TOTAL_FORMS': '1',
131 'emailaddress_set-INITIAL_FORMS': '1',131 'emailaddress_set-INITIAL_FORMS': '1',
132 'emailaddress_set-0-id': str(email.id),132 'emailaddress_set-0-id': str(email.id),
@@ -150,7 +150,7 @@
150 new_password = 'blah'150 new_password = 'blah'
151 parameters = {151 parameters = {
152 'emailaddress_set-0-email': new_email,152 'emailaddress_set-0-email': new_email,
153 'accountpassword-0-password': new_password,153 'accountpassword_set-0-password': new_password,
154 }154 }
155 self.post_account_change(**parameters)155 self.post_account_change(**parameters)
156156
@@ -244,8 +244,8 @@
244 self.assertEqual(r.status_code, 200)244 self.assertEqual(r.status_code, 200)
245 self.assertContains(r,245 self.assertContains(r,
246 '<input type="password" '246 '<input type="password" '
247 'name="accountpassword-0-password" '247 'name="accountpassword_set-0-password" '
248 'id="id_accountpassword-0-password">',248 'id="id_accountpassword_set-0-password">',
249 html=True)249 html=True)
250250
251 def test_add_api_user(self):251 def test_add_api_user(self):
252252
=== modified file 'src/identityprovider/tests/test_models_account.py'
--- src/identityprovider/tests/test_models_account.py 2013-11-21 13:15:42 +0000
+++ src/identityprovider/tests/test_models_account.py 2013-11-27 18:08:05 +0000
@@ -425,6 +425,13 @@
425 self.assertNotEqual(account.accountpassword.password,425 self.assertNotEqual(account.accountpassword.password,
426 original_password)426 original_password)
427427
428 def test_password_change_deletes_tokns(self):
429 account = self.factory.make_account()
430 self.factory.make_oauth_token(account=account)
431
432 account.set_password('fooBar123')
433 self.assertEqual(account.oauth_tokens().count(), 0)
434
428 def test_sets_password_with_django_cypher(self):435 def test_sets_password_with_django_cypher(self):
429 password = 'Some password from 2013'436 password = 'Some password from 2013'
430 account = self.factory.make_account()437 account = self.factory.make_account()
431438
=== modified file 'src/identityprovider/tests/test_signals.py'
--- src/identityprovider/tests/test_signals.py 2013-08-16 15:12:35 +0000
+++ src/identityprovider/tests/test_signals.py 2013-11-27 18:08:05 +0000
@@ -5,15 +5,12 @@
55
6from django.contrib.auth.signals import user_logged_in6from django.contrib.auth.signals import user_logged_in
7from django.core.urlresolvers import reverse7from django.core.urlresolvers import reverse
8from django.db.models.signals import post_save
98
10from oauth_backend.models import Token9from oauth_backend.models import Token
1110
12from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME11from identityprovider.const import SESSION_TOKEN_KEY, SESSION_TOKEN_NAME
13from identityprovider.signals import (12from identityprovider.signals import set_session_oauth_token
14 invalidate_account_oauth_tokens,13
15 set_session_oauth_token,
16)
17from identityprovider.readonly import ReadOnlyManager14from identityprovider.readonly import ReadOnlyManager
18from identityprovider.tests import DEFAULT_USER_PASSWORD15from identityprovider.tests import DEFAULT_USER_PASSWORD
19from identityprovider.tests.utils import (16from identityprovider.tests.utils import (
@@ -77,18 +74,3 @@
77 self.client.login(username=self.email, password=DEFAULT_USER_PASSWORD)74 self.client.login(username=self.email, password=DEFAULT_USER_PASSWORD)
78 self.assertEqual(75 self.assertEqual(
79 self.client.session.get(SESSION_TOKEN_KEY), token.token)76 self.client.session.get(SESSION_TOKEN_KEY), token.token)
80
81
82class InvalidateOauthTokenOnPasswordChangeTestCase(SSOBaseTestCase):
83
84 def test_signal_connected(self):
85 # r[1] is a weak ref
86 registered_functions = [r[1]() for r in post_save.receivers]
87 self.assertIn(invalidate_account_oauth_tokens, registered_functions)
88
89 def test_listener_on_password_change(self):
90 account = self.factory.make_account()
91 self.factory.make_oauth_token(account=account)
92
93 account.set_password('fooBar123')
94 self.assertEqual(account.oauth_tokens().count(), 0)
9577
=== modified file 'src/identityprovider/validators.py'
--- src/identityprovider/validators.py 2013-09-12 14:45:21 +0000
+++ src/identityprovider/validators.py 2013-11-27 18:08:05 +0000
@@ -1,6 +1,7 @@
1from contextlib import contextmanager1from contextlib import contextmanager
2import re2import re
33
4from django.conf import settings
4from django.core.exceptions import ValidationError5from django.core.exceptions import ValidationError
5from django.utils.translation import ugettext_lazy as _6from django.utils.translation import ugettext_lazy as _
67
@@ -11,6 +12,10 @@
11 "Password must consist of lower- and upper-case characters, and at "12 "Password must consist of lower- and upper-case characters, and at "
12 "least one digit."13 "least one digit."
13)14)
15PCI_PASSWORD_POLICY_ERROR = _(
16 "Your password matches a previous passowrd. PCI compliance rules "
17 "forbid the reuse of recent passwords."
18)
1419
1520
16class BasicValidator(object):21class BasicValidator(object):
@@ -88,7 +93,37 @@
88 raise ValidationError(CANONICAL_PASSWORD_POLICY_ERROR)93 raise ValidationError(CANONICAL_PASSWORD_POLICY_ERROR)
8994
9095
91_validators = [CanonicalValidator(), BasicValidator()]96class PCIValidator(BasicValidator):
97 """Apply PCI compliance password rules"""
98
99 def match(self, account):
100 """Return True if the account is in the 'PCI' team."""
101 # store the account
102 self.account = account
103 return (account is not None and
104 account.person_in_team(settings.PCI_COMLIANCE_GROUP))
105
106 def validate(self, password):
107 """Check the new password doesn't match any of the last four
108 passwords."""
109 super(CanonicalValidator, self).validate(password)
110
111 # import here to avoid circular imports
112 from identityprovider.auth import LaunchpadBackend
113
114 account = self.account
115 backend = LaunchpadBackend()
116 for accountpassword in account.accountpassword_set.all()[1:]:
117 try:
118 backend._validate_raw_password(
119 account.accountpassword, password, update=False)
120 except ValidationError:
121 pass
122 else:
123 raise ValidationError(PCI_PASSWORD_POLICY_ERROR)
124
125
126_validators = [CanonicalValidator(), BasicValidator(), PCIValidator()]
92127
93128
94def validate_password_policy(password, account=None):129def validate_password_policy(password, account=None):