Merge ~cjwatson/launchpad:person-id-permissions-again into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 20272627fefca0301d2866d6c7159fe8bebd1b46
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:person-id-permissions-again
Merge into: launchpad:master
Diff against target: 166 lines (+59/-39)
4 files modified
lib/lp/registry/browser/tests/test_person_webservice.py (+16/-2)
lib/lp/registry/interfaces/person.py (+20/-17)
lib/lp/registry/model/person.py (+22/-1)
lib/lp/registry/security.py (+1/-19)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+452312@code.launchpad.net

Commit message

Allow reading Person.id using read-only API tokens

Description of the change

See https://portal.admin.canonical.com/C158967 ("Migrate away from email and SSH key cron jobs on loganberry"). We have to do some manual permission checks to make this work properly.

This partially reverts commit 0dd8a14be7715daeb59c52a6ea1b0edc2a5d017f.

Quoting my most recent comment from the ticket:

```
I see the problem: this is due to using credentials that are set to only
allow reading private data, not changing private data. To be clear,
this is completely reasonable - by principles of least privilege, we
don't want the bot to have edit access to anything. However, I'd put
the exported "id" attribute under the launchpad.Moderate permission on
Person, and that doesn't work because launchpad.Moderate is defined like
this:

  <permission
    id="launchpad.Moderate" title="Moderate something" access_level="write" />

The access_level="write" bit means that only tokens with some kind of
write permission can do anything with attributes that require that
permission, so that won't work.
```

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/registry/browser/tests/test_person_webservice.py b/lib/lp/registry/browser/tests/test_person_webservice.py
2index 0081dc8..df60d7e 100644
3--- a/lib/lp/registry/browser/tests/test_person_webservice.py
4+++ b/lib/lp/registry/browser/tests/test_person_webservice.py
5@@ -149,6 +149,20 @@ class TestPersonAccountStatus(TestCaseWithFactory):
6 class TestPersonExportedID(TestCaseWithFactory):
7 layer = DatabaseFunctionalLayer
8
9+ def test_anonymous_user_cannot_see_id(self):
10+ # An anonymous user cannot read the `id` field.
11+ person = self.factory.makePerson()
12+ person_url = api_url(person)
13+
14+ body = (
15+ webservice_for_person(
16+ None, permission=OAuthPermission.WRITE_PRIVATE
17+ )
18+ .get(person_url, api_version="devel")
19+ .jsonBody()
20+ )
21+ self.assertEqual("tag:launchpad.net:2008:redacted", body["id"])
22+
23 def test_normal_user_cannot_see_id(self):
24 # A normal user cannot read the `id` field, not even their own.
25 person = self.factory.makePerson()
26@@ -172,7 +186,7 @@ class TestPersonExportedID(TestCaseWithFactory):
27 body = (
28 webservice_for_person(
29 self.factory.makeRegistryExpert(),
30- permission=OAuthPermission.WRITE_PRIVATE,
31+ permission=OAuthPermission.READ_PRIVATE,
32 )
33 .get(person_url, api_version="devel")
34 .jsonBody()
35@@ -188,7 +202,7 @@ class TestPersonExportedID(TestCaseWithFactory):
36 body = (
37 webservice_for_person(
38 self.factory.makeCommercialAdmin(),
39- permission=OAuthPermission.WRITE_PRIVATE,
40+ permission=OAuthPermission.READ_PRIVATE,
41 )
42 .get(person_url, api_version="devel")
43 .jsonBody()
44diff --git a/lib/lp/registry/interfaces/person.py b/lib/lp/registry/interfaces/person.py
45index 4e66cfd..3aeae4c 100644
46--- a/lib/lp/registry/interfaces/person.py
47+++ b/lib/lp/registry/interfaces/person.py
48@@ -849,6 +849,26 @@ class IPersonViewRestricted(
49 ):
50 """IPerson attributes that require launchpad.View permission."""
51
52+ # Most API clients have no need for the ID, but some systems need it as
53+ # a stable identifier for users even across username changes (see
54+ # https://portal.admin.canonical.com/C158967). Access to this is
55+ # restricted by checks in the property implementation to limit the scope
56+ # of privacy issues.
57+ exported_id = exported(
58+ doNotSnapshot(
59+ Int(
60+ title=_("ID"),
61+ description=_(
62+ "Internal immutable identifier for this person. Only "
63+ "visible by privileged users."
64+ ),
65+ required=True,
66+ readonly=True,
67+ )
68+ ),
69+ exported_as="id",
70+ )
71+
72 account = Object(schema=IAccount)
73 account_id = Int(title=_("Account ID"), required=True, readonly=True)
74 karma = exported(
75@@ -2198,23 +2218,6 @@ class IPersonModerate(IPersonSettingsModerate):
76 class IPersonModerateRestricted(Interface):
77 """IPerson attributes that require launchpad.Moderate permission."""
78
79- # Most API clients have no need for the ID, but some systems need it as
80- # a stable identifier for users even across username changes (see
81- # https://portal.admin.canonical.com/C158967). Access to this is
82- # restricted to limit the scope of privacy issues.
83- exported_id = exported(
84- Int(
85- title=_("ID"),
86- description=_(
87- "Internal immutable identifier for this person. Only visible "
88- "by privileged users."
89- ),
90- required=True,
91- readonly=True,
92- ),
93- exported_as="id",
94- )
95-
96 @call_with(user=REQUEST_USER)
97 @operation_parameters(
98 status=copy_field(IAccount["status"]),
99diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py
100index 1bc9a03..33cd2b9 100644
101--- a/lib/lp/registry/model/person.py
102+++ b/lib/lp/registry/model/person.py
103@@ -495,7 +495,28 @@ class Person(
104 @property
105 def exported_id(self):
106 """See `IPerson`."""
107- return self.id
108+ # Unfortunately, none of the standard permissions are suitable for
109+ # what we need here: launchpad.Moderate comes closest, but it isn't
110+ # quite right because that permission is defined with
111+ # access_level="write", so it only works with OAuth tokens that have
112+ # write permission. Instead, we have to just use launchpad.View and
113+ # do the security checks manually.
114+ user = getUtility(ILaunchBag).user
115+ if user is None:
116+ raise Unauthorized
117+ roles = IPersonRoles(user)
118+ if (
119+ roles.in_admin
120+ # Allowing commercial admins is a bit of a cheat, but it allows
121+ # IS automation to see Person.id
122+ # (https://portal.admin.canonical.com/C158967) without needing
123+ # to use an account that's a fully-fledged member of ~admins.
124+ or roles.in_commercial_admin
125+ or roles.in_registry_experts
126+ ):
127+ return self.id
128+ else:
129+ raise Unauthorized
130
131 sortingColumns = SQL("person_sort_key(Person.displayname, Person.name)")
132 # If we're using SELECT DISTINCT, then we can't use sortingColumns
133diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
134index 259455c..03692fe 100644
135--- a/lib/lp/registry/security.py
136+++ b/lib/lp/registry/security.py
137@@ -126,28 +126,10 @@ class ModerateProjectGroupSet(ModerateByRegistryExpertsOrAdmins):
138 usedfor = IProjectGroupSet
139
140
141-class ModeratePerson(AuthorizationBase):
142+class ModeratePerson(ModerateByRegistryExpertsOrAdmins):
143 permission = "launchpad.Moderate"
144 usedfor = IPerson
145
146- def checkAuthenticated(self, user):
147- """Allow admins, commercial admins, and registry experts.
148-
149- Allowing commercial admins here is a bit of a cheat, but it allows
150- IS automation to see Person.id
151- (https://portal.admin.canonical.com/C158967) without needing to use
152- an account that's a fully-fledged member of ~admins. The only extra
153- exposure here is that commercial admins gain the ability to set the
154- status of other people's accounts, which isn't completely ideal, but
155- in practice people in the commercial admins team are always
156- highly-privileged anyway.
157- """
158- return (
159- user.in_admin
160- or user.in_commercial_admin
161- or user.in_registry_experts
162- )
163-
164
165 class ViewPillar(AuthorizationBase):
166 usedfor = IPillar

Subscribers

People subscribed via source and target branches

to status/vote changes: