{"id":22901757,"url":"https://github.com/mbuczko/cerber-roles","last_synced_at":"2025-06-14T11:35:55.375Z","repository":{"id":62431392,"uuid":"112251422","full_name":"mbuczko/cerber-roles","owner":"mbuczko","description":"Role Based Access Control","archived":false,"fork":false,"pushed_at":"2019-05-16T20:49:25.000Z","size":70,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-29T15:05:53.723Z","etag":null,"topics":["clojure","permissions"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mbuczko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-11-27T21:36:16.000Z","updated_at":"2023-12-05T14:22:10.000Z","dependencies_parsed_at":"2022-11-01T20:45:39.227Z","dependency_job_id":null,"html_url":"https://github.com/mbuczko/cerber-roles","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-roles","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-roles/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-roles/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Fcerber-roles/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mbuczko","download_url":"https://codeload.github.com/mbuczko/cerber-roles/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252983760,"owners_count":21835764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","permissions"],"created_at":"2024-12-14T01:40:48.356Z","updated_at":"2025-05-08T02:11:14.021Z","avatar_url":"https://github.com/mbuczko.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Clojars Project](https://img.shields.io/clojars/v/cerber/cerber-roles.svg)](https://clojars.org/cerber/cerber-roles)\n\n# Roles and permissions\n\nThis simple library tries to fill in the gap between OAuth2 authorization and role-based access control.\n\nCode has been separated from [Cerber OAuth2 Provider](https://github.com/mbuczko/cerber-oauth2-provider) implementation and published as optional add-on which hopefully makes scopes and roles easier to match.\n\n## Terminology\n\nTerminology used in this doc bases on Apache Shiro: http://shiro.apache.org/terminology.html\n\n## Anatomy of Permission\n\nPermission implemented by this library consists of two parts: a _domain_ and list of comma-separated _actions_, both joined with colon, like `user:read` or `user:read,write`.\n\nThis imposes 3 additional cases:\n\n - wildcard action: any action on given domain is allowed, eg: `user:*`, or simply `user`\n - wildcard domain: given action on any domain is allowed, eg: `*:write`\n - wildcard permission: any action on any domain is allowed: `*:*`, or simply `*`\n\n## Anatomy of Role\n\nRole is a collection of permissions. Technically, it is represented by a qualified keyword, eg. `:user/default` or `:admin/all`:\n\n``` clojure\n{:user/all      #{\"user:read\" \"user:write\"}\n :project/read  #{\"project:read\"}}\n```\n\nRoles may also map to wildcard actions and other roles (explicit- or wildcarded ones).\n\n``` clojure\n{:admin/all     \"*\"                          ;; maps to wildcard permission\n :admin/company #{:user/* :project/*}        ;; maps to other roles from user and project domains\n :project/all   #{\"project:*\" \"timeline:*\"}} ;; maps to wildcard-action permissions\n\n```\n\n# Usage\n\nOnce permissions and roles are defined and bound together with carefully crafted mapping, how to make them showing up in a request? \n\nA `wrap-permissions` middleware is an answer. It bases on a context set up by companion middleware - `wrap-authorized` exposed by [Cerber API](https://github.com/mbuczko/cerber-oauth2-provider) and populates subject's roles and permissions.\n\nLet's walk through routes configuration based on popular [Compojure](https://github.com/weavejester/compojure) to see how it works.\n\nCerber's OAuth2 routes go first:\n\n```clojure\n(require '[cerber.handlers])\n\n(defroutes oauth2-routes\n  (GET  \"/authorize\" [] cerber.handlers/authorization-handler)\n  (POST \"/approve\"   [] cerber.handlers/client-approve-handler)\n  (GET  \"/refuse\"    [] cerber.handlers/client-refuse-handler)\n  (POST \"/token\"     [] cerber.handlers/token-handler)\n  (GET  \"/login\"     [] cerber.handlers/login-form-handler)\n  (POST \"/login\"     [] cerber.handlers/login-submit-handler))\n```\n\nRoutes that should have roles and permission populated go next:\n\n```clojure\n(require '[cerber.oauth2.context :as ctx])\n\n(defroutes user-routes\n  (GET \"/users/me\" [] (fn [req]\n                        {:status 200\n                         :body {:client (::ctx/client req)\n                                :user   (::ctx/user req)}})))\n```\n\nNow, the crucial step is to apply both `wrap-authorized` and `wrap-permissions` middlewares:\n\n```clojure\n(require '[cerber.roles]\n(require '[cerber.handlers]\n(require '[compojure.core :refer [routes wrap-routes]]\n(require '[ring.middleware.defaults :refer [api-defaults wrap-defaults]])\n\n(defn api-routes\n  [roles scopes-\u003eroles]\n  (wrap-defaults\n   (routes oauth2-routes (-\u003e user-routes\n                             (wrap-routes cerber.roles/wrap-permissions roles scopes-\u003eroles)\n                             (wrap-routes cerber.handlers/wrap-authorized)))\n   api-defaults))\n   ```\n\nLast step is to initialize routes with _roles_ and _scopes-to-roles_ mapping, here assuming that OAuth2 client may have any of `resources:read`, `resources:write` or `resource:manage` scopes assigned:\n\n```clojure\n(def roles (cerber.roles/init-roles\n             {;; admin can do everything with photos and comments\n              :user/admin #{\"photos:*\" \"comments:*\"}\n              \n              ;; registered user can read and write to photos and comments\n              :user/all #{\"photos:read\" \"photos:write\" \"comments:read\" \"comments:write\"}\n              \n              ;; unregistered user can only read photos and comments\n              :user/unregistered #{\"photos:read\" \"comments:read\"}}))\n\n(def scopes-\u003eroles {\"resources:read\"   #{:user/unregistered}\n                    \"resources:write\"  #{:users/all}\n                    \"resources:manage\" #{:user/admin}})\n\n(def app-routes\n  (routes (api-routes roles scopes-\u003eroles) oauth2-routes))\n```\n# How it works?\n\nLooking at example above it's clear that entire mechanism boils down to 3 elements:\n\n* _roles_, for performance reasons unrolled by `init-roles` to contain no nested entries.\n* _scopes-\u003eroles_ map which says how to translate an OAuth2 client's scope into a set of roles.\n* a middleware which takes _roles_ and _scopes-\u003eroles_ and calculates corresponding roles/permissions.\n\nOne unknown is how middleware populates roles and permissions bearing in mind that two scenarios may happen:\n\n1. Request is a cookie-based user-originated one.\n   \n   In this scenario, subject initialized and stored in context by cerber's `wrap-authorized` middleware keeps its own roles and permissions calculated upon the roles.\n\n2. Request is a token-based client-originated one.\n   \n   In this scenario OAuth2 client requests on behalf of user with approved set of scopes. Scopes are translated into roles (based on _scopes-\u003eroles_ mapping) and intersected with user's own roles.\n   This is to avoid a situation where client's scopes may translate into roles exceeding user's own roles. Calculated permissions are also intersected with user's permissions to avoid potential elevation of priviledges.\n\n# API\n\n`(init-roles [roles-map])`\n\nInitializes roles-to-permissions mapping.\n\nInitialized mapping has no longer nested roles (they get unrolled with corresponding permissions).\n\n`(make-permission str)`\n\nBuilds a `Permission` based on string consisting of domain and actions, separated by colon, like \"user:read,write\".\nPermission may be exact one, have actions or domain (or both) wildcarded.\n\nWildcard is denoted by \"\\*\", and means _any_, so \"document:*\" permission can be read as _any action on document_.\n\n`(implied-by? [permission permissions])`\n\nReturns resource `permission` if it's implied (has access to) by the set of `permissions`.\nReturns falsey otherwise.\n\n`(has-role? [subject role])`\n\nReturns matching `role` if it's been found in `subject's` set of `:roles`.\nReturns falsey otherwise.\n\n`(has-permission [subject permission])`\n\nReturns resource permission if it's implied by `subject`'s set of `:permissions`.\nReturns falsey otherwise.\n\n`(intersect-permissions [coll1 coll2])`\n\nIntersects 2 sets of permissions calculating their common domains and actions.\nFor example, intersection of following permissions: `[\"*:read,write\"]` and `[\"doc:read,create\"]` results in `[\"doc:read\"]`.\n\n`(roles-\u003epermissions [roles mapping])`\n\nReturns set of permissions based on collection of `roles` and `mapping` returned by `init-roles` function.\n\n# Example\n\n``` clojure\n(def subject {:roles #{:user/read :user/write}\n              :permissions #{(make-permission \"project:read\")\n                             (make-permission \"contacts:*\")}}\n\n(has-permission subject \"contacts:write\"))\n(has-permission subject \"contacts:read,write\"))\n\n(has-role? subject :user/write)\n\n(implied-by? \"document:read\" \n             (intersect-permissions [(make-permission \"document:read,write\")\n                                     (make-permission \"workspace:create\")\n                                     (make-permission \"document:delete,create\")]\n                                    [(make-permission \"document:read,write\")]))\n```\n\n# Set up for local development\n\nThis library uses clojure [deps](https://clojure.org/guides/deps_and_cli) and [revolt](https://github.com/mbuczko/revolt) to set up comfortable local environment:\n\n```clojure\n    clj -A:dev -p nrepl,watch,rebel\n```\n\n...and connect to the REPL.\n\n# License\n\nEclipse Public License - v 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmbuczko%2Fcerber-roles","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmbuczko%2Fcerber-roles","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmbuczko%2Fcerber-roles/lists"}