February 8, 2025

Modelling permissions in Datomic

permissions

For most services/applications we write, we want to have some sort of permissions. Deleting a team in your application should only be allowed by trusted individuals for example, not the new member who is still wet behind the ears.

Note
Permission modelling is complex, because business always have exceptions. This is a simple example that covers my needs.

Clojure structure for teams

When we get data from any backend, or we reason about it in the code, this is a reasonable default and what you might start out with. In this scenario a team has members, and each member has a set of permissions.

Clojure schema
{:team/id #uuid "some-uuid"
 :team/name "My fantastic team"
 :team/members [{:team.member/user #uuid "member1-uuid"
                 :team.member/permissions #{:team/delete
                                            :team/add-member
                                            :team/remove-member
                                            :team/view}}
                {:team.member/user #uuid "member2-uuid"
                 :team.member/permissions #{:team/view}}]}

Approach 1

Modelling the Clojure schema straight into Datomic it would look like this. This is a 1-to-1 translation of what we see in the Clojure schema and is easy to reason about.

With this setup we would have to duplicate a similar schema for each new type of data structure we want permissions on.

Datomic schema
[ ;; -- user
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :user/id
    :db/index              true
    :db/unique             :db.unique/identity
    :db/valueType          :db.type/uuid
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :user/email
    :db/index              true
    :db/unique             :db.unique/identity
    :db/valueType          :db.type/string
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  ;; -- end user

  ;; -- team
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team/id
    :db/index              true
    :db/unique             :db.unique/identity
    :db/valueType          :db.type/uuid
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team/name
    :db/valueType          :db.type/string
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  ;; reference to :team.member/user
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team/members
    :db/valueType          :db.type/ref
    :db/cardinality        :db.cardinality/many
    :db.install/_attribute :db.part/db}]
  ;; -- end team

  ;; -- team member
  ;; reference to :user/id
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team.member/user
    :db/valueType          :db.type/ref
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team.member/permissions
    :db/valueType          :db.type/keyword
    :db/cardinality        :db.cardinality/many
    :db.install/_attribute :db.part/db}]
  ;; -- end team member
]

Approach 2

With this schema we invert both membership and permissions. :target/members allows for adding any members onto any target (in this case team). And :target/permissions allows for adding any permissions onto the target (team or team members). The neat thing about Datomic and it’s free associating model, is that we can add this to anything we want, and only the part of the schema we actually need.

With those two schemas we can now model a team that have members, where each member has permissions. We can also model, with the exact same schema, a team that has permissions and has x amount of members, where each member inherit the permissions that the team has. Or a single entity that simply has global permissions.

Different Clojure schemas
;; A
{:team/id #uuid "some-uuid"
 :team/name "My fantastic team"
 :team/members [{:team.member/user #uuid "member1-uuid"
                 :team.member/permissions #{:team/delete
                                            :team/add-member
                                            :team/remove-member
                                            :team/view}}
                {:team.member/user #uuid "member2-uuid"
                 :team.member/permissions #{:team/view}}]}

;; B
{:team/id #uuid "some-other-uuid"
 :team/name "My fantastic team (v2)"
 :team/permissions #{:team/delete
                     :team/add-member
                     :team/remove-member
                     :team/view}
 :team/members [{:team.member/user #uuid "member1-uuid"}
                {:team.member/user #uuid "member2-uuid"}]}
;; C
{:user/id #uuid "another-uuid"
 :user/name "Super admin"
 :user/permissions #{:all-the-permissions}}
Datomic schema 2
[ ;; -- user
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :user/id
    :db/index              true
    :db/unique             :db.unique/identity
    :db/valueType          :db.type/uuid
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :user/email
    :db/index              true
    :db/unique             :db.unique/identity
    :db/valueType          :db.type/string
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  ;; -- end user

  ;; -- team
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team/id
    :db/index              true
    :db/unique             :db.unique/identity
    :db/valueType          :db.type/uuid
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :team/name
    :db/valueType          :db.type/string
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]
  ;; -- end team

  ;; -- target
  ;; reference to user or team or <insert-here>
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :target/permissions
    :db/valueType          :db.type/keyword
    :db/cardinality        :db.cardinality/many
    :db.install/_attribute :db.part/db}]
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :target/members
    :db/valueType          :db.type/ref
    :db/cardinality        :db.cardinality/many
    :db.install/_attribute :db.part/db}]
  ;; -- end target
]

Approach 3 (addendum)

We can supplement approach 2 with :target/user. With this, it’s now possible to create entities that would constitute a new entity in an ER diagram that would sit between say a team and users that are members of that team.

Datomic schema 3
  [{:db/id                 #db/id[:db.part/db]
    :db/ident              :target/user
    :db/valueType          :db.type/ref
    :db/cardinality        :db.cardinality/one
    :db.install/_attribute :db.part/db}]

Mapping to Clojure schema

For both approaches with Datomic we can map it to the Clojure schema, to be consumed by various clients and to be used internally in the application/service. The first approach is a very simple 1-to-1 mapping, which has limited flexibility. The second approach is an abstraction that allows for more flexibility. In both cases you would get out the same Clojure schema from Datomic.

I came across this problem working on a new project, where I currently have three different entities in the project needing permissions (account, team and workspace). Far too much time has been spent on trying to figure out how to model this, without backing myself into a corner. In addition I do not yet know to what extent permissions are needed for each type of entity, only that each entity will require both membership and permissions. So my hope is that approach 2 will be just about right.

Cheers
Tags: clojure