Modelling permissions in Datomic

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.
{: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.
[ ;; -- 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.
;; 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}}
[ ;; -- 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.
[{: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.
