The geocamFolder
Django app defines hierarchical folders and
folder-based row-level access control inspired by AFS.
The focus of the geocamFolder
app is the Folder
model. Each
Folder
has a name
and a parent
pointer. The parent
relationship defines a familiar folder hierarchy:
$ ./manage.py shell >>> from geocamFolder.models import Folder >>> Folder.mkdir('/foo') # saves a new Folder in the db <Folder: foo parent=root> >>> Folder.mkdir('/foo/bar') <Folder: bar parent=foo> >>> Folder.getFolder('/foo/bar') # fetches a Folder from the db <Folder: bar parent=foo>
Folders are intended to serve as containers for other database objects.
They can provide a familiar hierarchical way for your users to organize
their data. To make one of your models aware of folders, give it a
ForeignKey(Folder)
field named folder
and (optionally) make it
inherit from the FolderMember
mixin:
from django.db import models from geocamFolder.models import Folder, FolderMember class FolderAwarePosition(models.Model, FolderMember): x = models.FloatField() y = models.FloatField() folder = models.ForeignKey(Folder) def __unicode__(self): return 'x=%s y=%s' % (self.x, self.y)
Now you can place instances of your model in a Folder
:
$ ./manage.py shell >>> from geocamFolder.models import Folder >>> from myApp.models import FolderAwarePosition >>> p1 = FolderAwarePosition(x=0, y=1, folder=Folder.getRootFolder()) >>> p1.save() >>> p2 = FolderAwarePosition(x=3, y=7, folder=Folder.getRootFolder()) >>> p2.save() >>> FolderAwarePosition.objects.filter(folder=Folder.getRootFolder()).all() [<FolderAwarePosition: x=0.0 y=1.0>, <FolderAwarePosition: x=3.0 y=7.0>]
Besides organizing objects, the main purpose of folders is to provide a flexible form of row-level access control, which is the ability to grant users different permissions to different database objects, even different instances of the same model.
As of version 1.2, the built-in Django authorization system only
provides model-level access control. This means you can grant user
alice
permission to write
, insert
, or delete
all
instances of your model Foo
, but there's no way to grant alice
permission to write
some instances of Foo
but not others.
The geocamFolder
approach to row-level access control is to provide
each Folder
with an access control list (ACL) that grants access
privileges both to the Folder
itself and to any other objects
contained in the Folder. Here's an example:
$ ./manage.py shell >>> from geocamFolder.models import Folder, Action >>> root = Folder.getRootFolder() >>> root.getAcl() {u'group:anyuser': 'vl'} >>> from django.contrib.auth.models import User >>> alice = User.objects.create_user('alice', '[email protected]') >>> root.isAllowed(alice, Action.VIEW) True >>> root.isAllowed(alice, Action.CHANGE) False >>> admin = User.objects.filter(is_superuser=True)[0] >>> root.isAllowed(admin, Action.CHANGE) True
Let's pull the example apart. First, we asked what the ACL was for the
root folder. The answer was {u'group:anyuser': 'vl'}
, which means
that any user can VIEW
(v
) objects in the folder and LIST
(l
) subfolders of the folder. Those are the default permissions
for the root folder.
Then we created a new user alice
and checked her permissions. Like
all users, alice
can use any permissions granted to
group:anyuser
, so she can view the root folder, but she can't change
it. However, super-users can always do anything, regardless of the ACL,
so admin
can write to the root folder.
An access control list is a dictionary mapping agents to permission
sets. An agent is a user or a group (using the User
and Group
models of the built-in Django auth app), and a permission set is a
non-empty set of permissions from the following:
Permission Actions Controlled by Permission VIEW View objects in the folder LIST List subfolders of the folder ADD Add objects or subfolders to the folder DELETE Delete objects or subfolders from the folder CHANGE Change objects in the folder MANAGE Change the folder ACL
We have a standard string notation for permission sets: To show that a
permission is included in the set we put its first letter in the string,
so the permission set vl
includes VIEW
and LIST
permissions.
There are some standard permission sets that are used so often we give
them nicknames in the Actions
class:
Nickname Permission Set READ vl WRITE vladc ALL vladcm
Here's a typical ACL for the /groups/basinFire/public
folder.
Groups start with group:
to distinguish them from users.
Agent Permissions group:authuser vl (READ) group:basinFire vladc (WRITE) tjones vladc (WRITE) group:basinFireAdmins vladcm (ALL)
A user U has a permission P if P has been granted in any of the following ways:
- By user: There is an entry in the ACL granting P to U.
- By group: U is a member of a group G, and there is an entry in the ACL granting P to G. (Note that users can belong to multiple groups.)
- By special group: All registered users are considered to belong to
group:authuser
; by convention this membership is not recorded in the database. Similarly, all users, even guests who have not logged in, belong togroup:anyuser
.
Here's an advanced example of granting and revoking ACL permissions:
$ ./manage.py shell >>> from django.contrib.auth.models import User, Group >>> alice = User.objects.create_user('alice', '[email protected]') >>> basinFireUsers = Group.objects.create(name='basinFireUsers') >>> alice.groups.add(basinFireUsers) >>> from geocamFolder.models import Folder, Action, Actions >>> f = Folder.mkdir('/basinFire') >>> f.getAcl() # initial ACL inherited from parent folder {u'group:anyuser': 'vl'} >>> f.setPermissions(alice, Actions.WRITE) >>> f.getAcl() {u'alice': 'vladc', u'group:anyuser': 'vl'} >>> a = Folder.mkdirAssertAllowed(alice, '/basinFire/alice') >>> a.getAcl() # initial ACL inherited + ALL access granted to requesting user {u'alice': 'vladcm', u'group:anyuser': 'vl'} >>> f.setPermissions(alice, Actions.NONE) # revoke alice's write access >>> f.getAcl() {u'group:anyuser': 'vl'} >>> f.rmdirAssertAllowed(alice, '/basinFire/alice') # this won't work PermissionDenied: user alice does not have delete permission for folder basinFire >>> f.isAllowed(alice, Action.VIEW) # but alice can still view via group:anyuser True >>> f.setPermissions(basinFireUsers, 'vld') >>> f.getAcl() {u'group:anyuser': 'vl', u'group:basinFireUsers': 'vld'} >>> f.isAllowed(alice, Action.DELETE) # now alice has delete permission via group:basinFireUsers True
Note that many functions in the Folder
class have a "basic" and
"assert-allowed" version. The assert-allowed version takes the
requesting user as its first argument and checks that the user has
permission to perform the action (raising PermissionDenied
if not).
To enforce proper access control, code that runs within a Django view
and performs actions on behalf of a user should typically use the
assert-allowed version of the function with request.user
as the
first argument. Administrative scripts might use the basic version.
But this is only a convention and usage is entirely up to you.
[coming soon]
Some ACL systems have the ability to explicitly deny access by
specifying negative rights to users who would normally have access
through one of their group memberships. That feature is not supported
by geocamFolder
.