Role basics
-----------

Roles are managed through an IRoleRegistry utility which handles
IRole objects.  Roles have a name used for referencing them, an
informative title, an optional description, and also an optional
numeric key, suitable for creating references in relational
databases.

  >>> from zope.interface.verify import verifyObject

  >>> from canonical.security.interfaces import IRoleRegistry, IRole
  >>> from canonical.security.role import RoleRegistry, Role

  >>> registry = RoleRegistry()
  >>> role1 = Role("name1", "Title", "Description", 1)
  >>> role2 = Role("name2")

  >>> role1.name, role1.title, role1.description, role1.key
  ('name1', 'Title', 'Description', 1)
  >>> role2.name, role2.title, role2.description, role2.key
  ('name2', None, None, None)

  >>> (verifyObject(IRoleRegistry, registry),
  ...  verifyObject(IRole, role1),
  ...  verifyObject(IRole, role2))
  (True, True, True)


Role keys must be numeric.

  >>> Role("name", key="key")
  Traceback (most recent call last):
  ...
  TypeError: Role keys must be numeric


Roles may be added to registries, and retrieved by key or name.

  >>> registry.add(role1)
  >>> registry.add(role2)

  >>> registry.get(1) is role1
  True
  >>> registry.get("name1") is role1
  True
  >>> registry.get("name2") is role2
  True

  >>> registry.get(3)
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role 3 isn't registered

  >>> registry.get("name3") is None
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role 'name3' isn't registered


Duplicate roles aren't accepted.

  >>> registry.add(Role("name3", key=1))
  Traceback (most recent call last):
  ...
  canonical.security.role.DuplicateRoleError: Role with key 1 is already registered

  >>> registry.add(Role("name1", key=3))
  Traceback (most recent call last):
  ...
  canonical.security.role.DuplicateRoleError: Role with name 'name1' is already registered


But they can be removed and added back.

  >>> registry.remove(role1)
  >>> registry.add(role1)


It's not possible to remove a nonexistent role, even if it has the
same key or name.

  >>> registry.remove(Role("name3", key=1))
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role with key 1 doesn't match what's registered

  >>> registry.remove(Role("name1", key=3))
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role with name 'name1' doesn't match what's registered

  >>> registry.remove(Role("name3", key=3))
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role 'name3' isn't registered


The registry is also used to grant, revoke, deny, and query for
permissions.  Querying the registry returns a three state flag
with True (granted), False (forbidden), or None (can't tell it).

  >>> registry.grant(role1, "perm1")
  >>> registry.grant(role1, "perm2")
  >>> registry.grant(role2, "perm2")
  >>> registry.grant(role2, "perm2.1")
  >>> registry.grant(role2, "perm2.2")
  >>> registry.forbid(role1, "perm3")
  >>> registry.grant(role1, "perm3")
  >>> registry.forbid(role2, "perm3")
  >>> registry.forbid(role2, "perm3.1")

  >>> registry.revoke(role2, "perm2.1")
  >>> registry.revoke(role2, "perm3.1")

  >>> for perm in "perm1 perm2 perm2.1 perm2.2 perm3 perm3.1".split():
  ...     (registry.may([role1], perm),
  ...      registry.may([role2], perm),
  ...      registry.may([role1, role2], perm))
  (True, None, True)
  (True, True, True)
  (None, None, None)
  (None, True, True)
  (True, False, True)
  (None, None, None)



There's some protection against unknown roles when dealing with
permissions.  Revoking unknown permissions is accepted.

  >>> registry.revoke(role1, "perm3")

  >>> registry.remove(role1)
  >>> registry.may([role1], "perm1")
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role 'name1' isn't registered

  >>> registry.revoke(role1, "perm1")
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role 'name1' isn't registered

  >>> registry.grant(role1, "perm1")
  Traceback (most recent call last):
  ...
  canonical.security.role.UnknownRoleError: Role 'name1' isn't registered


Default roles
-------------

The role registry supports the concept of "default roles". Permission
for default roles may be checked by passing None to the may() method.
When this is done, all default roles will be considered available.

  >>> registry = RoleRegistry()
  >>> role1 = Role("Anonymous1")
  >>> role2 = Role("Anonymous2")
  >>> registry.add(role1, default=True)
  >>> registry.add(role2, default=True)
  >>> registry.grant(role1, "perm1")
  >>> registry.grant(role2, "perm2")

  >>> registry.may([], "perm1")
  >>> registry.may(None, "perm1")
  True
  >>> registry.may(None, "perm2")
  True
  >>> registry.may(None, "perm3")

  >>> registry.remove(role1)
  >>> registry.may(None, "perm1")


Inheriting grants
-----------------

A role may be granted the same permissions of another role in the
same registry.  Even permissions granted to the inherited role
*after* the inheriting granting has happened will be considered.

  >>> role1 = Role("name1")
  >>> role2 = Role("name2", inherits=[role1])

  >>> registry = RoleRegistry()
  >>> registry.add(role1)
  >>> registry.add(role2)
  >>> print(registry.may([role2], "perm1"))
  None

  >>> registry.grant(role1, "perm1")
  >>> print(registry.may([role2], "perm1"))
  True

  >>> registry.forbid(role1, "perm1")
  >>> print(registry.may([role2], "perm1"))
  False

  >>> registry.revoke(role1, "perm1")
  >>> print(registry.may([role2], "perm1"))
  None

  >>> registry.grant(role1, "perm1")
  >>> registry.may([role2], "perm1")
  True

  >>> registry.remove(role1)
  >>> print(registry.may([role2], "perm1"))
  None

  >>> registry.add(role1)


Multi-level inheritance is supported as well.

  >>> role3 = Role("name3", inherits=[role2])
  >>> role4 = Role("name4", inherits=[role3])
  >>> role5 = Role("name5", inherits=[role4])
  >>> registry.add(role3)
  >>> registry.add(role4)
  >>> registry.add(role5)

  >>> registry.grant(role1, "perm1")
  >>> print(registry.may([role5], "perm1"))
  True


When inheriting from multiple roles, granting a permission to only
one of them is enough to give the permission to the inheriting
role as well.  In other words, grants have precedence over forbids.

  >>> registry = RoleRegistry()
  >>> roles = []
  >>> for i in range(1,5):
  ...     role = Role('name%d' % (i,))
  ...     roles.append(role)
  ...     registry.add(role)
  ...     registry.forbid(role, "perm1")
  >>> role5 = Role("name5", inherits=roles)
  >>> registry.add(role5)

  >>> print(registry.may([role5], "perm1"))
  False

  >>> registry.grant(roles[0], "perm1")
  >>> print(registry.may([role5], "perm1"))
  True


vim:ts=4:sw=4:et:ft=doctest
