Merge lp:~bjornt/launchpad/acl-adapter into lp:launchpad/db-devel

Proposed by Björn Tillenius
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
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Disapprove
Review via email: mp+28893@code.launchpad.net

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://dev.launchpad.net/LEP/BetterPrivacy/ACLImplementationForBugs.

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.

To post a comment you must log in.
lp:~bjornt/launchpad/acl-adapter updated
9491. By Björn Tillenius

Update copyright.

Revision history for this message
Robert Collins (lifeless) 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 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

Revision history for this message
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

lp:~bjornt/launchpad/acl-adapter updated
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.

Revision history for this message
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://launchpad.net/~bjornt

Revision history for this message
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://launchpad.net/~bjornt

Revision history for this message
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-or-big-set-of-people' quickly,
and we can use that to directly manage the person-or-big-set-of-people
objects that are added to ACLs.

-Rob

Revision history for this message
Björn Tillenius (bjornt) wrote :
Download full text (4.5 KiB)

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-wise), since the attribute would have to have
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...

Read more...

Revision history for this message
Robert Collins (lifeless) wrote :

Thanks for the feedback - and good luck in your next role :)

Revision history for this message
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.

review: Disapprove (code)

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: