schema.lit

  1@code_type sql .sql
  2@comment_type -- %s
  3@compiler lit -t schema.lit && bash -c 'sql-formatter -l sqlite --fix **/*.sql' && bash -c 'for f in **/*.sql; do echo "GENERATED - DO NOT MODIFY. See schema.lit" >> $f; done'
  4
  5@title Attribute-based Access Control
  6
  7This is an ABAC module implemented in Go and SQL (using sqlite3).  It is meant to be embedded in a `users` database
  8and called and exposed through higher-level interfaces to the rest of the program. It provides low-level functions
  9for assigning and checking permissions, and persisting/querying them in a database.  Much of the logic is implemented
 10in SQL to ensure the correct permissions under various conflicting scenarios using relational constraints.  Literate
 11programming is used to explain the design and generate the SQL files that are embedded in the Go library.
 12
 13@s Schema
 14
 15The basic schema consists of three tables and a materialized view of the permissions set.  Goose is used to handle
 16migrations.
 17
 18The `permissions` table uses a zanzibar-style system.  There should be one answer to the question: Can `{actor}` do `{action}`
 19to `{resource}`.  Permissions are simplified to a bit-field integer (see permissions.go).  Resources have a `type` and
 20an `id` (a int64 snowflake primary key).  For example, in a blog system the type might be `blog` corresponding to a
 21path of `blog/{article_id}`.  Resource type must always be provided, but the id may be NULL.  Nulls are interpreted as
 22matching any id.  So, a row that contains a permission for `type=blog id=NULL` is the default permission for that actor unless
 23for any blog resource type unless a more specific permission is given to a resource by id.
 24
 25The actor can be one of two types: either an id for a specific actor (user id) or a reference to an attribute that defines
 26a group of one or more users.  In this way, permissions can be granted based on the specifics of the user or a persona
 27created by an attribute.
 28
 29--- schema/001_base.sql
 30-- +goose Up
 31-- +goose StatementBegin
 32CREATE TABLE IF NOT EXISTS permissions (
 33  resource_type TEXT NOT NULL,
 34  resource_id INTEGER,
 35  user_id INTEGER,
 36  attribute_id INTEGER,
 37  permissions INTEGER NOT NULL
 38);
 39
 40---
 41
 42Attributes are unique key, value pairs of any type.
 43
 44--- schema/001_base.sql +=
 45CREATE TABLE IF NOT EXISTS attributes (key TEXT NOT NULL, value BLOB NOT NULL);
 46
 47---
 48
 49To assign one or more users to an attribute, a join table connects the unique key,value attribute with a user ID.
 50
 51--- schema/001_base.sql +=
 52CREATE TABLE IF NOT EXISTS attribute_user (
 53  attribute_id INTEGER NOT NULL,
 54  user_id INTEGER NOT NULL
 55);
 56
 57---
 58
 59A materialized view creates a table `abac` that consists of permissions rules associated either with a specific
 60`user_id` or with NULL fields that represent default permissions for that `resource_type` or `resource_id`.  The table
 61is joined with the attributes table to create a permissions row for each user and resource.  For example, if permissions
 62are given to the attribute 'role: users' and you add give that attribute to three users, the materialized view creates
 63three rows in the table, one for each `resource->user->permission` that is derived from the attribute permission.
 64
 65--- schema/001_base.sql +=
 66CREATE VIEW IF NOT EXISTS abac (resource_type, resource_id, user_id, permissions) AS
 67SELECT
 68  permissions.resource_type,
 69  permissions.resource_id,
 70  COALESCE(permissions.user_id, attribute_user.user_id) AS user_id,
 71  permissions.permissions
 72FROM
 73  permissions
 74  LEFT JOIN attribute_user USING (attribute_id);
 75
 76---
 77
 78Unique indexes are used to ensure that there is a single unambigious row for each resource and user combination.
 79
 80--- schema/001_base.sql +=
 81CREATE UNIQUE INDEX IF NOT EXISTS permission_idx_user ON permissions (resource_type, resource_id, user_id)
 82WHERE
 83  attribute_id IS NULL;
 84
 85CREATE UNIQUE INDEX IF NOT EXISTS permission_idx_att ON permissions (resource_type, resource_id, attribute_id)
 86WHERE
 87  user_id IS NULL;
 88
 89CREATE UNIQUE INDEX IF NOT EXISTS permission_idx_def_res ON permissions (resource_type, resource_id)
 90WHERE
 91  user_id IS NULL
 92  AND attribute_id IS NULL;
 93
 94CREATE UNIQUE INDEX IF NOT EXISTS permission_idx_def_type ON permissions (resource_type)
 95WHERE
 96  resource_id IS NULL
 97  AND user_id IS NULL
 98  AND attribute_id IS NULL;
 99
