Merge lp:~wgrant/launchpad/invisible-person into lp:launchpad
- invisible-person
- Merge into devel
Proposed by
William Grant
Status: | Merged |
---|---|
Merged at revision: | 18008 |
Proposed branch: | lp:~wgrant/launchpad/invisible-person |
Merge into: | lp:launchpad |
Diff against target: |
251 lines (+111/-8) 7 files modified
lib/lp/app/browser/launchpad.py (+4/-1) lib/lp/app/browser/tests/test_launchpad.py (+17/-0) lib/lp/registry/interfaces/person.py (+21/-0) lib/lp/registry/model/person.py (+18/-1) lib/lp/registry/tests/test_personset.py (+23/-1) lib/lp/services/identity/interfaces/account.py (+23/-2) lib/lp/services/identity/model/account.py (+5/-3) |
To merge this branch: | bzr merge lp:~wgrant/launchpad/invisible-person |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+292940@code.launchpad.net |
Commit message
Add AccountStatus.
Description of the change
Add AccountStatus.
Username mastery should eventually move to SSO, but it is more expedient to create invisible placeholder Person records in LP for now.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/app/browser/launchpad.py' | |||
2 | --- lib/lp/app/browser/launchpad.py 2015-08-06 00:46:39 +0000 | |||
3 | +++ lib/lp/app/browser/launchpad.py 2016-04-26 13:09:54 +0000 | |||
4 | @@ -857,12 +857,15 @@ | |||
5 | 857 | # team membership or Launchpad administration. | 857 | # team membership or Launchpad administration. |
6 | 858 | if (person.is_team and | 858 | if (person.is_team and |
7 | 859 | not check_permission('launchpad.LimitedView', person)): | 859 | not check_permission('launchpad.LimitedView', person)): |
9 | 860 | raise NotFound(self.context, name) | 860 | return None |
10 | 861 | # Only admins are permitted to see suspended users. | 861 | # Only admins are permitted to see suspended users. |
11 | 862 | if person.account_status == AccountStatus.SUSPENDED: | 862 | if person.account_status == AccountStatus.SUSPENDED: |
12 | 863 | if not check_permission('launchpad.Moderate', person): | 863 | if not check_permission('launchpad.Moderate', person): |
13 | 864 | raise GoneError( | 864 | raise GoneError( |
14 | 865 | 'User is suspended: %s' % name) | 865 | 'User is suspended: %s' % name) |
15 | 866 | if person.account_status == AccountStatus.PLACEHOLDER: | ||
16 | 867 | if not check_permission('launchpad.Moderate', person): | ||
17 | 868 | return None | ||
18 | 866 | return person | 869 | return person |
19 | 867 | 870 | ||
20 | 868 | # Dapper and Edgy shipped with https://launchpad.net/bazaar hard coded | 871 | # Dapper and Edgy shipped with https://launchpad.net/bazaar hard coded |
21 | 869 | 872 | ||
22 | === modified file 'lib/lp/app/browser/tests/test_launchpad.py' | |||
23 | --- lib/lp/app/browser/tests/test_launchpad.py 2015-01-07 00:35:41 +0000 | |||
24 | +++ lib/lp/app/browser/tests/test_launchpad.py 2016-04-26 13:09:54 +0000 | |||
25 | @@ -370,6 +370,23 @@ | |||
26 | 370 | login_person(self.any_user) | 370 | login_person(self.any_user) |
27 | 371 | self.assertRaises(GoneError, self.traverse, segment, segment) | 371 | self.assertRaises(GoneError, self.traverse, segment, segment) |
28 | 372 | 372 | ||
29 | 373 | def test_placeholder_person_visibility(self): | ||
30 | 374 | # Verify a placeholder user is only traversable by an admin. | ||
31 | 375 | name = u'placeholder-person' | ||
32 | 376 | person = getUtility(IPersonSet).createPlaceholderPerson(name, name) | ||
33 | 377 | login_person(self.admin) | ||
34 | 378 | segment = '~%s' % name | ||
35 | 379 | # Admins can see the placeholder user. | ||
36 | 380 | traversed = self.traverse(segment, segment) | ||
37 | 381 | self.assertEqual(person, traversed) | ||
38 | 382 | # Registry experts can see the placeholder user. | ||
39 | 383 | login_person(self.registry_expert) | ||
40 | 384 | traversed = self.traverse(segment, segment) | ||
41 | 385 | self.assertEqual(person, traversed) | ||
42 | 386 | # Regular users cannot see the placeholder user. | ||
43 | 387 | login_person(self.any_user) | ||
44 | 388 | self.assertRaises(NotFound, self.traverse, segment, segment) | ||
45 | 389 | |||
46 | 373 | def test_public_team(self): | 390 | def test_public_team(self): |
47 | 374 | # Verify a public team is returned. | 391 | # Verify a public team is returned. |
48 | 375 | name = 'public-team' | 392 | name = 'public-team' |
49 | 376 | 393 | ||
50 | === modified file 'lib/lp/registry/interfaces/person.py' | |||
51 | --- lib/lp/registry/interfaces/person.py 2016-03-18 03:30:26 +0000 | |||
52 | +++ lib/lp/registry/interfaces/person.py 2016-04-26 13:09:54 +0000 | |||
53 | @@ -406,6 +406,14 @@ | |||
54 | 406 | and commercial archive) was made via Software Center. | 406 | and commercial archive) was made via Software Center. |
55 | 407 | """) | 407 | """) |
56 | 408 | 408 | ||
57 | 409 | USERNAME_PLACEHOLDER = DBItem(17, """ | ||
58 | 410 | Created by setting a username in SSO. | ||
59 | 411 | |||
60 | 412 | Somebody without a Launchpad account set their username in SSO. | ||
61 | 413 | Since SSO doesn't store usernames directly, an invisible | ||
62 | 414 | placeholder Launchpad account is required. | ||
63 | 415 | """) | ||
64 | 416 | |||
65 | 409 | 417 | ||
66 | 410 | class PersonNameField(BlacklistableContentNameField): | 418 | class PersonNameField(BlacklistableContentNameField): |
67 | 411 | """A `Person` team name, which is unique and performs psuedo blacklisting. | 419 | """A `Person` team name, which is unique and performs psuedo blacklisting. |
68 | @@ -2117,6 +2125,19 @@ | |||
69 | 2117 | used. | 2125 | used. |
70 | 2118 | """ | 2126 | """ |
71 | 2119 | 2127 | ||
72 | 2128 | def createPlaceholderPerson(openid_identifier, name): | ||
73 | 2129 | """Create and return an SSO username placeholder `IPerson`. | ||
74 | 2130 | |||
75 | 2131 | The returned Person will have no email address, just a username and an | ||
76 | 2132 | OpenID identifier. | ||
77 | 2133 | |||
78 | 2134 | :param openid_identifier: The SSO account's OpenID suffix. | ||
79 | 2135 | :param name: The person's name. | ||
80 | 2136 | :raises InvalidName: When the passed name isn't valid. | ||
81 | 2137 | :raises NameAlreadyTaken: When the passed name has already been | ||
82 | 2138 | used. | ||
83 | 2139 | """ | ||
84 | 2140 | |||
85 | 2120 | def ensurePerson(email, displayname, rationale, comment=None, | 2141 | def ensurePerson(email, displayname, rationale, comment=None, |
86 | 2121 | registrant=None): | 2142 | registrant=None): |
87 | 2122 | """Make sure that there is a person in the database with the given | 2143 | """Make sure that there is a person in the database with the given |
88 | 2123 | 2144 | ||
89 | === modified file 'lib/lp/registry/model/person.py' | |||
90 | --- lib/lp/registry/model/person.py 2016-03-01 13:02:42 +0000 | |||
91 | +++ lib/lp/registry/model/person.py 2016-04-26 13:09:54 +0000 | |||
92 | @@ -3397,7 +3397,13 @@ | |||
93 | 3397 | raise NameAlreadyTaken( | 3397 | raise NameAlreadyTaken( |
94 | 3398 | "The account matching the identifier is inactive.") | 3398 | "The account matching the identifier is inactive.") |
95 | 3399 | elif person.account.status in [AccountStatus.DEACTIVATED, | 3399 | elif person.account.status in [AccountStatus.DEACTIVATED, |
97 | 3400 | AccountStatus.NOACCOUNT]: | 3400 | AccountStatus.NOACCOUNT, |
98 | 3401 | AccountStatus.PLACEHOLDER]: | ||
99 | 3402 | if person.account.status == AccountStatus.PLACEHOLDER: | ||
100 | 3403 | # Placeholder accounts were never visible to anyone | ||
101 | 3404 | # before, so make them appear fresh to the user. | ||
102 | 3405 | removeSecurityProxy(person).display_name = full_name | ||
103 | 3406 | removeSecurityProxy(person).datecreated = UTC_NOW | ||
104 | 3401 | removeSecurityProxy(person.account).reactivate(comment) | 3407 | removeSecurityProxy(person.account).reactivate(comment) |
105 | 3402 | if email is None: | 3408 | if email is None: |
106 | 3403 | email = getUtility(IEmailAddressSet).new( | 3409 | email = getUtility(IEmailAddressSet).new( |
107 | @@ -3490,6 +3496,17 @@ | |||
108 | 3490 | name, displayname, hide_email_addresses=True, rationale=rationale, | 3496 | name, displayname, hide_email_addresses=True, rationale=rationale, |
109 | 3491 | comment=comment, registrant=registrant) | 3497 | comment=comment, registrant=registrant) |
110 | 3492 | 3498 | ||
111 | 3499 | def createPlaceholderPerson(self, openid_identifier, name): | ||
112 | 3500 | """See `IPersonSet`.""" | ||
113 | 3501 | account = getUtility(IAccountSet).new( | ||
114 | 3502 | AccountCreationRationale.USERNAME_PLACEHOLDER, name, | ||
115 | 3503 | openid_identifier=openid_identifier, | ||
116 | 3504 | status=AccountStatus.PLACEHOLDER) | ||
117 | 3505 | return self._newPerson( | ||
118 | 3506 | name, name, True, | ||
119 | 3507 | rationale=PersonCreationRationale.USERNAME_PLACEHOLDER, | ||
120 | 3508 | comment="when setting a username in SSO", account=account) | ||
121 | 3509 | |||
122 | 3493 | def _newPerson(self, name, displayname, hide_email_addresses, | 3510 | def _newPerson(self, name, displayname, hide_email_addresses, |
123 | 3494 | rationale, comment=None, registrant=None, account=None): | 3511 | rationale, comment=None, registrant=None, account=None): |
124 | 3495 | """Create and return a new Person with the given attributes.""" | 3512 | """Create and return a new Person with the given attributes.""" |
125 | 3496 | 3513 | ||
126 | === modified file 'lib/lp/registry/tests/test_personset.py' | |||
127 | --- lib/lp/registry/tests/test_personset.py 2015-10-01 10:25:19 +0000 | |||
128 | +++ lib/lp/registry/tests/test_personset.py 2016-04-26 13:09:54 +0000 | |||
129 | @@ -6,7 +6,10 @@ | |||
130 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
131 | 7 | 7 | ||
132 | 8 | 8 | ||
134 | 9 | from testtools.matchers import LessThan | 9 | from testtools.matchers import ( |
135 | 10 | GreaterThan, | ||
136 | 11 | LessThan, | ||
137 | 12 | ) | ||
138 | 10 | import transaction | 13 | import transaction |
139 | 11 | from zope.component import getUtility | 14 | from zope.component import getUtility |
140 | 12 | from zope.security.interfaces import Unauthorized | 15 | from zope.security.interfaces import Unauthorized |
141 | @@ -424,6 +427,25 @@ | |||
142 | 424 | self.assertEqual(AccountStatus.ACTIVE, self.person.account_status) | 427 | self.assertEqual(AccountStatus.ACTIVE, self.person.account_status) |
143 | 425 | self.assertEqual(addr, self.person.preferredemail.email) | 428 | self.assertEqual(addr, self.person.preferredemail.email) |
144 | 426 | 429 | ||
145 | 430 | def testPlaceholderAccount(self): | ||
146 | 431 | # Logging into a username placeholder account activates the | ||
147 | 432 | # account and adds the email address. | ||
148 | 433 | email = u'placeholder@example.com' | ||
149 | 434 | openid = u'placeholder-id' | ||
150 | 435 | name = u'placeholder' | ||
151 | 436 | person = self.person_set.createPlaceholderPerson(openid, name) | ||
152 | 437 | self.assertEqual(AccountStatus.PLACEHOLDER, person.account.status) | ||
153 | 438 | original_created = person.datecreated | ||
154 | 439 | transaction.commit() | ||
155 | 440 | found, updated = self.person_set.getOrCreateByOpenIDIdentifier( | ||
156 | 441 | openid, email, 'New Name', PersonCreationRationale.UNKNOWN, | ||
157 | 442 | 'No Comment') | ||
158 | 443 | self.assertEqual(person, found) | ||
159 | 444 | self.assertEqual(AccountStatus.ACTIVE, person.account.status) | ||
160 | 445 | self.assertEqual(name, person.name) | ||
161 | 446 | self.assertEqual('New Name', person.display_name) | ||
162 | 447 | self.assertThat(person.datecreated, GreaterThan(original_created)) | ||
163 | 448 | |||
164 | 427 | 449 | ||
165 | 428 | class TestCreatePersonAndEmail(TestCase): | 450 | class TestCreatePersonAndEmail(TestCase): |
166 | 429 | """Test `IPersonSet`.createPersonAndEmail().""" | 451 | """Test `IPersonSet`.createPersonAndEmail().""" |
167 | 430 | 452 | ||
168 | === modified file 'lib/lp/services/identity/interfaces/account.py' | |||
169 | --- lib/lp/services/identity/interfaces/account.py 2016-01-26 15:47:37 +0000 | |||
170 | +++ lib/lp/services/identity/interfaces/account.py 2016-04-26 13:09:54 +0000 | |||
171 | @@ -50,6 +50,16 @@ | |||
172 | 50 | class AccountStatus(DBEnumeratedType): | 50 | class AccountStatus(DBEnumeratedType): |
173 | 51 | """The status of an account.""" | 51 | """The status of an account.""" |
174 | 52 | 52 | ||
175 | 53 | PLACEHOLDER = DBItem(5, """ | ||
176 | 54 | Placeholder | ||
177 | 55 | |||
178 | 56 | The account was created automatically by SSO and exists solely | ||
179 | 57 | to hold a username. It is visible only to staff. | ||
180 | 58 | |||
181 | 59 | XXX wgrant 2016-03-03: This is a hack until usernames are moved | ||
182 | 60 | into SSO properly. | ||
183 | 61 | """) | ||
184 | 62 | |||
185 | 53 | NOACCOUNT = DBItem(10, """ | 63 | NOACCOUNT = DBItem(10, """ |
186 | 54 | Unactivated | 64 | Unactivated |
187 | 55 | 65 | ||
188 | @@ -76,7 +86,8 @@ | |||
189 | 76 | 86 | ||
190 | 77 | 87 | ||
191 | 78 | INACTIVE_ACCOUNT_STATUSES = [ | 88 | INACTIVE_ACCOUNT_STATUSES = [ |
193 | 79 | AccountStatus.DEACTIVATED, AccountStatus.SUSPENDED] | 89 | AccountStatus.PLACEHOLDER, AccountStatus.DEACTIVATED, |
194 | 90 | AccountStatus.SUSPENDED] | ||
195 | 80 | 91 | ||
196 | 81 | 92 | ||
197 | 82 | class AccountCreationRationale(DBEnumeratedType): | 93 | class AccountCreationRationale(DBEnumeratedType): |
198 | @@ -206,6 +217,14 @@ | |||
199 | 206 | and commercial archive) was made via Software Center. | 217 | and commercial archive) was made via Software Center. |
200 | 207 | """) | 218 | """) |
201 | 208 | 219 | ||
202 | 220 | USERNAME_PLACEHOLDER = DBItem(17, """ | ||
203 | 221 | Created by setting a username in SSO. | ||
204 | 222 | |||
205 | 223 | Somebody without a Launchpad account set their username in SSO. | ||
206 | 224 | Since SSO doesn't store usernames directly, an invisible | ||
207 | 225 | placeholder Launchpad account is required. | ||
208 | 226 | """) | ||
209 | 227 | |||
210 | 209 | 228 | ||
211 | 210 | class AccountStatusError(LaunchpadValidationError): | 229 | class AccountStatusError(LaunchpadValidationError): |
212 | 211 | """The account status cannot change to the proposed status.""" | 230 | """The account status cannot change to the proposed status.""" |
213 | @@ -215,6 +234,8 @@ | |||
214 | 215 | """A valid status and transition.""" | 234 | """A valid status and transition.""" |
215 | 216 | 235 | ||
216 | 217 | transitions = { | 236 | transitions = { |
217 | 237 | AccountStatus.PLACEHOLDER: [ | ||
218 | 238 | AccountStatus.NOACCOUNT, AccountStatus.ACTIVE], | ||
219 | 218 | AccountStatus.NOACCOUNT: [AccountStatus.ACTIVE], | 239 | AccountStatus.NOACCOUNT: [AccountStatus.ACTIVE], |
220 | 219 | AccountStatus.ACTIVE: [ | 240 | AccountStatus.ACTIVE: [ |
221 | 220 | AccountStatus.DEACTIVATED, AccountStatus.SUSPENDED], | 241 | AccountStatus.DEACTIVATED, AccountStatus.SUSPENDED], |
222 | @@ -224,7 +245,7 @@ | |||
223 | 224 | 245 | ||
224 | 225 | def constraint(self, value): | 246 | def constraint(self, value): |
225 | 226 | """See `IField`.""" | 247 | """See `IField`.""" |
227 | 227 | if not IAccount.providedBy(self.context): | 248 | if removeSecurityProxy(self.context)._SO_creating: |
228 | 228 | # This object is initializing. | 249 | # This object is initializing. |
229 | 229 | return True | 250 | return True |
230 | 230 | if self.context.status == value: | 251 | if self.context.status == value: |
231 | 231 | 252 | ||
232 | === modified file 'lib/lp/services/identity/model/account.py' | |||
233 | --- lib/lp/services/identity/model/account.py 2015-07-08 16:05:11 +0000 | |||
234 | +++ lib/lp/services/identity/model/account.py 2016-04-26 13:09:54 +0000 | |||
235 | @@ -92,11 +92,13 @@ | |||
236 | 92 | class AccountSet: | 92 | class AccountSet: |
237 | 93 | """See `IAccountSet`.""" | 93 | """See `IAccountSet`.""" |
238 | 94 | 94 | ||
240 | 95 | def new(self, rationale, displayname, openid_identifier=None): | 95 | def new(self, rationale, displayname, openid_identifier=None, |
241 | 96 | status=AccountStatus.NOACCOUNT): | ||
242 | 96 | """See `IAccountSet`.""" | 97 | """See `IAccountSet`.""" |
244 | 97 | 98 | assert status in (AccountStatus.NOACCOUNT, AccountStatus.PLACEHOLDER) | |
245 | 98 | account = Account( | 99 | account = Account( |
247 | 99 | displayname=displayname, creation_rationale=rationale) | 100 | displayname=displayname, creation_rationale=rationale, |
248 | 101 | status=status) | ||
249 | 100 | 102 | ||
250 | 101 | # Create an OpenIdIdentifier record if requested. | 103 | # Create an OpenIdIdentifier record if requested. |
251 | 102 | if openid_identifier is not None: | 104 | if openid_identifier is not None: |