Skip to content

Commit a5d1591

Browse files
author
scribahti
committed
Add support for RLS
- implements Postgres exporter - new Policy model - new Policy element validator - new Policy element interpreter - tests - docs
1 parent f9928b1 commit a5d1591

45 files changed

Lines changed: 453 additions & 29 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dbml-homepage/docs/docs.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ outlines the full syntax documentations of DBML.
3030
- [Table Notes](#table-notes)
3131
- [Column Notes](#column-notes)
3232
- [TableGroup Notes](#tablegroup-notes)
33+
- [Policy Definition](#policy-definition)
3334
- [Sticky Notes](#sticky-notes)
3435
- [TableGroup](#tablegroup)
3536
- [TableGroup Notes](#tablegroup-notes-1)
@@ -508,6 +509,38 @@ TableGroup e_commerce [note: 'Contains tables that are related to e-commerce sys
508509
}
509510
```
510511

512+
## Policy Definition
513+
514+
You can define RLS policies. dbml2sql only supports export for Postgres.
515+
516+
```text
517+
Table users {
518+
id integer [pk]
519+
...
520+
}
521+
522+
Policy {
523+
name 'Users can take all actions on their own accounts'
524+
schema public
525+
table users
526+
behavior permissive
527+
command all
528+
roles [authenticated]
529+
using `auth.uid() = id`
530+
check null
531+
}
532+
```
533+
534+
`schema` defaults to public if not provided.
535+
536+
`behavior` can have the values `permissive` or `restrictive`. It defaults to `permissive` if not provided.
537+
538+
`command` can have the values `select`, `insert`, `update`, `delete`, or `all`.
539+
540+
`roles` accepts a list of roles and defaults to `public` if none are provided.
541+
542+
`using` and `check` default to null if not provided.
543+
511544
## Sticky Notes
512545

513546
You can add sticky notes to the diagram canvas to serve as a quick reminder or to elaborate on a complex idea.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Table users {
2+
id int [pk]
3+
email varchar
4+
}
5+
6+
Table posts {
7+
id int [pk]
8+
user_id int
9+
title varchar
10+
}
11+
12+
Policy {
13+
name 'Users can view their own data'
14+
schema public
15+
table users
16+
behavior permissive
17+
command select
18+
roles [authenticated]
19+
using `auth.uid() = id`
20+
check null
21+
}
22+
23+
Policy {
24+
name 'Users can insert their own posts'
25+
table posts
26+
command insert
27+
roles [authenticated, admin]
28+
using null
29+
check `auth.uid() = user_id`
30+
}
31+
32+
Policy {
33+
name 'Public read access'
34+
table posts
35+
command select
36+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
CREATE TABLE "users" (
2+
"id" int PRIMARY KEY,
3+
"email" varchar
4+
);
5+
6+
CREATE TABLE "posts" (
7+
"id" int PRIMARY KEY,
8+
"user_id" int,
9+
"title" varchar
10+
);
11+
12+
CREATE POLICY "Users can view their own data" ON "public"."users"
13+
AS PERMISSIVE
14+
FOR SELECT
15+
TO authenticated
16+
USING (auth.uid() = id);
17+
18+
CREATE POLICY "Users can insert their own posts" ON "public"."posts"
19+
AS PERMISSIVE
20+
FOR INSERT
21+
TO authenticated, admin
22+
WITH CHECK (auth.uid() = user_id);
23+
24+
CREATE POLICY "Public read access" ON "public"."posts"
25+
AS PERMISSIVE
26+
FOR SELECT
27+
TO public;

packages/dbml-core/src/export/PostgresExporter.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,21 @@ class PostgresExporter {
540540
return commentArr;
541541
}
542542

543+
static exportPolicies (policyIds, model) {
544+
return policyIds.map((policyId) => {
545+
const policy = model.policies[policyId];
546+
let line = `CREATE POLICY "${policy.name}"`;
547+
line += ` ON "${policy.schemaName}"."${policy.tableName}"`;
548+
line += `\n AS ${policy.behavior.toUpperCase()}`;
549+
line += `\n FOR ${policy.command.toUpperCase()}`;
550+
line += `\n TO ${policy.roles.join(', ')}`;
551+
if (policy.using) line += `\n USING (${policy.using})`;
552+
if (policy.check) line += `\n WITH CHECK (${policy.check})`;
553+
line += ';\n';
554+
return line;
555+
});
556+
}
557+
543558
static export (model) {
544559
const database = model.database['1'];
545560

@@ -623,6 +638,10 @@ class PostgresExporter {
623638
]
624639
: [];
625640

641+
const policyStatements = database.policyIds
642+
? PostgresExporter.exportPolicies(database.policyIds, model)
643+
: [];
644+
626645
const res = concat(
627646
statements.schemas,
628647
statements.enums,
@@ -631,6 +650,7 @@ class PostgresExporter {
631650
statements.comments,
632651
statements.refs,
633652
recordsSection,
653+
policyStatements,
634654
).join('\n');
635655
return res;
636656
}

packages/dbml-core/src/model_structure/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export const NOTE = 'note';
44
export const ENUM = 'enum';
55
export const REF = 'ref';
66
export const TABLE_GROUP = 'table_group';
7+
export const POLICY = 'policy';

packages/dbml-core/src/model_structure/database.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import TableGroup from './tableGroup';
66
import Table from './table';
77
import StickyNote from './stickyNote';
88
import Element from './element';
9+
import Policy from './policy';
910
import {
1011
DEFAULT_SCHEMA_NAME, TABLE, TABLE_GROUP, ENUM, REF, NOTE,
1112
} from './config';
@@ -24,6 +25,7 @@ class Database extends Element {
2425
aliases = [],
2526
records = [],
2627
tablePartials = [],
28+
policies = [],
2729
}) {
2830
super();
2931
this.dbState = new DbState();
@@ -39,6 +41,7 @@ class Database extends Element {
3941
this.aliases = aliases;
4042
this.records = [];
4143
this.tablePartials = [];
44+
this.policies = [];
4245

4346
// The global array containing references with 1 endpoint being a field injected from a partial to a table
4447
// These refs are add to this array when resolving partials in tables (`Table.processPartials()`)
@@ -48,6 +51,7 @@ class Database extends Element {
4851
this.processNotes(notes);
4952
this.processRecords(records);
5053
this.processTablePartials(tablePartials);
54+
this.processPolicies(policies);
5155
this.processSchemas(schemas);
5256
this.processSchemaElements(enums, ENUM);
5357
this.processSchemaElements(tables, TABLE);
@@ -93,6 +97,12 @@ class Database extends Element {
9397
});
9498
}
9599

100+
processPolicies (rawPolicies) {
101+
rawPolicies.forEach((rawPolicy) => {
102+
this.policies.push(new Policy({ ...rawPolicy, database: this }));
103+
});
104+
}
105+
96106
pushNote (note) {
97107
this.checkNote(note);
98108
this.notes.push(note);
@@ -233,13 +243,15 @@ class Database extends Element {
233243
schemas: this.schemas.map((s) => s.export()),
234244
notes: this.notes.map((n) => n.export()),
235245
records: this.records.map((r) => ({ ...r })),
246+
policies: this.policies.map((p) => p.export()),
236247
};
237248
}
238249

239250
exportChildIds () {
240251
return {
241252
schemaIds: this.schemas.map((s) => s.id),
242253
noteIds: this.notes.map((n) => n.id),
254+
policyIds: this.policies.map((p) => p.id),
243255
};
244256
}
245257

@@ -266,12 +278,14 @@ class Database extends Element {
266278
fields: {},
267279
records: {},
268280
tablePartials: {},
281+
policies: {},
269282
};
270283

271284
this.schemas.forEach((schema) => schema.normalize(normalizedModel));
272285
this.notes.forEach((note) => note.normalize(normalizedModel));
273286
this.records.forEach((record) => { normalizedModel.records[record.id] = { ...record }; });
274287
this.tablePartials.forEach((tablePartial) => tablePartial.normalize(normalizedModel));
288+
this.policies.forEach((policy) => policy.normalize(normalizedModel));
275289
return normalizedModel;
276290
}
277291
}

packages/dbml-core/src/model_structure/dbState.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class DbState {
1515
this.indexColumnId = 1;
1616
this.recordId = 1;
1717
this.tablePartialId = 1;
18+
this.policyId = 1;
1819
}
1920

2021
generateId (el) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Element from './element';
2+
3+
class Policy extends Element {
4+
constructor ({
5+
name, schemaName, tableName, behavior, command, roles, using, check, token, database = {},
6+
} = {}) {
7+
super(token);
8+
this.name = name;
9+
this.schemaName = schemaName;
10+
this.tableName = tableName;
11+
this.behavior = behavior;
12+
this.command = command;
13+
this.roles = roles;
14+
this.using = using;
15+
this.check = check;
16+
this.database = database;
17+
this.dbState = this.database.dbState;
18+
this.generateId();
19+
}
20+
21+
generateId () {
22+
this.id = this.dbState.generateId('policyId');
23+
}
24+
25+
shallowExport () {
26+
return {
27+
name: this.name,
28+
schemaName: this.schemaName,
29+
tableName: this.tableName,
30+
behavior: this.behavior,
31+
command: this.command,
32+
roles: this.roles,
33+
using: this.using,
34+
check: this.check,
35+
};
36+
}
37+
38+
export () {
39+
return {
40+
...this.shallowExport(),
41+
};
42+
}
43+
44+
normalize (model) {
45+
model.policies[this.id] = {
46+
id: this.id,
47+
...this.shallowExport(),
48+
};
49+
}
50+
}
51+
52+
export default Policy;

packages/dbml-parse/__tests__/snapshots/interpreter/output/array_type.out.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,6 @@
151151
"aliases": [],
152152
"project": {},
153153
"tablePartials": [],
154-
"records": []
154+
"records": [],
155+
"policies": []
155156
}

packages/dbml-parse/__tests__/snapshots/interpreter/output/checks.out.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,5 +362,6 @@
362362
]
363363
}
364364
],
365-
"records": []
365+
"records": [],
366+
"policies": []
366367
}

0 commit comments

Comments
 (0)