100CREATE UNIQUE INDEX IF NOT EXISTS attribute_user_idx ON attribute_user (attribute_id, user_id);
101
102CREATE UNIQUE INDEX IF NOT EXISTS attribute_idx ON attributes (key, value);
103-- +goose StatementEnd
104
105---
106
107And a corresponding rollback is provided for migrations.
108
109--- schema/001_base.sql +=
110-- +goose Down
111-- +goose StatementBegin
112DROP TABLE permissions;
113DROP TABLE attributes;
114DROP TABLE attribute_user;
115DROP INDEX permission_idx_user;
116DROP INDEX permission_idx_att;
117DROP INDEX permission_idx_def_res;
118DROP INDEX permission_idx_def_type;
119DROP INDEX attribute_user_idx;
120DROP INDEX attribute_idx;
121-- +goose StatementEnd
122---
123
124<h1>Adding and querying permissions</h1>
125
126@s Setting the default for a type
127
128ABAC provides a system for a fallback "default" permission to be set for a particular resource type or a specific
129resource identified by ID.  This is a namespaced system where every resource is defined as `type:id` or can be mapped to
130URLs as `type/id`.  For example, a blog may be identified by `blog/{id}`.  To define default permissions for a type,
131fields `resource_id`, `user_id`, and `attribute_id` are `NULL`.
132
133--- set_default_for_type.sql
134INSERT INTO permissions (resource_type, permissions, resource_id, user_id)
135	VALUES (?, ?, NULL, NULL)
136ON CONFLICT
137	DO UPDATE SET permissions=excluded.permissions;
138---
139
140@s Setting the default for a resource
141
142Similarly, a particular resource identified by `type/id` can have a default permission set that applies to all users
143unless a more specific rule is associated with an actor.
144
145--- set_default_for_resource.sql
146INSERT INTO permissions (resource_type, resource_id, permissions, user_id)
147	VALUES (?, ?, ?, NULL)
148ON CONFLICT
149	DO UPDATE SET permissions=excluded.permissions;
150---
151
152@s Setting a permission for a user
153
154For a specific actor, permissions can apply to a particular resource identified by `resource_id` or a class of resources
155identified by `resource_type`.
156
157--- set_permission_for_user.sql
158INSERT INTO permissions (resource_type, resource_id, user_id, permissions)
159	VALUES (?, ?, ?, ?)
160ON CONFLICT
161	DO UPDATE SET permissions=excluded.permissions;
162---
163
164A user can also have a default permission for a type.  This is often be used for blanket rules (e.g., banning an actor from accessing a class of resources or controlling administrative routes.)
165
166--- set_default_for_user.sql
167INSERT INTO permissions (resource_type, resource_id, user_id, permissions)
168	VALUES (?, NULL, ?, ?)
169ON CONFLICT
170	DO UPDATE SET permissions=excluded.permissions;
171---
172
173@s Setting a permission for an attribute
174
175Similarly, permissions can be set for an attribute.  In the database, creation the attribute and assigning the permission
176happens in a transaction and uses conflict rules to ensure uniqueness.  This provides a simplified interface for consumers
177who don't need to care about the database schema and can think instead of key/value attributes and permissions.
178
179--- set_permission_for_attribute.sql
180BEGIN TRANSACTION;
181INSERT INTO attributes (key, value) VALUES (?, ?) ON CONFLICT DO NOTHING;
182INSERT INTO permissions (resource_type, resource_id, attribute_id, permissions)
183	VALUES (
184			?,
185			?,
186			(SELECT rowid FROM attributes WHERE key=? AND value=?),
187			?
188			)
189ON CONFLICT
190	DO UPDATE SET permissions=excluded.permissions;
191COMMIT;
192---
193
194An attribute can also be used to set the default permissions for a type.
195
196--- set_default_for_attribute.sql
197BEGIN TRANSACTION;
198INSERT INTO attributes (key, value) VALUES (?, ?) ON CONFLICT DO NOTHING;
199INSERT INTO permissions (resource_type, resource_id, attribute_id, permissions)
200	VALUES (
201			?,
202			NULL,
203			(SELECT rowid FROM attributes WHERE key=? AND value=?),
204			?
205			)
206ON CONFLICT
207	DO UPDATE SET permissions=excluded.permissions;
208COMMIT;
209---
210
211Attributes are set on a user and created in the database if needed.
212
213--- set_attribute_for_user.sql
214BEGIN TRANSACTION;
215INSERT INTO attributes (key, value) VALUES (?, ?) ON CONFLICT DO NOTHING;
216INSERT INTO attribute_user (attribute_id, user_id)
217	VALUES (
218			(SELECT rowid FROM attributes WHERE key=? AND value=?)
219			, ?)
220ON CONFLICT
221	DO NOTHING;
222COMMIT;
223---
224
225@s Querying permissions for a user
226
227Querying makes use of the database schema and ordering behavior to return a single permission that reflects
228this precedence:
229
230* (1) exact match of `type/id` with a rule that resolves to the `user_id` directly or from an attribute (highest permission wins)</li>
231* (2) default for the `resource_id`</li>
232* (3) default for the `resource_type`</li>
233
234--- get_permission.sql
235SELECT permissions FROM abac
236WHERE
237	resource_type=?
238AND
239	(resource_id=? OR resource_id IS NULL)
240AND
241	(user_id=? OR user_id IS NULL)
242ORDER BY
243	user_id NULLS LAST,
244	resource_id NULLS LAST,
245    permissions DESC
246LIMIT 1;
247---