ACL thoughts

3 views
Skip to first unread message

Scott Merrill

unread,
Apr 25, 2008, 8:39:53 AM4/25/08
to habar...@googlegroups.com
Brainstorming with Jay Pipes last night has allowed some new thoughts
to percolate into my consciousness.

What do folks think of the idea of ACL::group_can() supporting a
wildcard operator? ACL::group_can('create_*') would iterate over all
the possible content types defined and return true if the user is
granted any of create_{content_type}. This would be for high-level
operations like menu generation: use the wildcard to define whether
the user can see the top-level "Create" menu, and then iterate over
each create_{content_type} permission to build the sub-menus that user
is permitted to see.

Likewise, when building the query for Posts::get(), we could use
wildcards. First, make sure the user can see any content; then
iterate over each permutation to build up the necessary query. Here's
some psuedocode:

if ( can('view_*') )
foreach content_type:
foreach content_status:
if can view_type_status:
statuses[]= content_status
(content_type=content_type AND status IN( statuses ) )
AND type+status collections

We'd need to adjust the above with a user qualification, too, in the
event that the user can (or cannot) view certain combinations of
type+status by group (admins can see all drafts, authors see only
their own drafts, for example).

The wildcard in this instance might not be necessary, I guess.

Cheers,
Scott

Chris J. Davis

unread,
Apr 25, 2008, 8:49:22 AM4/25/08
to habar...@googlegroups.com
Sounds good to me, as long as it wouldn't needlessly complicate the
code, which I am assuming is the exact opposite of the intent of this
change/addition.

Chris

Scott Merrill

unread,
Apr 25, 2008, 10:28:49 PM4/25/08
to habar...@googlegroups.com
On Fri, Apr 25, 2008 at 8:39 AM, Scott Merrill <ski...@skippy.net> wrote:
> We'd need to adjust the above with a user qualification, too, in the
> event that the user can (or cannot) view certain combinations of
> type+status by group (admins can see all drafts, authors see only
> their own drafts, for example).

I've been thinking pretty much all day about ways to adjust our ACL
implementation to better handle our current system.

One very compelling idea is to add a few more columns to the
permission table so that we describe in the table the kind of
permissions we want to have. Consider something like the following:
CREATE TABLE permissions
group_id ...
action ...
object ...
owner ...
extra

This would allow us to do things like:
User::can( 'view', 'entry', 'any', 'published')
User::can( 'edit', 'page', 'self', 'draft')
User::can( 'edit', 'entry', 'self', 'scheduled')
User::can( 'create', 'user')
User::can( 'activate', 'plugins')

I think this avoids the problem of trying to build compound strings
for permission checks. It allows us to build an array of permissions
for the current user that we could parse fairly easily, based on its
constituent parts.

It also makes it easier to build UI for assigning permissions: we
could provide a row of drop-down menus corresponding to each column in
the permissions table with which to build the permissions you assign
to a group.

Thoughts? What columns might we need in this table?

Owen Winkler

unread,
Apr 26, 2008, 12:26:27 PM4/26/08
to habar...@googlegroups.com
Scott Merrill wrote:
>
> One very compelling idea is to add a few more columns to the
> permission table so that we describe in the table the kind of
> permissions we want to have. Consider something like the following:
> CREATE TABLE permissions
> group_id ...
> action ...
> object ...
> owner ...
> extra

>

> Thoughts? What columns might we need in this table?

I like the idea of adding a set of qualifiers to the permissions, but
I'm unsure about adding some predefined number of fields to the
permissions table which would then either limit or be more than what
we'd need for each permission.

I think that the order of the values is insignificant: 'view' could be
anywhere in the chain of permissions, not necessarily the first one.
The only caveat would be that each permission name would need to be
unique among all permission sets. So for example, 'view' couldn't mean
both 'view posts' and 'view comments', there would need to be separate
permission tokens for each of these.

If so, then we could instead have permissions that belong to a
permissions set:

CREATE TABLE permissions
permission_id,
permission_name,
permission_set

Using this, we might be able to construct sets of permissions without
having to build explicit fields to store them, and they could be of
arbitrary length.

To be clear about usage with $user->can(), one of groups that the user
has membership to would need to have a permission set that contains all
of the qualifiers in the call. So to qualify for this:

$user->can('view', 'entry', 'any', 'published')

They would need to belong to a group that has a permission set that
contains all of those permission tokens. If a group does qualify for
that permission, then they automatically qualify for:

$user->can('view', 'entry', 'any')

Is that a problem?

Using this method, would there be a way to say "Can the group view an
entry that belongs to anyone that is either published or draft?"

Just throwing this embryonic idea out there to see if that's helpful.

Owen

Scott Merrill

unread,
Apr 26, 2008, 12:54:21 PM4/26/08
to habar...@googlegroups.com
> I like the idea of adding a set of qualifiers to the permissions, but
> I'm unsure about adding some predefined number of fields to the
> permissions table which would then either limit or be more than what
> we'd need for each permission.

> The only caveat would be that each permission name would need to be


> unique among all permission sets. So for example, 'view' couldn't mean
> both 'view posts' and 'view comments', there would need to be separate
> permission tokens for each of these.

