Package: targe
TargeA powerful and flexible authorization library utilizing a policy system that reflects your domain.InstallationWith pip:pip install targeor with poetrypoetry add targeQuick startfromtargeimportAuth,ActorProvider,Actor,Policyfromtarge.errorsimportAccessDeniedError# This will provide an actor for the auth mechanismclassMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:returnActor(actor_id)# Initialise auth classauth=Auth(MyActorProvider())# Retrieve and authorize actor by its idauth.authorize("actor_id")# `auth.guard` decorator assigns auth scope to a function and# protects it from non-authorized access@auth.guard(scope="protected")defprotect_this()->None:...# code that should be protected by authtry:protect_this()exceptAccessDeniedError:...# this will fail as actor has no access to scope `protected`auth.actor.policies.append(Policy.allow("protected"))# add `protected` scope to actor policiesprotect_this()# now this worksFeaturesCustomisable and flexible policy systemThe policy system inTargeis not limited to specific keywords likeread,write,create, etc.
It uses scopes instead, scopes can hold any value that makes sense in your application's domain
likesalads : eat. To increase flexibility and control over your domainTargeallows you to define dynamic
scopes that can point to specific data in your application.Minimal, close to 0 learning curveIf you already have some experience with otheraclorauthorizationlibraries there is
almost 0 learning curve. In order to start using the library you only need to learn these 5 methods:Auth.guardAuth.guard_afterPolicy.allowPolicy.denyActorProvider.get_actorBuilt-in audit logEverytime a guarded function is executed, the library creates a log entry. These log entries can be persisted
and used later on to understand who, when, how and what has changed within your application.Elegant and easy to use interfaceYou don't have to write complexifstatements asserting whether a user has a given role or policy.
All of that happens automatically in one small@guarddecorator, which can be attached to
any function / method within your codebase, and easily removed if needed.UsageExecution flowThe following diagram is a high level representation of the execution flow:When a protected function gets called, thetarge.Authclass checks whether theactoris accessible
(targe.Auth.initis responsible for providing a valid instance oftarge.Actor).If for some reason theactoris not accessible (Auth.initwas not called, ortarge.ActorProviderhas failed),
anUnauthorizedErrorexception is raised.When anactoris present, the library will attempt to resolve thescopeand assert whether theactorhas the required
policy or access to the given scope. Once this is done, the library will automatically generate an audit log
which can be persisted to a database for future reference.Finally, if actor has sufficient rights guarded function is executed, otherwisetarge.AccessDeniedErroris raised.ActorAn actor represents an authenticated user in your application. Other important characteristics of an actor are:An actor aggregates permissions and rolesAn actor encapsulates its state and may act upon its changeAn actor knows whether is can access given scopeAn actor's id is referenced in audit logAn actor can be extended further to encapsulate your application logicCreating a new actorfromtargeimportActormy_actor=Actor("actor_id")Assigning policiesfromtargeimportActor,Policymy_actor=Actor("actor_id")# assign policiesmy_actor.policies.append(Policy.allow("articles : update"))Note: whitespaces in scope are irrelevant, botharticles:update,articles : updateare equal from the library points of view.Assigning rolesfromtargeimportActor,Policy,Rolemy_actor=Actor("actor_id")# simple roleuser_manager=Role("user_manager")user_manager.policies.append(Policy.allow("user:*"))# assign rolemy_actor.roles.append(user_manager)Providing an actor to the auth systemBy default, the auth system does not know who is your actor and what it can do.To provide information about your actor, you have to implement thetarge.ActorProviderprotocol,
please consider the following example:fromtargeimportActorProvider,Actor,AuthclassMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:...# you can query your database or do other relevant task to factory your instance of `targe.Actor`returnActor(actor_id)# now we have to just instantiate auth and pass an instance of our ActorProvider implementationauth=Auth(MyActorProvider())# The following line will cause the auth system to use `MyActorProvider.get_actor` method.auth.authorize("actor_id")PoliciesPolicyis an object representing a logical rule that can either allow or deny accessing
certain areas of your application.
Once policies are created they can ba attached to a role or a user, to ensure fine-grained
access control.Policies containscopesandeffect. The first one holds information how data is
being accessed within your application (read,write,update,etc), the latter
tell whether the scope is accessible or not.You can define a policy like in the example below:fromtargeimportPolicypolicy=Policy.allow(scope="articles : update")ScopesScope is a string representing a logical boundary within your application in which data is being
accessed and/or manipulated. This string must follow the pattern [a-zA-Z][a-zA-Z0-9-_]*, whitespaces are ignored.
Characters like::,,,*are allowed in the scope, but they have specific meaning::to build namespaced scopes,to match multiple scope sections*for pattern matching expressionsScopes can be used in policies and in guarded functions. Use scopes in policies to set rules for existing logical
boundaries in our application. To define those boundaries, scopes must be provided in theAuth.guarddecorator, which
decorates a given function or method.The following is a list of valid policy scopes:articles
articles : update
articles : update : article_id
articles : create, upate, delete
articles : *
articles : meta : set-*
articles : meta : *Name
articles : update : * : tagsPattern matchingLet's review the following code snippet which defines multiple policies:fromtargeimportPolicyPolicy.allow("article : meta : setKeywords")Policy.allow("article : meta : setVersion")Policy.allow("article : meta : setCategory")Policy.allow("article : meta : getKeywords")Policy.allow("article : meta : getVersion")Policy.allow("article : meta : getCategory")It is quite a repetitive task which can be simplified by using pattern matching in the policy's scopes:fromtargeimportPolicyPolicy.allow("article : meta : set*")Policy.allow("article : meta : get*")An asterisk at the end of each scope tellsTargeto use the pattern matching mechanism.
The first policy might be interpreted as "match all the scopes which start with thearticlenamespace, followed by themetanamespace, followed by a namespace that starts withset". The second scope is very similar but the last namespace
has to start withgetinstead.Here are some examples to help you understand how pattern matching works:article : *Match all scopes that start with article namespace.article : * : idMatch all scopes that start with anarticle, hasanynamespace after that and ends with aid.article : *NameMatch all scopes that start with anarticlenamespace and are followed by a namespace that ends withNameLet's now go back to our last example that we simplified with pattern matching. We can simplify iteven further with grouping. Let's consider the following code snippet:fromtargeimportPolicyPolicy.allow("article : meta : set*, get*")Now with the above policy we can match all the scopes that were presented at the beginning of this chapter.RolesRole is a collection of policies with a unique name. Roles can also be
used to build Role-based access control (RBAC), which is a simplified mechanism
for regulating access to part of your application based on the roles of an individual actor.The following is an example where theuser_managerrole is defined:fromtargeimportRole,Policyrole=Role("user_manager")# You can also attach policies to a role, but it is not needed in a RBAC scenariorole.policies.append(Policy.allow("user : create, update, delete, read"))Role names must follow the [a-z][a-z0-9_-]+ pattern. A role name is also its identifier,
thus they should be unique across your application.Guarding functionProtecting a function from unauthorized access is one of theTarge's main objectives.We can protect a function from unauthorized execution using two styles:ACL styleRBAC styleUse the RBAC style in scenarios where you have to just assert if an actor has a given role, and use the ACL style in other cases.
The ACL style is not only giving you more control over your resources but also automatically enables the audit log.Guarding function - RBAC style exampleTo protect a function from unauthorized execution use theAuth.guard(rbac=[...])decorator withrbacas the argument. Therbacargument accepts a list of strings where each string is a role name that is required to execute annotated function.If more than one role is passed in therbacargument, this means the actor has to own all of the required roles
to execute the annotated function.fromtargeimportActorProvider,Actor,Authfromtarge.errorsimportAccessDeniedErrorclassMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:returnActor(actor_id)auth=Auth(MyActorProvider())auth.authorize("actor_id")@auth.guard(roles=["user_manager"])# Here we use `Auth.guard` decorator to protect `create_user` functiondefcreate_user()->None:...try:create_user()exceptAccessDeniedError:print("`create_user` is protected from unauthorized access.")Keep in mind you can still take advantage of audit logs in RBAC mode,
the only requirement is to provide ascopeargument in theAuth.guarddecorator.Guarding function - ACL style examplefromtargeimportActorProvider,Actor,Authfromtarge.errorsimportAccessDeniedErrorclassMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:returnActor(actor_id)auth=Auth(MyActorProvider())auth.authorize("actor_id")@auth.guard(scope="user : create")defcreate_user()->None:...try:create_user()exceptAccessDeniedError:print("`create_user` is protected from unauthorized access.")Dynamic scopesSometimes you might run into scenarios where you would like to limit access to a given entity
or group of entities. In these scenarios, dynamic scopes may come in handy.
Dynamic scopes contain placeholders for values held by arguments passed to a guarded function.
Everytime a function is called, placeholders are replaced with corresponding values.fromtargeimportActorProvider,Actor,Auth,Policyfromtarge.errorsimportAccessDeniedErrorclassMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:# lest initialize actor and allow access to `articles:update:allowed_id` scope.actor=Actor(actor_id)actor.policies.append(Policy.allow("articles:update:allowed_id"))returnactorauth=Auth(MyActorProvider())auth.authorize("actor_id")classArticle:def__init__(self,article_id:str,body:str):self.article_id=article_idself.body=body# Here we define a dynamic scope that holds reference to the function's parameter `article`# and tries to access its property `article_id`@auth.guard(scope="article : update : { article.article_id }")defupdate_article(article:Article)->None:print("article updated")# the following attempt will fail as the actor has access only to the `article:update:allowed_id` scopetry:update_article(Article("other_id","Lorem Ipsum"))exceptAccessDeniedError:print("`update_article` is protected from unauthorized access.")# this line will succeedupdate_article(Article("allowed_id","Lorem Ipsum"))Overriding function guarding mechanismYou can override the default behavior of the guard mechanism in scenarios when it denies access to a guarded
function. In order to do that, pass a callable object to theAuthinitializer, like below:fromtargeimportActorProvider,Actor,AuthclassMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:returnActor(actor_id)defon_guard(actor:Actor,scope:str)->bool:ifscope=="user:create":returnTruereturnFalseauth=Auth(MyActorProvider(),on_guard=on_guard)auth.authorize("actor_id")@auth.guard(scope="user:create")defcreate_user()->None:...create_user()The callable object must return aboolvalue (Truein order to allow access,Falseto deny access) and accept two parameters:actor: targe.Actor- an actor that is currently authorized in the systemscope: str- the scope assigned to the guarded functionAudit logThe audit log might be useful if you need to track an actor's activities in your application.
By default, all actor's actions against guarded functions are automatically recorded and stored
in memory as long as thescopeattribute is provided in theAuth.guarddecorator.TheInMemoryAuditStoreclass is a default in-memory implementation of theAuditStoreprotocol, which
is instantiated by theAuthclass if no other implementation is provided.AuditEntry structuretarge.AuditEntryis a representation of a single actor's action against a guarded function in your application.Each audit entry contains the following information:entry_id:str- unique identifier 16 characters longactor_id:str- id of authenticated actor may reference to a user in your applicationscope:str- scope in which the function was executed, defined in guard decoratorstatus:targe.AuditStatus- tells whether execution of a guarded function was successful or notcreated_on:datetime.datetime- the time that the action was initializedPersisting audit logAs we already discussed, by default the audit log is stored only in memory, this behaviour
can be simply amended by implementing thetarge.AuditStoreprotocol and passing an instance
of the new implementation intotarge.Auth's initializer:fromtargeimportAuditStore,AuditEntry,Auth,ActorProvider,Actorimportsqlite3classMyActorProvider(ActorProvider):defget_actor(self,actor_id:str)->Actor:returnActor(actor_id)classMyAuditStore(AuditStore):def__init__(self,connection:sqlite3.Connection):self.connection=connectiondefappend(self,log:AuditEntry)->None:query=self.connection.cursor()query.execute("INSERT INTO audit_log VALUES(?, ?, ?, ?, ?)",(log.entry_id,log.actor_id,log.scope,log.status,log.created_on))db_connection=sqlite3.connect("audit_log.db")auth=Auth(actor_provider=MyActorProvider(),audit_store=MyAuditStore(db_connection))auth.authorize("actor_id")...