Merge lp:~bjornt/launchpad/acl-adapter into lp:launchpad/db-devel
- acl-adapter
- Merge into db-devel
Status: | Rejected |
---|---|
Rejected by: | Brad Crittenden |
Proposed branch: | lp:~bjornt/launchpad/acl-adapter |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
421 lines (+389/-0) 5 files modified
lib/canonical/configure.zcml (+1/-0) lib/lp/services/acl/configure.zcml (+13/-0) lib/lp/services/acl/interfaces.py (+60/-0) lib/lp/services/acl/model.py (+106/-0) lib/lp/services/doc/acl.txt (+209/-0) |
To merge this branch: | bzr merge lp:~bjornt/launchpad/acl-adapter |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Disapprove | |
Review via email: mp+28893@code.launchpad.net |
Commit message
Description of the change
First part of the ACL work that is to be done.
This branch adds the basic infrastructure, for a very basic ACL system.
It's enough so that someone can build ACLs for bug task as per
https:/
Improvements to this basic system, like inheritance, will follow later,
when it's needed.
An ACLAdapter was added, which is used to manage and check ACLs for any
objects having ACLs. There's also a doc, acl.txt, explaining how to add
ACLs for an object. I want that one to be as clear as possible, so if
anything is unclear, please ask, and let's try to make it better.
- 9491. By Björn Tillenius
-
Update copyright.
Robert Collins (lifeless) wrote : | # |
Robert Collins (lifeless) wrote : | # |
The code and tests are all pretty clear etc.
Why is it an adapter rather than an attribute?
E.g.
bugs.read_acl
bugs.write_acl
bugs.admin_acl
?
Secondly, why do you need a new table for each new object guarded by
an ACL? That seems unneeded.
Thirdly, this design seems to create a 'list of things referring to
people' - it looks to me like this has all the potential scaling
issues of Teams, and it would be better to just link to a single team
for each (object, permission) tuple. We could easily make a hidden
team concept, for system maintained lists-of-users, which would mean
we get to reuse all the team participation code.
-Rob
- 9492. By Björn Tillenius
-
Add note about creating an index.
- 9493. By Björn Tillenius
-
Add IACL to __all__.
- 9494. By Björn Tillenius
-
Remove parent columns, since they aren't used yet.
Björn Tillenius (bjornt) wrote : | # |
On Wed, Jun 30, 2010 at 08:01:24PM -0000, Robert Collins wrote:
> I have a few meta questions:
>
> How did your performance profiling on staging go? Were there more SQL
> queries to satisfy pages with / without ACLs, or were there less?
>
> how does it fare showing a list of bugs where each bug has a different ACL?
I didn't get around doing the testing yet. I wanted to test code similar
to what will land in the first step. I've written the DB patch for
BugTaskACL lp:~bjornt/launchpad/bugtask-acl-db, and have requested to
have it applied on staging, so that I can do the testing now. I will
come back with the results.
BTW, I've updated this branch to remove two db columns in the example
patch, since they weren't used yet.
--
Björn Tillenius | https:/
Björn Tillenius (bjornt) wrote : | # |
On Wed, Jun 30, 2010 at 10:49:23PM -0000, Robert Collins wrote:
> The code and tests are all pretty clear etc.
>
> Why is it an adapter rather than an attribute?
>
> E.g.
> bugs.read_acl
> bugs.write_acl
> bugs.admin_acl
It's an adapter to keep the code more separated. Using attributes would
mean having a mix-in class, and that easily gets quite confusing.
> Secondly, why do you need a new table for each new object guarded by
> an ACL? That seems unneeded.
We did consider having one big table for all ACLs. It's basically
equivalent, it would be possible to have one table. However, we went
with separate tables, since it helps keeping referential integrity, and
keeps the different ACLs a bit separated, so it's easier to focus on
only one. Also, you have to write a DB patch anyway, to do migration. If
we in the future find out that using one big table would be better, we
can change it then without much problem, but it's too early to say that
one option has a lot of advantages over another.
> Thirdly, this design seems to create a 'list of things referring to
> people' - it looks to me like this has all the potential scaling
> issues of Teams, and it would be better to just link to a single team
> for each (object, permission) tuple. We could easily make a hidden
> team concept, for system maintained lists-of-users, which would mean
> we get to reuse all the team participation code.
I guess. But wouldn't you just take that scaling problem away from ACL,
and push it to Team? I.e., I don't see a reduction in data, rather a
slight increase. Could you elaborate on what problems this would solve,
and in what way?
--
Björn Tillenius | https:/
Robert Collins (lifeless) wrote : | # |
2010/7/2 Björn Tillenius <email address hidden>:
> On Wed, Jun 30, 2010 at 10:49:23PM -0000, Robert Collins wrote:
>> The code and tests are all pretty clear etc.
>>
>> Why is it an adapter rather than an attribute?
>>
>> E.g.
>> bugs.read_acl
>> bugs.write_acl
>> bugs.admin_acl
>
> It's an adapter to keep the code more separated. Using attributes would
> mean having a mix-in class, and that easily gets quite confusing.
I don't see that you'd need a mixin class; just an attribute (or attributes).
E.g. BugTask.acl or BugTask.read_acl and BugTask.write_acl - depending
on how we model it.
>> Secondly, why do you need a new table for each new object guarded by
>> an ACL? That seems unneeded.
>
> We did consider having one big table for all ACLs. It's basically
> equivalent, it would be possible to have one table. However, we went
> with separate tables, since it helps keeping referential integrity, and
> keeps the different ACLs a bit separated, so it's easier to focus on
> only one. Also, you have to write a DB patch anyway, to do migration. If
> we in the future find out that using one big table would be better, we
> can change it then without much problem, but it's too early to say that
> one option has a lot of advantages over another.
It is possible to have no table in between IHasACL and Team - you can
have a separate table, like code's 'vote' table, for reference, but
not actually need to look at it in most queries. Then its just 'if
user in object.read_acl, where acl is a 'people set' - the base
building block we should have built teams on, which we can retrofit
pretty easily.
>> Thirdly, this design seems to create a 'list of things referring to
>> people' - it looks to me like this has all the potential scaling
>> issues of Teams, and it would be better to just link to a single team
>> for each (object, permission) tuple. We could easily make a hidden
>> team concept, for system maintained lists-of-users, which would mean
>> we get to reuse all the team participation code.
>
> I guess. But wouldn't you just take that scaling problem away from ACL,
> and push it to Team? I.e., I don't see a reduction in data, rather a
> slight increase. Could you elaborate on what problems this would solve,
> and in what way?
My experience with ACLs in the NDS and AD space, as a sysadmin, is
that they tend to blow out. They become long tangled lists of people
and teams, particularly when they are not gardened by sysadmins but
instead users are able to control them. I have every expectation that
our users will do the same thing: they will create acls with 1000's of
entries in them, rather than manually creating a permissions team and
adding to that.
So I have the expectation that the set of people in an ACL will grow -
pretty predictable - as more users get access to the feature. Team is
already a scaling issue, but one we've solved: we have a lot of work
put into managing 'a big set of person-
and we can use that to directly manage the person-
objects that are added to ACLs.
-Rob
Björn Tillenius (bjornt) wrote : | # |
On Thu, Jul 01, 2010 at 08:40:47PM -0000, Robert Collins wrote:
> 2010/7/2 Björn Tillenius <email address hidden>:
> > On Wed, Jun 30, 2010 at 10:49:23PM -0000, Robert Collins wrote:
> >> The code and tests are all pretty clear etc.
> >>
> >> Why is it an adapter rather than an attribute?
> >>
> >> E.g.
> >> bugs.read_acl
> >> bugs.write_acl
> >> bugs.admin_acl
> >
> > It's an adapter to keep the code more separated. Using attributes would
> > mean having a mix-in class, and that easily gets quite confusing.
>
> I don't see that you'd need a mixin class; just an attribute (or attributes).
>
> E.g. BugTask.acl or BugTask.read_acl and BugTask.write_acl - depending
> on how we model it.
Right. So let's go with having a single 'acl' attribute. Then you also
need to add it to the interface (or make the class implement a new
interface), as well as declaring security permissions for it.
It's easier to explain 'set __acl_class__' to the ACL class than the
above. I think having an adapter will be a bit easier to understand as
well (implementation
access to self. I don't think we have anything like this (a re-usuable
attribute that have access to self) in the code base, but we do have
adapters.
> >> Secondly, why do you need a new table for each new object guarded by
> >> an ACL? That seems unneeded.
> >
> > We did consider having one big table for all ACLs. It's basically
> > equivalent, it would be possible to have one table. However, we went
> > with separate tables, since it helps keeping referential integrity, and
> > keeps the different ACLs a bit separated, so it's easier to focus on
> > only one. Also, you have to write a DB patch anyway, to do migration. If
> > we in the future find out that using one big table would be better, we
> > can change it then without much problem, but it's too early to say that
> > one option has a lot of advantages over another.
>
> It is possible to have no table in between IHasACL and Team - you can
> have a separate table, like code's 'vote' table, for reference, but
> not actually need to look at it in most queries. Then its just 'if
> user in object.read_acl, where acl is a 'people set' - the base
> building block we should have built teams on, which we can retrofit
> pretty easily.
Ok, I think I get what you mean. That approach is ok for the simple ACL
system in this branch, but I know that we are going to extend it quite
soon to have inheritance. Then for each ACL (one ACL per permission) you
need to know whether it's inherited and what the parent is. So instead
of one column per permission, you need three or four. Doable, but not as
nice, imo. We should only do that, if we can show that it would improve
performance. It's something for the next TA to test, since I won't have
time to do all that today :)
> >> Thirdly, this design seems to create a 'list of things referring to
> >> people' - it looks to me like this has all the potential scaling
> >> issues of Teams, and it would be better to just link to a single team
> >> for each (object, permission) tuple. We could easily make a hidden
> >> team concept, for system maintained lists-of-users, which wou...
Robert Collins (lifeless) wrote : | # |
Thanks for the feedback - and good luck in your next role :)
Brad Crittenden (bac) wrote : | # |
The ACL approach in this branch is not the way we'll be doing privacy, as decided at the Epic, so this branch should not land at the moment. It will be valuable in the future if we do implement a more generic ACL system.
Unmerged revisions
- 9494. By Björn Tillenius
-
Remove parent columns, since they aren't used yet.
- 9493. By Björn Tillenius
-
Add IACL to __all__.
- 9492. By Björn Tillenius
-
Add note about creating an index.
- 9491. By Björn Tillenius
-
Update copyright.
- 9490. By Björn Tillenius
-
No need for _object_id attribute.
- 9489. By Björn Tillenius
-
Add/improve docstrings.
- 9488. By Björn Tillenius
-
Granting a permission to EVERYONE removes all other ACL rows for that permission.
- 9487. By Björn Tillenius
-
Ensure all ACL rows can't be deleted.
- 9486. By Björn Tillenius
-
No need to filter on the permission in the list expansion.
- 9485. By Björn Tillenius
-
Require a permission to getACLItems(), since that seems to be the common case.
Preview Diff
1 | === modified file 'lib/canonical/configure.zcml' |
2 | --- lib/canonical/configure.zcml 2010-03-24 15:22:26 +0000 |
3 | +++ lib/canonical/configure.zcml 2010-07-01 16:12:36 +0000 |
4 | @@ -15,6 +15,7 @@ |
5 | <include package="canonical.launchpad" file="permissions.zcml" /> |
6 | <include package="canonical.launchpad.webapp" file="meta.zcml" /> |
7 | <include package="lazr.restful" file="meta.zcml" /> |
8 | + <include package="lp.services.acl" /> |
9 | <include package="lp.services.database" /> |
10 | <include package="lp.services.inlinehelp" file="meta.zcml" /> |
11 | <include package="lp.services.openid" /> |
12 | |
13 | === added directory 'lib/lp/services/acl' |
14 | === added file 'lib/lp/services/acl/__init__.py' |
15 | === added file 'lib/lp/services/acl/configure.zcml' |
16 | --- lib/lp/services/acl/configure.zcml 1970-01-01 00:00:00 +0000 |
17 | +++ lib/lp/services/acl/configure.zcml 2010-07-01 16:12:36 +0000 |
18 | @@ -0,0 +1,13 @@ |
19 | +<!-- Copyright 2010 Canonical Ltd. This software is licensed under the |
20 | + GNU Affero General Public License version 3 (see the file LICENSE). |
21 | +--> |
22 | + |
23 | +<configure |
24 | + xmlns="http://namespaces.zope.org/zope" |
25 | + xmlns:browser="http://namespaces.zope.org/browser" |
26 | + xmlns:i18n="http://namespaces.zope.org/i18n" |
27 | + xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc" |
28 | + xmlns:lp="http://namespaces.canonical.com/lp" |
29 | + i18n_domain="launchpad"> |
30 | + <adapter factory="lp.services.acl.model.adapt_to_acl" /> |
31 | +</configure> |
32 | |
33 | === added file 'lib/lp/services/acl/interfaces.py' |
34 | --- lib/lp/services/acl/interfaces.py 1970-01-01 00:00:00 +0000 |
35 | +++ lib/lp/services/acl/interfaces.py 2010-07-01 16:12:36 +0000 |
36 | @@ -0,0 +1,60 @@ |
37 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
38 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
39 | + |
40 | +"""Interfaces and enums for ACLs.""" |
41 | + |
42 | +__metaclass__ = type |
43 | +__all__ = [ |
44 | + 'ACLObjectType', |
45 | + 'ACLPermission', |
46 | + 'EVERYONE', |
47 | + 'IACL', |
48 | + ] |
49 | + |
50 | +from zope.interface import Interface |
51 | + |
52 | +from lazr.enum import DBEnumeratedType, DBItem |
53 | + |
54 | + |
55 | +EVERYONE = None |
56 | + |
57 | + |
58 | +class ACLPermission(DBEnumeratedType): |
59 | + """Permissions for ACLs.""" |
60 | + |
61 | + VIEW = DBItem(0, """ |
62 | + View |
63 | + |
64 | + Allows you to view an object. |
65 | + """) |
66 | + |
67 | + MODIFY_ACL = DBItem(1, """ |
68 | + Modify ACL |
69 | + |
70 | + Allows you to grant and revoke permissions for an object. |
71 | + """) |
72 | + |
73 | + |
74 | +class ACLObjectType(DBEnumeratedType): |
75 | + |
76 | + BUGTASK = DBItem(0, """ |
77 | + BugTask |
78 | + |
79 | + ACL for a BugTask. |
80 | + """) |
81 | + |
82 | + |
83 | +class IACL(Interface): |
84 | + """Manage ACL for an object.""" |
85 | + |
86 | + def grant(permission, user): |
87 | + """Grant the given permission to the user.""" |
88 | + |
89 | + def revoke(permission, user): |
90 | + """Revoke the given permission for the user.""" |
91 | + |
92 | + def has(permission, user): |
93 | + """Check whether a user has the given permission.""" |
94 | + |
95 | + def getACLItems(permission): |
96 | + """Return all the ACL items for the object.""" |
97 | |
98 | === added file 'lib/lp/services/acl/model.py' |
99 | --- lib/lp/services/acl/model.py 1970-01-01 00:00:00 +0000 |
100 | +++ lib/lp/services/acl/model.py 2010-07-01 16:12:36 +0000 |
101 | @@ -0,0 +1,106 @@ |
102 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
103 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
104 | + |
105 | +"""ACL model classes.""" |
106 | + |
107 | +__metaclass__ = type |
108 | +__all__ = [ |
109 | + 'ACLAdapter', |
110 | + ] |
111 | + |
112 | + |
113 | +from zope.component import adapter |
114 | +from zope.interface import implementer, implements, Interface |
115 | +from zope.security.proxy import removeSecurityProxy |
116 | + |
117 | +from canonical.launchpad.interfaces.lpstorm import IStore |
118 | +from lp.registry.model.teammembership import TeamParticipation |
119 | +from lp.services.acl.interfaces import EVERYONE, IACL |
120 | + |
121 | + |
122 | +class ACLAdapter: |
123 | + """Generic ACL adapter. |
124 | + |
125 | + This one should be used to manage the ACL for any object. It |
126 | + shouldn't be necessary to create more specific ones. |
127 | + """ |
128 | + |
129 | + implements(IACL) |
130 | + |
131 | + def __init__(self, ob, ACL_class): |
132 | + self._object = ob |
133 | + self._ACL_class = ACL_class |
134 | + |
135 | + def grant(self, permission, user): |
136 | + """See `IACL`.""" |
137 | + store = IStore(self._ACL_class) |
138 | + existing_acls = [ |
139 | + acl for acl in self.getACLItems(permission)] |
140 | + if len(existing_acls) == 1 and existing_acls[0].person is EVERYONE: |
141 | + public_acl = existing_acls.pop(0) |
142 | + store.remove(public_acl) |
143 | + if user == EVERYONE: |
144 | + for existing_acl in existing_acls: |
145 | + store.remove(existing_acl) |
146 | + object_acl = self._ACL_class( |
147 | + ob=self._object, person=user, |
148 | + permission=permission) |
149 | + store.add(object_acl) |
150 | + |
151 | + def revoke(self, permission, user): |
152 | + """See `IACL`.""" |
153 | + assert self.getACLItems(permission).count() > 1, ( |
154 | + "Can't remove last ACL row for %s." % permission.name) |
155 | + store = IStore(self._ACL_class) |
156 | + existing_acl = store.find( |
157 | + self._ACL_class, |
158 | + self._ACL_class.object_id == self._object.id, |
159 | + self._ACL_class.permission == permission, |
160 | + self._ACL_class.person == user).one() |
161 | + store.remove(existing_acl) |
162 | + |
163 | + def has(self, permission, user): |
164 | + """See `IACL`.""" |
165 | + store = IStore(self._ACL_class) |
166 | + public_acl = store.find( |
167 | + self._ACL_class, |
168 | + self._ACL_class.object_id == self._object.id, |
169 | + self._ACL_class.person == EVERYONE, |
170 | + self._ACL_class.permission == permission).one() |
171 | + if public_acl is not None: |
172 | + return True |
173 | + acls = store.find( |
174 | + self._ACL_class, |
175 | + self._ACL_class.object_id == self._object.id, |
176 | + self._ACL_class.person == TeamParticipation.teamID, |
177 | + self._ACL_class.permission == permission, |
178 | + TeamParticipation.person == user) |
179 | + return acls.any() is not None |
180 | + |
181 | + |
182 | + def getACLItems(self, permission): |
183 | + """See `IACL`.""" |
184 | + store = IStore(self._ACL_class) |
185 | + return store.find( |
186 | + self._ACL_class, |
187 | + self._ACL_class.permission == permission, |
188 | + self._ACL_class.object_id == self._object.id) |
189 | + |
190 | + |
191 | +@adapter(Interface) |
192 | +@implementer(IACL) |
193 | +def adapt_to_acl(context): |
194 | + """Adapt any object that has __acl_class__ set. |
195 | + |
196 | + It's registered against Interface, so that it's easier to hook up |
197 | + the ACL adpter, so that a separate ZCML registration isn't needed. |
198 | + |
199 | + If the object doesn't have an __acl__class__ attribute, None is |
200 | + returned, which means that the object couldn't be adapted. |
201 | + """ |
202 | + missing = object() |
203 | + acl_class = getattr( |
204 | + removeSecurityProxy(context.__class__), '__acl_class__', missing) |
205 | + if acl_class is missing: |
206 | + return None |
207 | + return ACLAdapter(context, acl_class) |
208 | |
209 | === added file 'lib/lp/services/doc/acl.txt' |
210 | --- lib/lp/services/doc/acl.txt 1970-01-01 00:00:00 +0000 |
211 | +++ lib/lp/services/doc/acl.txt 2010-07-01 16:12:36 +0000 |
212 | @@ -0,0 +1,209 @@ |
213 | +==== |
214 | +ACLs |
215 | +==== |
216 | + |
217 | +This document aims to explain how the ACL system works, and how a |
218 | +programmer can integrate it into his code. It starts with an overview, |
219 | +which explains what the goal of the ACL system is. For the more |
220 | +interested parties, it then continues explaining the ACL API; how a |
221 | +programmer would use it. |
222 | + |
223 | + |
224 | +Overview |
225 | +======== |
226 | + |
227 | +The ACL system is used to check whether some user has certain |
228 | +permissions to do various things with an object. To have something more |
229 | +concrete to talk about, lets say that we have a project, which has bugs. |
230 | +By default, the project and its bugs are accessible to anyone. A |
231 | +Launchpad Commercial Admin can grant the MODIFY_ACL permission to |
232 | +someone in the project team. That person (or team) can now limit who can |
233 | +have access to their project and its bugs. The can give people and teams |
234 | +permission project-wide, to let someone see the whole project and its |
235 | +bugs, or let someone to see only part of the project, for example only a |
236 | +single bug. (Giving permission to a single bugs means that the user will |
237 | +automatically get permission to see the name of the project, though). |
238 | + |
239 | +Teams can be used to manage permissions across multiple projects. By |
240 | +giving permission to a team, you can give others the same permission by |
241 | +adding them to the team. This way you can easily give someone access to |
242 | +multiple projects. |
243 | + |
244 | +In addition to controlling permission project-wide, or for a single |
245 | +bugs, it's also possible to control it for a larger part of the project. |
246 | +For example, to have specific permission for the Bugs part of the |
247 | +project, e.g. giving someone permission to change the importance of |
248 | +all bugs, but not editing the project details. |
249 | + |
250 | + |
251 | +Defining an ACL for an object |
252 | +============================= |
253 | + |
254 | +Each object type needs its own ACL table. The first thing you need to do |
255 | +is to create the DB patch. |
256 | + |
257 | + >>> from canonical.launchpad.webapp.interfaces import ( |
258 | + ... IStoreSelector, MAIN_STORE, MASTER_FLAVOR) |
259 | + >>> store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR) |
260 | + >>> store.execute(""" |
261 | + ... CREATE TABLE MyObjectACL( |
262 | + ... id serial NOT NULL PRIMARY KEY, |
263 | + ... object_id integer NOT NULL, --REFERENCES Product(id), |
264 | + ... person integer NULL, -- REFERENCES Person(id), |
265 | + ... permission INTEGER NOT NULL |
266 | + ... ); |
267 | + ... """, noresult=True) |
268 | + |
269 | +To avoid duplicate rows from being created, an unique constraint should |
270 | +be added for (object_id, person, permission): |
271 | + |
272 | + >>> store.execute(""" |
273 | + ... ALTER TABLE MyObjectACL |
274 | + ... ADD CONSTRAINT myobjectacl__object_id__person__permission___key |
275 | + ... UNIQUE (object_id, person, permission); |
276 | + ... """, noresult=True) |
277 | + |
278 | +The next things is of course to add the model class. |
279 | + |
280 | + >>> from storm.base import Storm |
281 | + >>> from storm.locals import Bool, Int, Reference |
282 | + >>> from canonical.database.enumcol import EnumCol |
283 | + >>> from lp.services.acl.interfaces import ACLObjectType, ACLPermission |
284 | + >>> class MyObjectACL(Storm): |
285 | + ... __storm_table__ = "MyObjectACL" |
286 | + ... def __init__(self, ob, person, permission): |
287 | + ... self.object_id = ob.id |
288 | + ... self.person = person |
289 | + ... self.permission = permission |
290 | + ... |
291 | + ... id = Int(primary=True) |
292 | + ... object_id = Int(name="object_id") |
293 | + ... person_id = Int(name='person') |
294 | + ... person = Reference(person_id, "Person.id") |
295 | + ... permission = EnumCol(enum=ACLPermission) |
296 | + |
297 | + |
298 | +By defining a __acl_class__ class attribute, we can connect the ACL |
299 | +class with the object type what we want to create. This should of course |
300 | +be a DB class, but we'll use a non-DB class as an example. |
301 | + |
302 | + >>> class MyObject: |
303 | + ... __acl_class__ = MyObjectACL |
304 | + ... def __init__(self, id): |
305 | + ... self.id = id |
306 | + |
307 | +That's everything that is needed to hook things up. We can now adapt an |
308 | +instance of our object to IACL to manage the permissions for the object. |
309 | + |
310 | + >>> from lp.services.acl.interfaces import IACL |
311 | + >>> acl = IACL(MyObject(1)) |
312 | + |
313 | + |
314 | +Managing ACLs |
315 | +============= |
316 | + |
317 | +The ACL of an object tells you who has certain permissions for an |
318 | +object. You have to hook into the places where the object is created and |
319 | +change, and make sure that the ACL is correct. |
320 | + |
321 | +When the object is created, you have to add the appropriate permissions. |
322 | +Usually this means granting VIEW to EVERYONE, if the object is public. |
323 | +EVERYONE gets mapped into NULL in the database. |
324 | + |
325 | +Grant |
326 | +----- |
327 | + |
328 | + >>> from lp.services.acl.interfaces import EVERYONE |
329 | + >>> my_object = MyObject(2) |
330 | + >>> IACL(my_object).grant(ACLPermission.VIEW, EVERYONE) |
331 | + >>> for acl_item in IACL(my_object).getACLItems(ACLPermission.VIEW): |
332 | + ... print "User: %s, Permission: %s" % ( |
333 | + ... acl_item.person, acl_item.permission.name) |
334 | + User: None, Permission: VIEW |
335 | + |
336 | +The EVERYONE ACL is special. There has to be at least one item in the |
337 | +ACL, so the EVERYONE ACL is basically the same as an empty ACL. Meaning |
338 | +that if you want to limit the access to an object, all you have to do is |
339 | +to grant the permission to the people you want to have access. The |
340 | +permission for EVERYONE will automatically be revoked by the ACL |
341 | +adapter. |
342 | + |
343 | + >>> bob = factory.makePerson(name='bob') |
344 | + >>> IACL(my_object).grant(ACLPermission.VIEW, bob) |
345 | + >>> for acl_item in IACL(my_object).getACLItems(ACLPermission.VIEW): |
346 | + ... print "User: %s, Permission: %s" % ( |
347 | + ... acl_item.person, acl_item.permission.name) |
348 | + User: <Person at ... bob (Bob)>, Permission: VIEW |
349 | + |
350 | +Has |
351 | +--- |
352 | + |
353 | +To check whether someone has permissions on an object, the has() method |
354 | +is used. |
355 | + |
356 | + >>> alice = factory.makePerson(name='alice') |
357 | + >>> my_object = MyObject(3) |
358 | + >>> IACL(my_object).grant(ACLPermission.VIEW, EVERYONE) |
359 | + >>> IACL(my_object).has(ACLPermission.VIEW, bob) |
360 | + True |
361 | + >>> IACL(my_object).has(ACLPermission.VIEW, alice) |
362 | + True |
363 | + |
364 | + >>> IACL(my_object).grant(ACLPermission.VIEW, bob) |
365 | + >>> IACL(my_object).has(ACLPermission.VIEW, bob) |
366 | + True |
367 | + >>> IACL(my_object).has(ACLPermission.VIEW, alice) |
368 | + False |
369 | + |
370 | +The user doesn't have to have the permission directly. He can also |
371 | +inherit it from a team. |
372 | + |
373 | + >>> from zope.security.proxy import removeSecurityProxy |
374 | + >>> team = factory.makeTeam() |
375 | + >>> IACL(my_object).grant(ACLPermission.VIEW, team) |
376 | + >>> IACL(my_object).has(ACLPermission.VIEW, alice) |
377 | + False |
378 | + >>> removeSecurityProxy(alice).join(team) |
379 | + >>> IACL(my_object).has(ACLPermission.VIEW, alice) |
380 | + True |
381 | + |
382 | + |
383 | +Revoke |
384 | +------ |
385 | + |
386 | +To revoke a permissions, the revoke() method is used. |
387 | + |
388 | + >>> my_object = MyObject(4) |
389 | + >>> IACL(my_object).grant(ACLPermission.VIEW, bob) |
390 | + >>> IACL(my_object).grant(ACLPermission.VIEW, alice) |
391 | + |
392 | + >>> IACL(my_object).revoke(ACLPermission.VIEW, alice) |
393 | + >>> IACL(my_object).has(ACLPermission.VIEW, bob) |
394 | + True |
395 | + >>> IACL(my_object).has(ACLPermission.VIEW, alice) |
396 | + False |
397 | + |
398 | +As with grant(), the EVERYONE permission is special. There has to exist |
399 | +at least one ACL row in the DB. To avoid making something public by |
400 | +accident, EVERYONE isn't given the permission automatically if the last |
401 | +row is removed. The last row should never be removed. If it is, it's a |
402 | +programming error, so an assertion is there to prevent the db to get |
403 | +into a bad state. |
404 | + |
405 | + >>> for acl_item in IACL(my_object).getACLItems(ACLPermission.VIEW): |
406 | + ... print "User: %s, Permission: %s" % ( |
407 | + ... acl_item.person, acl_item.permission.name) |
408 | + User: <Person at ... bob (Bob)>, Permission: VIEW |
409 | + >>> IACL(my_object).revoke(ACLPermission.VIEW, bob) |
410 | + Traceback (most recent call last): |
411 | + ... |
412 | + AssertionError: Can't remove last ACL row for VIEW. |
413 | + |
414 | +However, if EVERYONE is granted a permission, all other ACL rows will be |
415 | +deleted, since everyone will have access anyway. |
416 | + |
417 | + >>> IACL(my_object).grant(ACLPermission.VIEW, EVERYONE) |
418 | + >>> for acl_item in IACL(my_object).getACLItems(ACLPermission.VIEW): |
419 | + ... print "User: %s, Permission: %s" % ( |
420 | + ... acl_item.person, acl_item.permission.name) |
421 | + User: None, Permission: VIEW |
I have a few meta questions:
How did your performance profiling on staging go? Were there more SQL
queries to satisfy pages with / without ACLs, or were there less?
how does it fare showing a list of bugs where each bug has a different ACL?
I also had a number of design questions, but I think the performance
aspect is key, as per the LEP, so would really like to understand that
first ;)
-Rob