Package: authoritah
Authoritah - Framework Agnostic Python RBAC LibraryAuthoritah is a Python RBAC library. It is designed to be framework
agnostic, in the sense that it is not coupled with any Web framework or ORM
library. In addition, Authoritah provides a highly granular role system using
a unique approach of context-based role resolution.CompatibilityWe test Authoritah on Python 3.5, 3.6, 3.7 and Pypy 3. It is possible that
older versions of Authoritah will work with Python 2.7 as well, but Python
versions below 3.5 are not supported.InstallationThe easiest way to install Authoritah is via pip:pip install authoritahOverview & TerminologyThe following terms are common in many authorization frameworks, but can have
specific meaning inauthoritahso it is important to clarify them first:IdentitiesIn simple terms, an identity is a user - an entity using the system (be it a
person, a machine authenticating with an API key, a default "anonymous" user,
etc.) Identities can haveroles- inauthoritah, an identity's roles are
usually in relation to a givencontext object, although identities can have
default roles as well.Inauthoritahyou are not expected to use a specific structure for identity
objects - they are opaque as far as the library is concerned, and are only
passed around between different callables you provide.RolesA role is given to an identity, and defines a set ofpermissions- actions
that the user is allowed to perform on an object or in the system.An identity can have more than one role (for example a user may be both ahiring_managerand acontent_editor).  In addition, roles can inherit
permissions from one or more other roles (for example, acontent_editorcan
inherit fromcontent_viewer).Unlike many other authorization frameworks, inauthoritahroles are not
global (although they can be), but are derived from context - for example a
user may be acontent_editorfor all articles, or may be acontent_editoronly for the articles they created, andcontent_viewerfor all other articles.PermissionsPermissions, simply put, are "rights granted to a given identity based on its
roles". For example, someone with acontent_editorrole have thearticle_edit,article_publishandarticle_unpublishpermissions.Implementing authorization checks in a system normally involves checking
whether the user has one or more permissions granted to them before proceeding
with an action.Strict Mode:The Authorizer class may be instantiated with strict=True (defaults to False).Strict mode can raise exceptions in two cases:If is_allowed is called for a permission not defined in any of the roles defined.If a role in the identity provided to is_allowed is not definedThis is useful to check if one forgot to add a role or permission.Context ObjectsThecontext objectis the object on which the operation is performed. For
example, when editing an article the context object is the article. As
mentioned, inauthoritahcontext objects have a more central role than
with many other authorization frameworks - they are taken into account when
deciding the user's role.Quick StartThe following is a quick start guide to applying authorization to your code
usingauthoritah.We'll follow an example of an imaginary, simplified content management system
with 3 objects: articles, comments and users.1. Define Roles and PermissionsAs with any RBAC system, it is recommended to start with defining some roles
and what permissions they grant. WithAuthoritah, it is recommended to think
of roles in relation to objects in the system.You can define your roles and permissions in a configuration file - aYAMLorJSON, or even a Pythondict:---viewer:-article_list-article_view-comment_list-comment_view-user_createuser:parents:['viewer']grants:-comment_create-comment_upvotecontributor:parents:['user']grants:-article_createcontent_admin:-comment_edit-comment_delete-article_edit-article_deleteuser_admin:-user_edit-user_deletesuper_admin:parents:-contributor-content_admin-user_adminSome things to notice:Eachroleis designated by a unique key (or name), and defines,
optionally, a list of permissions (grants).Roles can inherit permissions from other roles. This is done by providing
a list of inherited roles asparents.As a shortcut, you can define a role by simply providing the list of
permissions it grants, skipping thegrantskey. This format works nicely
for roles not inheriting from any other role.Most importantly, roles are defined in a very granular way following an
approach of least possible access. The right way to think of permissions inauthoritahis to consider whether someone with the given role should have
a given permissionunder all circumstancesor only in specific contexts.In our example, acontributorcan create any article, but cannot delete
any article - only their own articles. Later, we will see how to use dynamic
role resolution to elevate a specific user tocontent_adminfor specific
contexts, so they can edit and delete their own articles.2. Initialize an Authorizer and hook in identity managementEverything you will do withauthoritahbegins with creating an anAuthorizerobject. Assuming we read our roles & permissions configuration
from a YAML file namedauthorization.yml, here is how this is done:importyamlfromauthoritahimportAuthorizerwithopen('authorization.yml')asf:roles=yaml.safe_load(f)authz=Authorizer(permissions=roles)Sinceauthoritahis not bound to any authentication or identity
management implementation, you will need to tell your Authorizer how to get
an identity object, and how to get a list of roles from this object.The simplest way to do this would be to use two decorator methods of theAuthorizerobject we just created:authz.identity_providerandauthz.default_role_provider:@authz.identity_providerdefget_current_user(request):"""This function returns the current authenticated user object"""returnrequest.user@authz.default_role_providerdefget_user_roles(user,context=None):"""Get roles for the current user.This function should always accept an identity object (as returnedby the defined identity provider) and return either a list of roles,a string representing a single role, or None for a user with no roles.Note that this function also receives the current context object. Itmay be used, if needed, to infer roles - but this is usually notrecommended."""returnuser.rolesIn most cases these two functions should be very simple - they are just "glue"
that integrates your existing code withauthoritah.3. Define Context-Specific Role ResolutionIf your system does not require any dynamic role resolution (e.g. permissions
are global and are not related to context objects in your case), you can skip
this phase and useauthoritahlike you would use any other RBAC library.However, in most cases you would like to give users additional roles based on
the object they are accessing - the context object.This is done using theauthz.context_role_providerdecorator. This decorator
should be used to decorate classes, specifying how we should get the roles when
the context object is of a certain type.Assume that our CRM has an ORM or entity class that represents an article:classArticle:title=Nonecontent=Nonecreated_by=NoneYou can now use a decorator to tell Authoritah thatarticle_user_rolesis the
role provider for objects of typeArticle, or any subtype of it:@authz.role_provider(Article)defarticle_user_roles(user,article):ifuser.id==article.created_by:return['content_admin']return[]Thus, we have told our authorizer to callarticle_user_roleswhenever the
context object is anArticleobject. The list of roles returned by this
callable will be appended to the list of existing global roles the user already
has. This way, we know that if the user is the creator of the article, they
should get permissions as if they have thecontent_adminrole (meaning they
can edit or deletethis specific article).Another way of doing this will be to use theclass_role_providerannotation
on the context object class, providing it a method name on the same class to
use as a context role provider:@authz.class_role_provider('user_roles')classProtectedArticle(Article):defuser_roles(self,user):ifuser.id==self.created_by:return['content_admin']return[]Note that this case will work only for objects ofProtectedArticle, not the
originalArticlebase class. However, itwillwork for any class inheriting
fromProtectedArticle.4. Apply Authorization Checks in Your CodeLast but very important, start checking for permissions before you perform
some operations in your code.There are two common ways to do this. One is explicit:defmodify_article(article_id,data):"""Assume this is your Web framework's handler for article editing"""article=DB.article.get(article_id)ifnotauthz.is_allowed('article_edit',article):return'You are not allowed to edit this article',403# ... proceed to update the articleThe other is using a decorator, which works well for object methods where the
object is our context object. Let's update our class definition from before:@authz.class_role_provider('user_roles')classProtectedArticle(Article):@authz.require('article_edit')defmodify(self,new_data):# ... proceed to modify my own attributespassdefuser_roles(self,user):ifuser.id==self.created_by:return['content_admin']return[]In the example above, if the user doesn't have thearticle_editpermission,
callingmodify()will raise anauthoritah.NotAuthorizedexception, which
you will then need to catch and handle.Background: Why Context-Based Role Resolution?In most RBAC / ACL frameworks, each user is given one or more pre-defined
roles, which in turn decide their permissions to perform operations on various
objects. This works well in many cases, but falls short when static permissions
are not enough to decide whether a user should be allowed to perform an
operation.For example, in a content management system a user may be have an "editor" role
granting them permission to edit articles. This works well in a "flat" system
where all editors can edit all articles.But what if we want users to only be able to edit articles that they created?
Or what if we want users to be able to designate specific editors for an
article? Granting a global "editor" role here is just not enough.Existing Solutions: Post-Hoc AssertionsMost current RBAC libraries tackle this problem by adding dynamic assertion
capabilities on top of static roles and permissions. They allow developers to
specify additional assertion callables per granted permission. Once a user is
granted permission to take an action based on their role, additional
assertions are executed, essentially checking if the permission should still
be granted, given the user and a context object (in our example the article
being edited).Unfortunately, this has a few major drawbacks:Writing custom assertions quickly becomes cumbersome as permissions become
more granular and the number of permissions in the system grows.This model forces an approach of granting roles with the maximal permissions.
Narrowing down permissions to only apply in specific contexts is an
afterthought.Enter A New Approach: Context-Based Role ResolutionWithauthoritah, a user's role is not static but changes based on the context
object. Essentially, instead of asking "what is this user's role?", we ask
"what is the user's role given this object?". Once the role is dynamically
decided, it is very easy to grant or deny permission to perform an action
without any need of additional assertions.In addition, it advocates a process where minimal permissions are granted
through each role initially. In the right context, a user may have additional
permissions through additional roles assigned to them.This, in our opinion, reduces the risk of permissions leakage as it encourages
a more granular and limited approach to granting permissions.LicenseCopyright (c) 2017 Shoppimon LTDLicensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.