← Back to team overview

launchpad-dev team mailing list archive

[tech] ACL system

 

So, we want to do have an ACL system in Launchpad, to be able to have
private projects. I'm attaching acl.txt from
lp:~bjornt/launchpad/privacy-spike, which hopefully explains the system
good enough. Jono, is this document something that you can show
stakeholders, or would you prefer something. I'm hoping that the
overview part will be enough for them. I'm happy to extend it as needed.

I'm currently working on a proof-of-concept implementation in the
mentioned branch, to try out the API. I have something that more or less
works, and I'm currently trying to do performance testing, to see if the
database model make sense, or we have to come up with something else.

For the more interested, test_acl.py in that branch shows what is
currently imlemented and working.


-- 
Björn Tillenius | https://launchpad.net/~bjornt
====
ACLs
====

This document aims to explain how the ACL system works, and how a
programmer can integrate it into his code. It starts with an overview,
which explains what the goal of the ACL system is. For the more
interested parties, it then continues explaining the ACL API; how a
programmer would use it.

Overview
========

The ACL system is used to check whether some user has certain
permissions to do various things with an object. To have something more
concrete to talk about, lets say that we have a project, which has bugs.
By default, the project and its bugs are accessible to anyone. A
Launchpad Commercial Admin can grant the MODIFY_ACL permission to
someone in the project team. That person (or team) can now limit who can
have access to their project and its bugs. The can give people and teams
permission project-wide, to let someone see the whole project and its
bugs, or let someone to see only part of the project, for example only a
single bug. (Giving permission to a single bugs means that the user will
automatically get permission to see the name of the project, though).

Teams can be used to manage permissions across multiple projects. By
giving permission to a team, you can give others the same permission by
adding them to the team. This way you can easily give someone access to
multiple projects.

In addition to controlling permission project-wide, or for a single
bugs, it's also possible to control it for a larger part of the project.
For example, to have specific permission for the Bugs part of the
project, e.g. giving someone permission to change the importance of
all bugs, but not editing the project details.


Getting the ACL for an object
=============================

The ACL is retrieved by adapting an object to `IACL`::

    >>> from lp.services.acl.interfaces.acl import (
    ...     ACLPermission, EVERYONE, IACL)
    >>> from lp.testing.factory import LaunchpadObjectFactory

    >>> factory = LaunchpadObjectFactory()
    >>> project = factory.makeProduct()
    >>> project_acl = IACL(project)


Granting and revoking permission
================================

The code that creates the object is responsible for granting the initial
permissions. For the project above, the `VIEW` permission was granted to
everyone

    >>> alice = factory.makePerson()
    >>> bob = factory.makePerson()

    >>> VIEW = ACLPermission.VIEW
    >>> project_acl.has(VIEW, EVERYONE)
    True
    >>> project_acl.has(VIEW, alice)
    True
    >>> project_acl.has(VIEW, bob)
    True


If you need to limit who can view the project, you can grant permissions
to specific people or teams::

    >>> project_acl.grant(VIEW, bob)

    >>> project_acl.has(VIEW, alice)
    False
    >>> project_acl.has(VIEW, bob)
    True

    >>> project_acl.grant(VIEW, alice)
    >>> project_acl.has(VIEW, alice)
    True

If you need to revoke access to a project, the `revoke()` method is
used::

    >>> project_acl.revoke(VIEW, alice)
    >>> project_acl.has(VIEW, alice)
    False

Giving permissions to teams
---------------------------

To make it easier to handle ACLs across different projects, teams can be
used to group people together. This makes it possible to create a team
for a company group, add it to multiple project ACLs, and manage the
ACLs for all those project by adding and removing members. This also
makes it possible to delegate granting/revoking of ACL permissions to
the team admins, if needed.

    >>> team = factory.makeTeam()
    >>> project_acl.grant(VIEW, team)
    >>> project_acl.has(VIEW, alice)
    False

