Skip to content

fmnoise/zakon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stand With Ukraine

zakon CircleCI cljdoc badge

But whoso looketh into the perfect law of liberty, and continueth therein, he being not a forgetful hearer, but a doer of the work, this man shall be blessed in his deed. (James 1:25)

zakon (/zakon/ ukr. закон - law) is declarative authorization library inspired by https://github.com/ryanb/cancan and built on top of Clojure multimethods

Usage

Current Version

(require '[zakon.core :as zkn :refer [can! cant! can? cant? any defrule])

Rules

The core concept of zakon is rule - a combination of actor(who performs action), action(what is performed), subject(on which action is performed) and associated result, which represents the answer to question "can this actor do this action on this subject?". So textual rule "user can create content", can be written as:

(can! :user :create :content)

In the example above actor is :user, action is :create and subject is :content. Result is simply true. The opposite effect can be achieved with cant!:

(cant! :user :delete :content)

can? and cant? can be used to test rules:

(can? :user :create :content) => true
(can? :user :delete :content) => false
(cant? :user :create :content) => false
(cant? :user :delete :content) => true

Rules have a priority, in case of conflict last applied rule always wins:

(can! :user :delete :content)
(can? :user :delete :content) => true
(cant! :user :delete :content)
(can? :user :delete :content) => false
(can! :user :delete :content)
(can? :user :delete :content) => true

List of registered rules can be obtained using rules method:

(zkn/rules)
=> ([:zakon.core/policy :zakon.core/any :zakon.core/any :zakon.core/any] [:zakon.core/policy :zakon.core/user :zakon.core/delete :zakon.core/content])

cleanup! cleans up all defined rules and prints cleaned rules count to stdout.

Wildcards

In some cases kind of wildcard should be specified in the rule, for example "admin can do anything with content" (action is a wildcard - anything). any can be used to wildcard actor, action or subject in any combinations:

(can! :admin any :content)
(can? :admin :create :content) => true
(can? :admin :delete :content) => true

(can! :admin any any)
(can? :admin :create :profile) => true
(can? :admin :upload :file) => true

any can be also used in rules test:

(can? :admin any any) => true
(can? any any any) => false

Rules with wildcards may sound ambigious, e.g. "user can do anything with content, but user cannot delete anything". Can user delete content? From zakon's point of view it can't, as statement about deletion restriction is last and works as clarification.

(can! :user any :content)
(cant! :user :delete any)
(can? :user :delete :content) => false

The same logic works if we swap sentence parts - "user cannot delete anything, but can do anything with content".

(cant! :user :delete any)
(can! :user any :content)
(can? :user :delete :content) => true

Default rule

In the example above, restricting part is redundant, because of zakon is restrictive by default (everything which is not specified as allowed, is restricted). So initially there's only default rule defined using wildcards and equivalent to:

(cant! any any any)

If we dispatch rule that was not yet defined, the default one will be used instead. If target system should be not restrictive by default(everything which is not specified as restricted, is allowed), that can be redefined:

(can! any any any)

Debugging rules

With all that wildcards and defaults ambiguity, it can be hard to understand which rule was dispatched for given arguments. find-rule can be used to debug rule:

(zkn/find-rule :user :delete :content) => {:line 4, :column 1, :ns "user"}
(zkn/find-rule any :delete :content) => :zakon.core/default-rule

Resolvers

can! and cant! are suitable for specifying only simple boolean result, which is enough for simple cases. For more complex ones, defrule should be used. defrule supports specifying both simple values and resolvers - atoms and fns to dispatch rule. In the following example atom is used to store result:

(def content-deletion-allowed (atom true))
(defrule [:user :delete :content] content-deletion-allowed)
(can? :user :delete :content) => true
(swap! content-deletion-allowed not)
(can? :user :delete :content) => false

Function or multimethod which is used as resolver should accept single argument - map with keys :actor, :action and :subject:

(def user-types #{:content :comment})
(defrule [:user :create any] (fn [{:keys [subject]}] (user-types subject)))
(can? :user :create :content) => true
(can? :user :create :comment) => true
(can? :user :create :share) => false

In order to keep things predictable can? and cant? always return boolean result, so there's no need to do conversion manually in defrule or resolver:

(defrule [:admin :delete :profile] 1)
(defrule [:user :delete :profile] nil)
(can? :admin :delete :profile) => true
(can? :user :delete :profile) => false

Entities

Actor, action and subject values are entities. In the examples above we used keywords, but any other value can also be an entity.

(can! 1 + 2)
(can? 1 + 2) => true

Values which are not keywords are converted into keywords automatically, e.g. in the example above 1 becomes :java.lang.Long\1 and + becomes :clojure.core$_PLUS_/clojure.core$_PLUS_@5d3882cf. Each entity belongs to domain, which is keyword's namespace(or current namespace if keyword is non-qualified). Each domain have special value any which represents domain root object. All domain entities are inherited from domain root:

(can! :java.lang.Long/any + :java.lang.Long/any)
(can? 3 + 4) => true

As shown above any can be used as wildcard for defining rules, so it's root object for all other entities and all domain roots are inherited from any. inherit! can be used to make child-parent relation for any other object:

(zkn/inherit! :role/admin :role/user)
(can! :role/user  :http/get :routes/home)
(can! :role/admin :http/any :routes/admin)

(can? :role/admin :http/get :routes/home)  => true
(can? :role/admin :http/get :routes/admin) => true
(can? :role/user  :http/get :routes/admin) => false

inherited? can be used to check if 2 enities are in child-parent relations:

(zkn/inherited? :role/admin :role/user) => true
(zkn/inherited? :http/get :http/any) => true
(zkn/inherited? :http/any any) => true

Turning objects into entities

Let's say we want to define a rule which allows to create content with type :acticle for any user, and allows doing anything to user with role :admin. User and content are respresented as records.

(defrecord User [role])
(defrecord Content [type])

Despite any value can be an entity, that's impractical to define rule like this:

(defrule [any any any]
  (fn [{:keys [actor action subject]}]
    (or (and (= action :create)
             (= (:type subject) :article))
        (= (:role actor) :admin))))

(can? admin :create topic) => true

Such rules are very generic and resolver function quickly becomes cumbersome. We can separate rule declaration and getting data required for rule checking from domain objects using Entity protocol, so rule from example above can be rewritten:

(extend-protocol zkn/Entity
  User
  (zkn/as-actor [{:keys [role]}] role)

  Content
  (zkn/as-subject [{:keys [type]}] type))

(can! any :create :article)
(can! :admin any any)
(can? (->User :admin) :create (->Content :topic)}) => true
(can? (->User :editor) :create (->Content :article)}) => true
(can? (->User :editor) :delete (->Content :article)}) => false
(can? (->User :editor) :create (->Content :topic)}) => false

Policies

Rule sets can be kept isolated from each other in scope of policy. Policies can contain the same rules with different values, for example:

;; policy is passed as first argument when defining rule
(cant! :restrictive-policy :user any any)
(can! :permissive-policy :user any any)

;; when checking rule, policy is passed as key :policy in optional 4th argument
(can? :user :say :hello {:policy :restrictive-policy}) => false
(can? :user :say :hello {:policy :permissive-policy}) => true

All policies are inherited from :zakon.core/policy which acts as global policy. If specified policy can't dispatch rule, :zakon.core/policy will be used.

Status

Library is alpha and subject to change.

License

Copyright © 2020 fmnoise

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

About

Declarative Clojure authorization

Resources

License

Stars

Watchers

Forks

Packages

No packages published