Why is that? I don't see the prohibition against using the same verb
on separate objects?

> If so, then we could instead have permissions that belong to a
> permissions set:
>
> CREATE TABLE permissions
> permission_id,
> permission_name,
> permission_set
>
> Using this, we might be able to construct sets of permissions without
> having to build explicit fields to store them, and they could be of
> arbitrary length.

What is permission_set in the schema above? Is it serialized data, or
a reference to some other db table?

> To be clear about usage with $user->can(), one of groups that the user
> has membership to would need to have a permission set that contains all
> of the qualifiers in the call. So to qualify for this:
>
> $user->can('view', 'entry', 'any', 'published')

If data is stored in the db in no prescribed format, then perhaps we
should extend the same flexibility to can()?
$user->can( array( 'status' => 'published', 'owner' => 'any', 'type'
=> 'entry', 'action' => 'view' )

> They would need to belong to a group that has a permission set that
> contains all of those permission tokens. If a group does qualify for
> that permission, then they automatically qualify for:
>
> $user->can('view', 'entry', 'any')
>
> Is that a problem?

Yes, I think that's a problem. If we assign the former permission to
guests so that they can view published entries, we don't want the
latter to grant them permission to see all drafts!

> Using this method, would there be a way to say "Can the group view an
> entry that belongs to anyone that is either published or draft?"

It was my expectation that we'd have separate permission entries for
each type+status you're allowed to see. Then also another set of
permissions for editing, deleting, etc.

Cheers,
Scott

Owen Winkler

unread,
Apr 26, 2008, 4:58:30 PM4/26/08
to habar...@googlegroups.com
Scott Merrill wrote:
>> I like the idea of adding a set of qualifiers to the permissions, but
>> I'm unsure about adding some predefined number of fields to the
>> permissions table which would then either limit or be more than what
>> we'd need for each permission.
>
>> The only caveat would be that each permission name would need to be
>> unique among all permission sets. So for example, 'view' couldn't mean
>> both 'view posts' and 'view comments', there would need to be separate
>> permission tokens for each of these.
>
> Why is that? I don't see the prohibition against using the same verb
> on separate objects?

If the verb was checked alone for some reason, it might not have the
implication assumed. For example, simply checking for "view" without
also checking for "entry" or "comment" would return true if you had
either of those combinations.

>> If so, then we could instead have permissions that belong to a
>> permissions set:
>>
>> CREATE TABLE permissions
>> permission_id,
>> permission_name,
>> permission_set
>>
>> Using this, we might be able to construct sets of permissions without
>> having to build explicit fields to store them, and they could be of
>> arbitrary length.
>
> What is permission_set in the schema above? Is it serialized data, or
> a reference to some other db table?

It is a set of grouped permissions. the following rows would indicate a
permission set of "edit" and "entry":

permission_id, permission_name, permission_set
1, 'edit', 1
2, 'entry', 1

We could also normalize this table to put the permission strings in an
external table.

>> To be clear about usage with $user->can(), one of groups that the user
>> has membership to would need to have a permission set that contains all
>> of the qualifiers in the call. So to qualify for this:
>>
>> $user->can('view', 'entry', 'any', 'published')
>
> If data is stored in the db in no prescribed format, then perhaps we
> should extend the same flexibility to can()?
> $user->can( array( 'status' => 'published', 'owner' => 'any', 'type'
> => 'entry', 'action' => 'view' )

This would be interesting, although I am not sure if this is necessary
(maybe it is) and would make things more complicated, as you've
demonstrated with the code for this check.

In any case, to affect this, just augment my previous table structure to:

CREATE TABLE permissions
permission_id,
permission_type,
permission_name,
permission_set

I guess my thought is that if all "status" identifiers are unique across
all permissions (that "published" isn't ever also a value for the
"type", for example), then we don't really need to indicate that
"published" is a status. This presents a problem with "any", since it
could apply to many permission segments, like "any type" or "any owner".

Or maybe we could just use "status_published" and "owner_any" as
identifiers outright?

>> They would need to belong to a group that has a permission set that
>> contains all of those permission tokens. If a group does qualify for
>> that permission, then they automatically qualify for:
>>
>> $user->can('view', 'entry', 'any')
>>
>> Is that a problem?
>
> Yes, I think that's a problem. If we assign the former permission to
> guests so that they can view published entries, we don't want the
> latter to grant them permission to see all drafts!

Right. But you would never use $user->can('view', 'entry', 'any') to
detect if a user could view drafts. You'd only use it to detect if they
can view entries of any kind written by other users. If you want to
detect if they have permission specifically to see drafts by other
users, you'd use $user->can('view', 'entry', 'any', 'draft'), which
would not match a permission set containing only 'view', 'entry', and 'any'.

To take this further, it might be useful to use an array to include
optional "or" values for a single permission setting. For example, to
detect whether a user can view anyone's entries OR pages:

$user->can('view', array('entry','page'), 'any')

Thinking aloud again, it would be useful to have a syntax that would
allow exclusions, like "anything but a page". It would also be useful
to have a syntax that would check for group membership on owners, like
"anyone in the admin group". And then combine them, "anyone not in the
admin group".

>> Using this method, would there be a way to say "Can the group view an
>> entry that belongs to anyone that is either published or draft?"
>
> It was my expectation that we'd have separate permission entries for
> each type+status you're allowed to see. Then also another set of
> permissions for editing, deleting, etc.

Yes, you'd do that with what I suggest, too. I was just suggesting a
way to do it without having specific slots (fields) for specific
criteria. This may be useful for using permissions for things outside
of posts that may not require these extra fields, or may require fields
other than those specific ones.

Owen

Scott Merrill

unread,
Apr 26, 2008, 5:32:06 PM4/26/08
to habar...@googlegroups.com
> >> The only caveat would be that each permission name would need to be
> >> unique among all permission sets. So for example, 'view' couldn't mean
> >> both 'view posts' and 'view comments', there would need to be separate
> >> permission tokens for each of these.
> >
> > Why is that? I don't see the prohibition against using the same verb
> > on separate objects?
>
> If the verb was checked alone for some reason, it might not have the
> implication assumed. For example, simply checking for "view" without
> also checking for "entry" or "comment" would return true if you had
> either of those combinations.

Ah, right. I understand. It was my understanding that one should
never leave the object empty in the permission check, but I can't
provide a good argument as to why this should be.

> >> If so, then we could instead have permissions that belong to a
> >> permissions set:
> >>
> >> CREATE TABLE permissions
> >> permission_id,
> >> permission_name,
> >> permission_set
> >>
> >> Using this, we might be able to construct sets of permissions without
> >> having to build explicit fields to store them, and they could be of
> >> arbitrary length.
> >
> > What is permission_set in the schema above? Is it serialized data, or
> > a reference to some other db table?
>
> It is a set of grouped permissions. the following rows would indicate a
> permission set of "edit" and "entry":
>
>
> permission_id, permission_name, permission_set
> 1, 'edit', 1
> 2, 'entry', 1
>
> We could also normalize this table to put the permission strings in an
> external table.

Ah yes, I understand. So the reference table would look something like this:
id,
permission_component,
component_value

With entries like the following:
1, verb, view
2, object, entry
3, object, comment
4, status, draft
5, status, published

Is that what you had in mind? The Post class can create the necessary
component entries when registering content types and statuses.

> >> To be clear about usage with $user->can(), one of groups that the user
> >> has membership to would need to have a permission set that contains all
> >> of the qualifiers in the call. So to qualify for this:
> >>
> >> $user->can('view', 'entry', 'any', 'published')
> >
> > If data is stored in the db in no prescribed format, then perhaps we
> > should extend the same flexibility to can()?
> > $user->can( array( 'status' => 'published', 'owner' => 'any', 'type'
> > => 'entry', 'action' => 'view' )
>
> This would be interesting, although I am not sure if this is necessary
> (maybe it is) and would make things more complicated, as you've
> demonstrated with the code for this check.

If some permissions have differing number of arguments, using the
above -- or a querystring -- would be one way to avoid passing NULLs
in the calls. It's probably not a sufficiently big deal.

> I guess my thought is that if all "status" identifiers are unique across
> all permissions (that "published" isn't ever also a value for the
> "type", for example), then we don't really need to indicate that
> "published" is a status. This presents a problem with "any", since it
> could apply to many permission segments, like "any type" or "any owner".
>
> Or maybe we could just use "status_published" and "owner_any" as
> identifiers outright?

Yuck. Back to using compound strings, which seem overly brittle to me.

> >> They would need to belong to a group that has a permission set that
> >> contains all of those permission tokens. If a group does qualify for
> >> that permission, then they automatically qualify for:
> >>
> >> $user->can('view', 'entry', 'any')
> >>
> >> Is that a problem?
> >
> > Yes, I think that's a problem. If we assign the former permission to
> > guests so that they can view published entries, we don't want the
> > latter to grant them permission to see all drafts!
>
> Right. But you would never use $user->can('view', 'entry', 'any') to
> detect if a user could view drafts. You'd only use it to detect if they
> can view entries of any kind written by other users. If you want to
> detect if they have permission specifically to see drafts by other
> users, you'd use $user->can('view', 'entry', 'any', 'draft'), which
> would not match a permission set containing only 'view', 'entry', and 'any'.

I was thinking of how the Posts::get() query would be built using this
new permissions structure. I suppose it's acceptable to require that
certain minimum criteria be passed to the method such that you need to
specifically ask for what you want.

> To take this further, it might be useful to use an array to include
> optional "or" values for a single permission setting. For example, to
> detect whether a user can view anyone's entries OR pages:
>
> $user->can('view', array('entry','page'), 'any')

I think this makes it more complicated for little specific benefit.
Plus, I generally dislike serialized arrays stored in the DB.

> Thinking aloud again, it would be useful to have a syntax that would
> allow exclusions, like "anything but a page". It would also be useful
> to have a syntax that would check for group membership on owners, like
> "anyone in the admin group". And then combine them, "anyone not in the
> admin group".

Again, I think this is making things overly complex. Yes, I'm sure
there might be some situations in which the above would be a good
idea, but I think (without much investigation) that these are less
frequent a case.


Cheers,
Scott

Owen Winkler

unread,
Apr 27, 2008, 12:07:14 AM4/27/08
to habar...@googlegroups.com
Scott Merrill wrote:
>
> Ah yes, I understand. So the reference table would look something like this:
> id,
> permission_component,
> component_value
>
> With entries like the following:
> 1, verb, view
> 2, object, entry
> 3, object, comment
> 4, status, draft
> 5, status, published
>
> Is that what you had in mind? The Post class can create the necessary
> component entries when registering content types and statuses.

Yes. I was originally in favor of omitting the permission_component
field, but it will be useful when using permissions on calls to
Posts::get(), since that's the only place I can think that you would
need to know the names of things in order to join the tables properly.
Maybe.

> If some permissions have differing number of arguments, using the
> above -- or a querystring -- would be one way to avoid passing NULLs
> in the calls. It's probably not a sufficiently big deal.

My concern was more with the storage of the permissions. I'd rather not
have a four-field-wide table and be limited to 4 fields or have to
insert nulls in ones we weren't using. I think it'll be better instead
to have a table of "permission sets" as I described previously.

Primarily, and this is a point I keep failing to make, I think that in
most cases, it's not necessary to specify the permission_component in
the request for the permission. So it's not necessary to include the
"object" and "verb" in $user->can(array('object'=>'entry',
'verb'=>'view') but simply use $user->can('entry', 'view').

In that case, a permission is granted only when the values in any
permission set assigned to a group intersected with the permission set
required for a task includes all of the elements in the set required for
a task.

So if P is the required permission set, and S is a permission set that
belongs to a group having many permission sets T, then the permission is
granted when this is true:

∃ S ∈ T such that P ∩ S = P

Calls to $user->can() may require some keys to be specified, because
'object'=>'entry' is a different id from 'foo'=>'entry' (which becomes
more significant when applying 'any' in a set, as mentioned below) and
there would be no other way to distinguish which one was needed for the
set to perform this intersection.

This is what I was rambling about before meaning that all of these
component values would need to be unique. If we didn't have the
permission component and component value, but instead just the component
value, then we wouldn't need to worry about that. I'm sure this seems
obvious, but it wasn't when I was thinking about it from a different
direction.

I think I'm mostly talking this out to myself, so if this is all plain
to you, or I'm not making sense, it might not matter. But then again,
maybe it's useful to your own thoughts.

>
> Yuck. Back to using compound strings, which seem overly brittle to me.

I was only considering their use to disambiguate, not to enforce a
behavior or as part of some crazy, unmentioned, complex string-parsing
permission system. No, I think we're all sane here, I'm just trying to
come up with ways to make things clearer for someone who reads the code.

> I was thinking of how the Posts::get() query would be built using this
> new permissions structure. I suppose it's acceptable to require that
> certain minimum criteria be passed to the method such that you need to
> specifically ask for what you want.

I'd like to hear your thoughts on this.

>> To take this further, it might be useful to use an array to include
>> optional "or" values for a single permission setting. For example, to
>> detect whether a user can view anyone's entries OR pages:
>>
>> $user->can('view', array('entry','page'), 'any')
>
> I think this makes it more complicated for little specific benefit.
> Plus, I generally dislike serialized arrays stored in the DB.

I think perhaps you misread what I was attempting. The intent was to
express a way to check if a user had any of two permission sets.

$user->can('view', array('entry','page'), 'any') would return true if
the user was in a group that has either of these sets:
view,entry,any
view,page,any

To check a permission for viewing of any content type by any user, you'd
simply call:

$user->can('view')

The 'any' isn't needed, since just 'view' will match any superset that
contains it.

What would be more difficult is to construct a permission set that is
associated to a user that returns true for any case that is more
specific. For example, if you wanted to construct a single permission
set that returned true for all of these:

$user->can('view', 'entry');
$user->can('view', 'post');
$user->can('view', 'aside');
...

Basically, giving permission to this user to view anything. How would
this be done? Assigning a permission set of simply "view,any" to a
group wouldn't be useful because then the test would need to both
specify the explicit case and any generic case, when it should really
only have to test for what it really wants to know:

if(
$user->can('view', 'entry') ||
$user->can( array('view', 'contenttype'=>'any'))
) ...

Rather than simply:

if( $user->can('view', 'entry') ) ...

Much of this might be able to be sorted out transparently in the can()
method or supporting functions, but it's something to consider.

Owen


Scott Merrill

unread,
Apr 28, 2008, 7:54:30 AM4/28/08
to habar...@googlegroups.com
> In that case, a permission is granted only when the values in any
> permission set assigned to a group intersected with the permission set
> required for a task includes all of the elements in the set required for
> a task.
>
> So if P is the required permission set, and S is a permission set that
> belongs to a group having many permission sets T, then the permission is
> granted when this is true:
>
> ∃ S ∈ T such that P ∩ S = P

Let's work through a few real world examples culled from our current
code base, in order to make this make more sense to me.

What would the permission set look like for the following:
* user can activate plugins
* user can create users
* user cannot modify other users' accounts
* user can view log entries
* user cannot delete log entries
* user can create entries
* user can only edit their own draft entries
* user can save entries with a status of "draft"

> Calls to $user->can() may require some keys to be specified, because
> 'object'=>'entry' is a different id from 'foo'=>'entry' (which becomes
> more significant when applying 'any' in a set, as mentioned below) and
> there would be no other way to distinguish which one was needed for the
> set to perform this intersection.

> This is what I was rambling about before meaning that all of these
> component values would need to be unique. If we didn't have the
> permission component and component value, but instead just the component
> value, then we wouldn't need to worry about that. I'm sure this seems
> obvious, but it wasn't when I was thinking about it from a different
> direction.

We could, perhaps, qualify calls to can() through the object executing
the request. We know that calls to User::can() from within the
Post(s) object will almost always be dealing with the post table. As
such, the "any" term can be applied (currently) to:
* content type
* content status
* content author

Moreover, any ACL check for a "view" action will, by necessity, be
restricted to the Posts table. So I think we could re-use the same
verbs across different objects with some flexibility. ACL checks on
$post->comments would be triggered by the Comment(s) class, so a
"view" check executed there would be constrained by the appropriate
class to the appropriate data type and any metadata.

> > I was thinking of how the Posts::get() query would be built using this
> > new permissions structure. I suppose it's acceptable to require that
> > certain minimum criteria be passed to the method such that you need to
> > specifically ask for what you want.
>
> I'd like to hear your thoughts on this.

I was thinking that perhaps Posts::get() should require one to specify
a content type or status (with "any" being permissible values) so that
the query is built up programmaticly based on the request, rather than
having default values assumed within the method.

Looking through Posts::get(), if one does not specifically request a
content type or status, one gets _everything_, and we leave it to the
calling code to filter out that which we don't want. I think that
Posts::get() should enforce ACLs automatically, so that in the absence
of a specific content type or status, it returns only that data which
you are permitted to see.

If we don't do this, we make it easy for plugins to circumvent the ACL
restrictions. Maybe this isn't that big of a deal, but seems
short-sighted to me.

> >> To take this further, it might be useful to use an array to include
> >> optional "or" values for a single permission setting. For example, to
> >> detect whether a user can view anyone's entries OR pages:
> >>
> >> $user->can('view', array('entry','page'), 'any')
> >
> > I think this makes it more complicated for little specific benefit.
> > Plus, I generally dislike serialized arrays stored in the DB.
>
> I think perhaps you misread what I was attempting. The intent was to
> express a way to check if a user had any of two permission sets.
>
> $user->can('view', array('entry','page'), 'any') would return true if
> the user was in a group that has either of these sets:
> view,entry,any
> view,page,any

I don't see that as particularly useful, because it's only a matter of
time before we add in an AND combination. My personal preference is
to keep the calling code clean and self-evident, so that one skimming
the code knows that an OR or an AND operation is being specifically
queried. But if the majority feels that the above is super convenient
and worth doing, I won't fuss.

> To check a permission for viewing of any content type by any user, you'd
> simply call:
>
> $user->can('view')
>
> The 'any' isn't needed, since just 'view' will match any superset that
> contains it.

This is what I dislike, still. Again, let's look at the case of a
real-world query.

When building up the Posts::get() query, I assume we're going to
iterate over the current user's list of permitted content type +
status combinations to build a query that specifically fetches things
the user is permitted to see.

Using an ACL check of can('view') is too coarse a check to
meaningfully determine which stuff to fetch for this user.

In what other instances would a single verb be useful? Activating
plugins and themes, perhaps. What else? Anything?

> What would be more difficult is to construct a permission set that is
> associated to a user that returns true for any case that is more
> specific. For example, if you wanted to construct a single permission
> set that returned true for all of these:
>
> $user->can('view', 'entry');
> $user->can('view', 'post');
> $user->can('view', 'aside');
> ...
>
> Basically, giving permission to this user to view anything. How would
> this be done? Assigning a permission set of simply "view,any" to a
> group wouldn't be useful because then the test would need to both
> specify the explicit case and any generic case, when it should really
> only have to test for what it really wants to know:
>
> if(
> $user->can('view', 'entry') ||
> $user->can( array('view', 'contenttype'=>'any'))
> ) ...
>
> Rather than simply:
>
> if( $user->can('view', 'entry') ) ...

I'm confused: if you want to view entries, why are you ORing the first
example with "any" content type after you've just checked that they
can view the specific content type for which you're checking?

I apologize if I'm making overly complex something that appears simple
and straightforward to you. This is neither simple nor
straightforward for me.

And I would very much like to see input from others reading this thread.

Cheers,
Scott

Owen Winkler

unread,
Apr 28, 2008, 9:26:21 AM4/28/08
to habar...@googlegroups.com
Scott Merrill wrote:
>
> What would the permission set look like for the following:
> * user can activate plugins
activate,plugins

> * user can create users

create,users

> * user cannot modify other users' accounts

modify,users,self (unassigned: modify,users,user=>any)

> * user can view log entries

view,logs

> * user cannot delete log entries

unassigned: delete,logs

> * user can create entries

create,entry

> * user can only edit their own draft entries

edit,entry,draft,user=>self

> * user can save entries with a status of "draft"

create,entry,draft

With the large set of options, wildcards start to look appealing. More
on that idea is below.

> We could, perhaps, qualify calls to can() through the object executing
> the request. We know that calls to User::can() from within the
> Post(s) object will almost always be dealing with the post table. As
> such, the "any" term can be applied (currently) to:

I'm assuming you're talking about using reflection for this, and
although that may reduce the complexity of the call to ->can(), it
obscures what it's doing just a bit. Plus, as you say, it's "almost
always" -- so in those cases that it's not, we'd have to specify anyway.
It seems to me that writing the extra code (the reflection stuff)
doesn't get us much.

> Moreover, any ACL check for a "view" action will, by necessity, be
> restricted to the Posts table. So I think we could re-use the same
> verbs across different objects with some flexibility. ACL checks on
> $post->comments would be triggered by the Comment(s) class, so a
> "view" check executed there would be constrained by the appropriate
> class to the appropriate data type and any metadata.

I think you're correct in that the check will involve an additional
parameter, but I think we'll need to specify it explicitly as I
mentioned above. So within the Comment class, a "view" check on
comments would require also a parameter "comment" to disambiguate it,
ala "view,comment" which is a different permission set than
"view,post,entry".

When you break it down, the permission will need to be stored in the
database with the type data anyway.

> I was thinking that perhaps Posts::get() should require one to specify
> a content type or status (with "any" being permissible values) so that
> the query is built up programmaticly based on the request, rather than
> having default values assumed within the method.

I'm curious why you think we can't/shouldn't specify reasonable defaults
for Posts::get() that would return something even when specific values
aren't requested. If the default is to return everything, then it could
still apply appropriate permissions filters to what it returns. Why
require specific parameters, especially if all of them would need to
have the option of being "any"?

> Looking through Posts::get(), if one does not specifically request a
> content type or status, one gets _everything_, and we leave it to the
> calling code to filter out that which we don't want. I think that
> Posts::get() should enforce ACLs automatically, so that in the absence
> of a specific content type or status, it returns only that data which
> you are permitted to see.

The only issue I see with that is when we use Posts::get() to
programmatically return posts to which you need access internally, but
shouldn't have direct access to display or edit.

I was discussing with someone on IRC the other day about using
Posts::get() to return a set of posts without having to use a direct
database query. In this particular instance, ACL wouldn't have applied
since the call was internal. We'll probably want to take that into
consideration, since our API would presumably make it easier to fetch
needed posts than having to write out queries to do so.

> If we don't do this, we make it easy for plugins to circumvent the ACL
> restrictions. Maybe this isn't that big of a deal, but seems
> short-sighted to me.

We should certainly allow plugins to circumvent ACL restrictions.

A good example of where this would be required is in a plugin that
itself implements a new approval workflow. It would need to have
unfettered access to retrieve posts outside of the normal permission
processing.

>> To check a permission for viewing of any content type by any user,
you'd
>> simply call:
>>
>> $user->can('view')
>>
>> The 'any' isn't needed, since just 'view' will match any superset that
>> contains it.
>
> This is what I dislike, still. Again, let's look at the case of a
> real-world query.
>
> When building up the Posts::get() query, I assume we're going to
> iterate over the current user's list of permitted content type +
> status combinations to build a query that specifically fetches things
> the user is permitted to see.
>
> Using an ACL check of can('view') is too coarse a check to
> meaningfully determine which stuff to fetch for this user.
>
> In what other instances would a single verb be useful? Activating
> plugins and themes, perhaps. What else? Anything?

I don't know in what instance the call I wrote would be useful. I was
simply illustrating that a call to ask "Does this user have permission
to view anything?" would not need to include "contenttype=>any", for
example. Omitting that as part of the check implies that a permission
set that includes any content type (or no content type) permits the user
to view anything.

This is a simple example that becomes important with more complex
checks. For example, a check for
$user->can('view','post','entry','draft') would return true if the user
had a permission set 'view,post,entry,draft,self', because the check is
in the powerset of the permission set associated to the user.

The original was more for a demonstration of how the system would
operate than a demonstration of its practical use with a real-world
example. I can't immediately think of any reason why you'd want to know
if a user can view anything, no matter what it is.

I think it's important to mention now that I think it may be impractical
to use calls to can() within the Post::get() to build queries that
retrieve only the permitted items. The problem I have is not with
Post::get() returning permitted results, but with the use of the method
can(). I think there's no way we can adequately cover all of the
possibilities simply by creating a lot of if($user->can()) statements
within Posts::get(). An alternative may need to be concocted. I just
don't know - I was hoping you'd have an idea.

To that extent, whenever I have referred to a check of $user->can() or
the use of the can() function, it has only been to check a permission,
not to fetch results from Posts::get().

This is the reason I was asking in my last message for your ideas on how
to do that, because I think that it's going to be more complicated doing
it with can() than might be possible or, at least, useful.

>> What would be more difficult is to construct a permission set that is
>> associated to a user that returns true for any case that is more
>> specific. For example, if you wanted to construct a single permission
>> set that returned true for all of these:
>>
>> $user->can('view', 'entry');
>> $user->can('view', 'post');
>> $user->can('view', 'aside');
>> ...
>>
>> Basically, giving permission to this user to view anything. How would
>> this be done? Assigning a permission set of simply "view,any" to a
>> group wouldn't be useful because then the test would need to both
>> specify the explicit case and any generic case, when it should really
>> only have to test for what it really wants to know:
>>
>> if(
>> $user->can('view', 'entry') ||
>> $user->can( array('view', 'contenttype'=>'any'))
>> ) ...
>>
>> Rather than simply:
>>
>> if( $user->can('view', 'entry') ) ...
>
> I'm confused: if you want to view entries, why are you ORing the first
> example with "any" content type after you've just checked that they
> can view the specific content type for which you're checking?

My goal at the start (from where I've included my quote) was to build a
single permission that says that I have access to ANY content type,
rather than adding individual permission sets for every possible content
type.

So instead of having all of these:
view,entry
view,page (previous was error)
view,aside

I would have a single "admin" permission set:
view,contenttype=>any

This would be useful for an administrator account who would have access
to view any extant content type, and any content type added in the future.

The problem is that there isn't a way for the can() function to match
'entry' (for example) to 'any'. So if I am permitted the single admin
permission set above, and this call is made:

$user->can('view', 'entry')

Does it return true? I would hope that it would because I have a
'contenttype'=>'any' in my permission set, but unless we've got some
special case inside can() to deal with that, then that wouldn't be the case.

So in order to check if my permissions are ok, I would need to check
both the specific case (do I have view access to 'entry' types?) and the
wildcard case (do I have access to any type?), ala:

$user->can('view', 'entry') ||

$user->can(array('view', 'contenttype'=>'any'))

As a developer, what I really want to do is only the $user->can('view',
'entry') call, since the 'any' should match that (if that's our
wildcard). But unless there is a special case, we can't do that. So
the point of this whole three-message mess is that there needs to be a
special case that handles matching specific values requested in code
against wildcards stored in the user's permissions.

To try to clarify, $user->can(array('view','contenttype'=>'any')) is
true ONLY if the user can view ALL content types. It is not true if the
user can view only one content type.

If you wanted to test if the user had access to view anything, where any
content type qualified them as passing, you'd omit the content type
altogether, so that any permission set containing 'view' would match and
return true:

$user->can('view')

Just to complicate things more...
It occurs to me that by adding more tokens to a permission set than you
normally would, you could affect multiple permissions without actually
specifying a tedious number of sets. For example, these permission sets:

view,entry
edit,entry,others
edit,entry,self
create,entry

Could all be replaced with this single set:

view,edit,create,entry,others,self

Because all of the explicit sets above are members of the powerset of
this strange permission set. So maybe that's a way around the "any"
special case? Not really, because it doesn't grant access to new
content types as they're added. There are some other problems with
this, too, that probably aren't really worth covering. Yeah, yeah, you
don't see how this would be useful, I just thought it was of
intellectual interest.

I keep using this term, maybe I should define it:
http://en.wikipedia.org/wiki/Power_set

My classes on set theory are finally proving useful.

> I apologize if I'm making overly complex something that appears simple
> and straightforward to you. This is neither simple nor
> straightforward for me.

This is not a simple topic, and I'm still feeling for answers. If it
was easy, we'd have it done already. ;)

> And I would very much like to see input from others reading this thread.

Yes please. Need more brains.

Owen

Scott Merrill

unread,
Apr 28, 2008, 10:09:29 AM4/28/08
to habar...@googlegroups.com
> I think it's important to mention now that I think it may be impractical
> to use calls to can() within the Post::get() to build queries that
> retrieve only the permitted items. The problem I have is not with
> Post::get() returning permitted results, but with the use of the method
> can(). I think there's no way we can adequately cover all of the
> possibilities simply by creating a lot of if($user->can()) statements
> within Posts::get(). An alternative may need to be concocted. I just
> don't know - I was hoping you'd have an idea.
>
> To that extent, whenever I have referred to a check of $user->can() or
> the use of the can() function, it has only been to check a permission,
> not to fetch results from Posts::get().

As I've said elsewhere, it's been my assumption that Posts::get()
would iterate over those types and statuses to which the user has
permission for the current action.

We have a finite number of pre-defined things to check (content type,
status, author, group, etc). When Habari starts, it can (and should)
prepare a cached copy of the current user's permissions. Determining
the intersections between what's possible and what's permitted ought
not be overly complex.

> This is the reason I was asking in my last message for your ideas on how
> to do that, because I think that it's going to be more complicated doing
> it with can() than might be possible or, at least, useful.

The first message in this thread has some psuedocode. It's probably
not ideal, but it seems the most logical way to proceed: start with
some minimal query and then consecutively add parameters based on the
user's permissions.

There might be a way to short-circuit this complexity. We have a
finite list of content types and statuses, as well as comment types
and statuses. For the ability to read a published item of any content
type, you could use
$user->can('view', 'content', 'published')
Then we could iterate over each defined content type. The same could
hold true for comments. A way to succinctly describe any content or
comment status currently eludes me, but I'm sure it's possible.

> Could all be replaced with this single set:
>
> view,edit,create,entry,others,self
>
> Because all of the explicit sets above are members of the powerset of
> this strange permission set. So maybe that's a way around the "any"
> special case? Not really, because it doesn't grant access to new
> content types as they're added.

I object to the notion of automatically applying permission to new
content types. If a plugin registers a new content type or status,
the plugin has no way to know how the local administrator is doling
out permissions. Just because we might default to roles of "guest,
author, editor, administrator" doesn't mean that everyone will use
those roles.

I think that new content types should not have permissions
automatically assigned, and that the local admin should do this
manually.

Cheers,
Scott

Owen Winkler

unread,
Apr 28, 2008, 10:48:07 AM4/28/08
to habar...@googlegroups.com
Scott Merrill wrote:
>
> We have a finite number of pre-defined things to check (content type,
> status, author, group, etc). When Habari starts, it can (and should)
> prepare a cached copy of the current user's permissions. Determining
> the intersections between what's possible and what's permitted ought
> not be overly complex.

That's true; however, transitioning what's permitted (stored in memory)
to what can be queried seems like a prime location for inefficiency to
crop up. I'd like to take the time now to avoid this if possible.

> There might be a way to short-circuit this complexity. We have a
> finite list of content types and statuses, as well as comment types
> and statuses. For the ability to read a published item of any content
> type, you could use
> $user->can('view', 'content', 'published')
> Then we could iterate over each defined content type. The same could
> hold true for comments. A way to succinctly describe any content or
> comment status currently eludes me, but I'm sure it's possible.

The ACL system (the classes that relate to it) should not be aware of
Habari-specific constructs. A well-designed ACL system will be
transportable to other projects, and couldn't include them.

There should be no query inside of the ACL system to determine what
permissions are available based on data structures outside of what ACL
uses. In other words, the ACL should have its own table of tokens, and
should not use the posttypes table to determine what types exist, since
it shouldn't know what posts or posttypes or comments etc. are at all.

In any case, doing what I described is not at all as complicated as
describing it.

> I object to the notion of automatically applying permission to new
> content types. If a plugin registers a new content type or status,
> the plugin has no way to know how the local administrator is doling
> out permissions. Just because we might default to roles of "guest,
> author, editor, administrator" doesn't mean that everyone will use
> those roles.
>
> I think that new content types should not have permissions
> automatically assigned, and that the local admin should do this
> manually.

Perhaps so. Conversely, as the operator of a single-user blog, I have
little interest in having to set permissions on every new content type I
add because there is no option to say that I have access to every
content type.

I agree that it should not be an automated process; Habari should not
automatically add access to any specific content type to any user
(except during install), but the ability to define a permission that
grants all access is essential to lowering the annoyance factor.

Besides that, having an all-access permission is something you had
advocated long ago. ;)

Owen

Scott Merrill

unread,
Apr 28, 2008, 10:55:04 AM4/28/08
to habar...@googlegroups.com
> > I think that new content types should not have permissions
> > automatically assigned, and that the local admin should do this
> > manually.
>
> Perhaps so. Conversely, as the operator of a single-user blog, I have
> little interest in having to set permissions on every new content type I
> add because there is no option to say that I have access to every
> content type.
>
> I agree that it should not be an automated process; Habari should not
> automatically add access to any specific content type to any user
> (except during install), but the ability to define a permission that
> grants all access is essential to lowering the annoyance factor.
>
> Besides that, having an all-access permission is something you had
> advocated long ago. ;)

Having a supreme override "I can do anything" permission is distinct
from what I understood you to be discussing previously with the "any"
term for content types and statuses. I agree that a "full access"
privilege is a good idea. I haven't discussed it in this thread
because it should be the first check performed by the can() method and
short-circuit all other checks if it's assigned.

Owen Winkler

unread,
Apr 28, 2008, 11:27:57 AM4/28/08
to habar...@googlegroups.com

Ok, but I'd still like the ability to set up permissions so that a user
has access to do anything to any content type existing or in the future
without also giving them access to add new users. The system wouldn't
add these new permissions automatically. I would need to explicitly set
this in advance, but I would like that capability.

Owen

Chris J. Davis

unread,
Apr 28, 2008, 1:24:52 PM4/28/08
to habar...@googlegroups.com
I don't have any input as to how we implement the backend of this
feature, since I am not as knowledgable about ACLs. The system we
wrote for $work approached things a bit differently that what you are
both advocating, so I am very much out of my depth.

That being said, I am interested in how things are handled on the
user end, and once we start talking about that, I will be much more
interested, and helpful.

Chris

Matthijs

unread,
Apr 30, 2008, 3:43:08 AM4/30/08
to habari-dev
Have you guys looked at the normal way an ACL system is build? With
any Access control system I think it's important to have a very fine-
grained system, but you have to make sure it's not tightly coupled to
specific implementations, otherwise you're in trouble as soon as you
change one thing later.
Phpgacl has a good article explaining it
http://phpgacl.sourceforge.net/demo/phpgacl/docs/manual.html
as does Zend
http://framework.zend.com/manual/en/zend.acl.html

Matthijs

Andrew da Silva

unread,
May 24, 2008, 7:58:47 PM5/24/08
to habari-dev
Suggestion for the ACL can() method.

Have it work like Options::, where it fetches all available ACL in one
swoop, less queries used.

It may slow things down too if there are 9000 permissions and the
script only uses 5 of them, but it's a thought! (of course we'ld
filter the query by user and group)

Unless we just use cache, like you're planning on Posts::get().
Reply all
Reply to author
Forward
0 new messages