When someone joins or leaves the team, they will automatically get or
loose the permissions that the team has::

    >>> alice.join(team)
    >>> project_acl.has(VIEW, alice)
    True

    >>> alice.leave(team)
    >>> project_acl.has(VIEW, alice)
    False


Permission inheritence
======================

An object usually have a number of objects that belong to it, which
needs to be covered by the same ACLs as the main object. For example, a
project has bugs. If a project has some ACL rules, the same ACL rules
should apply for its bugs 

    >>> project = factory.makeProduct()
    >>> project_acl = IACL(project)
    >>> project_acl.grant(VIEW, alice)

    >>> bug_task = factory.makeBugTask(target=project)
    >>> bugtask_acl = IACL(bug_task)
    >>> bugtask_acl.has(VIEW, alice)
    True

    >>> bugtask_acl.has(VIEW, bob)
    False

Changing the project's ACL will propagate the changes to its bugs, since
they inherit the ACL from the project.

    >>> project_acl.grant(VIEW, bob)
    >>> bugtask_acl.has(VIEW, bob)
    True

Overriding child ACLs
---------------------

Sometimes there's a need to give someone access to see one bug, but not
the other bugs in the project. To do this it's possible to override the
ACLs, so that they no longer inherit them from the project.

    >>> karl = factory.makePerson()
    >>> bugtask_acl.grant(VIEW, karl)
    >>> bugtask_acl.has(VIEW, karl)
    True
    >>> project_acl.has(VIEW, karl)
    False

    >>> bugtask_acl.has(VIEW, bob)
    True

Note that in order for this to work in practice, giving someone access
to a bug only, means that they will also be allowed to see basic
information about the project, for example the project name.

There will be a way to see all the ACLs within a project that has been
ovrridden, so that it's possible to audit, who has permission to view
different parts of the project. In addition to this, it will also be
possible to see all the objects that a certain user has permission to
see.

When changing the project ACL, the changes will no longer be propagated
to the overridden asset by default. If desired, it's possible to
explicitly do this, though.


ACL when searching for objects
==============================

Here's a proof-of-concept of how ACLs can be automatically integrated
with searching.

Instead of getting collections of all object, you get a collection of
objects that a user has access to. This way you're forced to think about
which user is performing the search. There can also be a way of getting
all objects, in case of doing a search as a robot.

    >>> private_project = factory.makeProduct()
    >>> IACL(private_project).revoke(VIEW, EVERYONE)
    >>> IACL(private_project).grant(VIEW, alice)

    >>> from zope.component import getUtility
    >>> from lp.registry.interfaces.productcollection import IAllProducts
    >>> alice_collection = getUtility(IAllProducts).visibleByUser(alice)
    >>> bob_collection = getUtility(IAllProducts).visibleByUser(bob)
    >>> anon_collection = getUtility(IAllProducts).visibleByUser(None)

The returned collections are very simple, they don't yet do any real
searching. They do however automatically append ACL queries in the base
class. The real collection class should probably look something like
BranchCollection.

    >>> private_project in alice_collection.search()
    True
    >>> private_project in bob_collection.search()
    False
    >>> private_project in anon_collection.search()
    False

    >>> private_bugtask = factory.makeBugTask()
    >>> IACL(private_bugtask).revoke(VIEW, EVERYONE)
    >>> IACL(private_bugtask).grant(VIEW, alice)

    >>> from lp.bugs.interfaces.bugtaskcollection import IAllBugTasks
    >>> alice_collection = getUtility(IAllBugTasks).visibleByUser(alice)
    >>> bob_collection = getUtility(IAllBugTasks).visibleByUser(bob)
    >>> anon_collection = getUtility(IAllBugTasks).visibleByUser(None)

    >>> private_bugtask in alice_collection.search()
    True
    >>> private_bugtask in bob_collection.search()
    False
    >>> private_bugtask in anon_collection.search()
    False

Follow ups