@cerebruminc/yates 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ name: Integration testing
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, closed]
6
+
7
+ jobs:
8
+ docker_test:
9
+ permissions:
10
+ id-token: write
11
+ contents: read
12
+ runs-on: ubuntu-20.04
13
+ name: integration testing in docker compose
14
+ defaults:
15
+ run:
16
+ shell: bash --noprofile --norc -eo pipefail -x {0}
17
+
18
+ env:
19
+ DOCKER_BUILDKIT: "1"
20
+
21
+ strategy:
22
+ fail-fast: false
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v1
27
+
28
+ - name: Run docker compose tests
29
+ # In a test environment we can use default env vars, by copying from .env.sample
30
+ run: |
31
+ cp .env.sample .env
32
+ yarn run test:compose:integration
@@ -0,0 +1,41 @@
1
+ name: Publish beta package
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - master
6
+ jobs:
7
+ release:
8
+ runs-on: ubuntu-latest
9
+ env:
10
+ CEREBRUM_NPM_TOKEN: ${{ secrets.CEREBRUM_NPM_TOKEN }}
11
+ steps:
12
+ # Checkout project repository
13
+ - name: Checkout
14
+ uses: actions/checkout@v2.3.4
15
+
16
+ # Setup Node.js environment
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v2
19
+ with:
20
+ registry-url: https://registry.npmjs.org/
21
+ node-version: 16
22
+
23
+ # Use a cache for dependencies
24
+ - uses: actions/cache@v2.1.4
25
+ with:
26
+ path: "**/node_modules"
27
+ key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
28
+
29
+ # Install dependencies so that prepublish scripts work as expected
30
+ - name: Install deps
31
+ run: npm i
32
+
33
+ # Update package version to a beta release, including the git commit sha
34
+ - name: Bump package.json
35
+ run: |
36
+ npm --no-git-tag-version version $(npm show . version)-beta.dangerous.$(git rev-parse --short HEAD)
37
+ # Publish version to npm
38
+ - name: Publish beta package
39
+ run: npm publish --tag beta
40
+ env:
41
+ NODE_AUTH_TOKEN: ${{ secrets.CEREBRUM_PUBLISH_NPM_TOKEN }}
@@ -0,0 +1,33 @@
1
+ on:
2
+ push:
3
+ branches:
4
+ - master
5
+
6
+ name: release-please
7
+ jobs:
8
+ release-please:
9
+ runs-on: ubuntu-latest
10
+ env:
11
+ CEREBRUM_NPM_TOKEN: ${{ secrets.CEREBRUM_NPM_TOKEN }}
12
+ steps:
13
+ - uses: google-github-actions/release-please-action@v3
14
+ id: release
15
+ with:
16
+ release-type: node
17
+ package-name: release-please-action
18
+ # The logic below handles the npm publication:
19
+ - uses: actions/checkout@v2
20
+ # these if statements ensure that a publication only occurs when
21
+ # a new release is created:
22
+ if: ${{ steps.release.outputs.release_created }}
23
+ - uses: actions/setup-node@v1
24
+ with:
25
+ node-version: 16
26
+ registry-url: "https://registry.npmjs.org"
27
+ if: ${{ steps.release.outputs.release_created }}
28
+ - run: npm i
29
+ if: ${{ steps.release.outputs.release_created }}
30
+ - run: npm publish
31
+ env:
32
+ NODE_AUTH_TOKEN: ${{secrets.CEREBRUM_PUBLISH_NPM_TOKEN}}
33
+ if: ${{ steps.release.outputs.release_created }}
@@ -0,0 +1,14 @@
1
+ name: Unit test
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v1
10
+ - uses: actions/setup-node@v1
11
+ with:
12
+ node-version: 16
13
+ - run: npm i
14
+ - run: npm test
package/Dockerfile.sut ADDED
@@ -0,0 +1,13 @@
1
+ # This container is used to run e2e tests against the built API container
2
+ FROM node:latest
3
+
4
+ COPY package.json /app/package.json
5
+ COPY package-lock.json /app/package-lock.json
6
+
7
+ WORKDIR /app
8
+
9
+ RUN npm i
10
+
11
+ COPY . .
12
+
13
+ CMD /bin/bash -c "npm run setup && npm run test:integration"
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ <div align="center">
2
+ <img width="200" height="200" src="./images/yates-icon.png">
3
+
4
+ <h1>Yates = Prisma + RLS</h1>
5
+
6
+ <p>
7
+ A module for implementing role based access control with Prisma when using Postgres
8
+ </p>
9
+ <br>
10
+ </div>
11
+
12
+ > English: from Middle English *yates* ‘gates’ plural of *yate* Old English *geat* ‘gate’ hence a topographic or occupational name for someone who lived by the gates of a town or castle and who probably acted as the gatekeeper or porter.
13
+
14
+ <br>
15
+
16
+ Yates is a module for implementing role based access control with Prisma. It is designed to be used with the [Prisma Client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client) and [PostgreSQL](https://www.postgresql.org/).
17
+ It uses the [Row Level Security](https://www.postgresql.org/docs/9.5/ddl-rowsecurity.html) feature of PostgreSQL to provide a simple and secure way to implement role based access control that allows you to define complex access control rules and have them apply to all of your Prisma queries automatically.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm i @cerebruminc/yates
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Once you've installed Yates, you can use it in your Prisma project by importing it and calling the `setup` function. This function takes a Prisma Client instance and a configuration object as arguments. Yates uses prisma middleware to intercept all queries and apply the appropriate row level security policies to them, so it's important that it is setup _after_ all other middleware has been applied to the Prisma client.
28
+
29
+ The `setup` function will generate CRUD abilities for each model in your Prisma schema, as well as any additional abilities that you have defined in your configuration. It will then create a new PG role for each ability and apply the appropriate row level security policies to each role. Finally, it will create a new PG role for each user role you specify and grant them the appropriate abilities.
30
+ For Yates to be able to set the correct user role for each request, you must pass a function called `getContext` in the `setup` configuration that will return the user role for the current request. This function will be called for each request and the user role returned will be used to set the `role` in the current session. If you want to bypass RLS completely for a specific role, you can return `null` from the `getContext` function for that role.
31
+ For accessing the context of a Prisma query, we recommend using a package like [cls-hooked](https://www.npmjs.com/package/cls-hooked) to store the context in the current session.
32
+
33
+ ```ts
34
+ import { setup } from "@cerebruminc/yates";
35
+ import { PrismaClient } from "@prisma/client";
36
+
37
+
38
+ const prisma = new PrismaClient();
39
+
40
+ await setup({
41
+ prisma,
42
+ // Define any custom abilities that you want to add to the system.
43
+ customAbilities: () => ({
44
+ USER: {
45
+ User: {
46
+ updateOwnUser: {
47
+ description: "Update own user",
48
+ // This expression uses a context setting returned by the getContext function
49
+ expression: `current_setting('context.user.id') = "id"`,
50
+ operation: "UPDATE",
51
+ },
52
+ }
53
+ }
54
+
55
+ })
56
+ // Return a mapping of user roles and abilities.
57
+ // This function is paramaterised with a list of all CRUD abilities that have been
58
+ // automatically generated by Yates, as well as any customAbilities that have been defined.
59
+ getRoles: (abilities) => {
60
+ return {
61
+ SUPER_ADMIN: "*",
62
+ USER: [
63
+ abilities.User.read,
64
+ abilities.Comment.read
65
+ ],
66
+ };
67
+ },
68
+ getContext: () => {
69
+ // Here we are using cls-hooked to access the context in the current session.
70
+ const ctx = clsSession.get("ctx");
71
+ if (!ctx) {
72
+ return null;
73
+ }
74
+ const { user } = ctx;
75
+
76
+ let role = user.role;
77
+
78
+ const role = user.role
79
+
80
+ return {
81
+ role,
82
+ context: {
83
+ // This context setting will be available in ability expressions using `current_setting('context.user.id')`
84
+ 'context.user.id': user.id,
85
+ },
86
+ };
87
+ },
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ ### Abilities
93
+
94
+ When defining an ability you need to provide the following properties:
95
+
96
+ - `description`: A description of the ability.
97
+ - `expression`: A boolean SQL expression that will be used to filter the results of the query. This expression can use any of the columns in the table that the ability is being applied to, as well as any context settings that have been defined in the `getContext` function.
98
+
99
+ - For `INSERT`, `UPDATE` and `DELETE` operations, the expression uses the values from the row being inserted. If the expression returns `false` for a row, that row will not be inserted, updated or deleted.
100
+ - For `SELECT` operations, the expression uses the values from the row being returned. If the expression returns `false` for a row, that row will not be returned.
101
+
102
+ - `operation`: The operation that the ability is being applied to. This can be one of `CREATE`, `READ`, `UPDATE` or `DELETE`.
103
+
104
+ ## License
105
+
106
+ The project is licensed under the MIT license.
107
+
108
+ <br>
109
+ <br>
110
+
111
+ <div align="center">
112
+
113
+ ![Cerebrum](./images/powered-by-cerebrum-lm.png#gh-light-mode-only)
114
+ ![Cerebrum](./images/powered-by-cerebrum-dm.svg#gh-dark-mode-only)
115
+
116
+ </div>
@@ -0,0 +1,55 @@
1
+ import { Prisma, PrismaClient } from "@prisma/client";
2
+ export type Models = Prisma.ModelName;
3
+ export interface Ability {
4
+ description?: string;
5
+ expression?: string;
6
+ operation: "SELECT" | "UPDATE" | "INSERT" | "DELETE";
7
+ model?: Models;
8
+ slug?: string;
9
+ }
10
+ export type ModelAbilities = {
11
+ [Model in Models]: {
12
+ [op: string]: Ability;
13
+ };
14
+ };
15
+ export type GetContextFn = () => {
16
+ role: string;
17
+ context: {
18
+ [key: string]: string;
19
+ };
20
+ } | null;
21
+ export declare const createAbilityName: (model: string, ability: string) => string;
22
+ export declare const createRoleName: (name: string) => string;
23
+ export declare const setupMiddleware: (prisma: PrismaClient, getContext: GetContextFn) => void;
24
+ export declare const createRoles: ({ prisma, customAbilities, getRoles, }: {
25
+ prisma: PrismaClient;
26
+ customAbilities?: Partial<ModelAbilities> | undefined;
27
+ getRoles: (abilities: ModelAbilities) => {
28
+ [key: string]: Ability[] | "*";
29
+ };
30
+ }) => Promise<void>;
31
+ export interface SetupParams {
32
+ /**
33
+ * The Prisma client instance. Used for database queries and model introspection.
34
+ */
35
+ prisma: PrismaClient;
36
+ /**
37
+ * Custom abilities to add to the default abilities.
38
+ */
39
+ customAbilities?: Partial<ModelAbilities>;
40
+ /**
41
+ * A function that returns the roles for your application.
42
+ * This is paramaterised by the abilities, so you can use it to create roles that are a combination of abilities.
43
+ */
44
+ getRoles: (abilities: ModelAbilities) => {
45
+ [key: string]: Ability[] | "*";
46
+ };
47
+ /**
48
+ * A function that returns the context for the current request.
49
+ * This is called on every prisma query, and is needed to determine the current user's role.
50
+ * You can also provide additional context here, which will be available in any RLS expressions you've defined.
51
+ * Returning `null` will result in the permissions being skipped entirely.
52
+ */
53
+ getContext: GetContextFn;
54
+ }
55
+ export declare const setup: ({ prisma, customAbilities, getRoles, getContext }: SetupParams) => Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
3
+ if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
4
+ return cooked;
5
+ };
6
+ var __assign = (this && this.__assign) || function () {
7
+ __assign = Object.assign || function(t) {
8
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
9
+ s = arguments[i];
10
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
11
+ t[p] = s[p];
12
+ }
13
+ return t;
14
+ };
15
+ return __assign.apply(this, arguments);
16
+ };
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __generator = (this && this.__generator) || function (thisArg, body) {
27
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
28
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
29
+ function verb(n) { return function (v) { return step([n, v]); }; }
30
+ function step(op) {
31
+ if (f) throw new TypeError("Generator is already executing.");
32
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
33
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
34
+ if (y = 0, t) op = [op[0] & 2, t.value];
35
+ switch (op[0]) {
36
+ case 0: case 1: t = op; break;
37
+ case 4: _.label++; return { value: op[1], done: false };
38
+ case 5: _.label++; y = op[1]; op = [0]; continue;
39
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
40
+ default:
41
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
42
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
43
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
44
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
45
+ if (t[2]) _.ops.pop();
46
+ _.trys.pop(); continue;
47
+ }
48
+ op = body.call(thisArg, _);
49
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
50
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
+ }
52
+ };
53
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
54
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
55
+ if (ar || !(i in from)) {
56
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
57
+ ar[i] = from[i];
58
+ }
59
+ }
60
+ return to.concat(ar || Array.prototype.slice.call(from));
61
+ };
62
+ var __importDefault = (this && this.__importDefault) || function (mod) {
63
+ return (mod && mod.__esModule) ? mod : { "default": mod };
64
+ };
65
+ exports.__esModule = true;
66
+ exports.setup = exports.createRoles = exports.setupMiddleware = exports.createRoleName = exports.createAbilityName = void 0;
67
+ var flatMap_1 = __importDefault(require("lodash/flatMap"));
68
+ var map_1 = __importDefault(require("lodash/map"));
69
+ var toPairs_1 = __importDefault(require("lodash/toPairs"));
70
+ var createAbilityName = function (model, ability) {
71
+ return "".concat(model, "_").concat(ability, "_role").toLowerCase();
72
+ };
73
+ exports.createAbilityName = createAbilityName;
74
+ var createRoleName = function (name) {
75
+ return "yates_role_".concat(name.toLowerCase());
76
+ };
77
+ exports.createRoleName = createRoleName;
78
+ // This middleware is used to set the role and context for the current user so that RLS can be applied
79
+ // It must be the *last* middleware in the chain, as it will call the original function itself and return the value.
80
+ var setupMiddleware = function (prisma, getContext) {
81
+ prisma.$use(function (params, next) { return __awaiter(void 0, void 0, void 0, function () {
82
+ var ctx, role, context, pgRole, modelName, txResults, queryResults, selectKeys, e_1;
83
+ var _a;
84
+ return __generator(this, function (_b) {
85
+ switch (_b.label) {
86
+ case 0:
87
+ if (!params.model || params.action === "queryRaw" || params.runInTransaction) {
88
+ return [2 /*return*/, next(params)];
89
+ }
90
+ ctx = getContext();
91
+ // If ctx is null, the middleware is explicitly skipped
92
+ if (ctx === null) {
93
+ return [2 /*return*/, next(params)];
94
+ }
95
+ role = ctx.role, context = ctx.context;
96
+ pgRole = (0, exports.createRoleName)(role);
97
+ // Check the role name only has lowercase alpha characters and underscores
98
+ // This also doubles as a check against SQL injection
99
+ if (pgRole.match(/[^a-z_]/)) {
100
+ throw new Error("Invalid role name.");
101
+ }
102
+ modelName = params.model.charAt(0).toLowerCase() + params.model.slice(1);
103
+ _b.label = 1;
104
+ case 1:
105
+ _b.trys.push([1, 3, , 4]);
106
+ return [4 /*yield*/, prisma.$transaction(__spreadArray(__spreadArray([
107
+ // Switch to the user role, We can't use a prepared statement here, due to limitations in PG not allowing prepared statements to be used in SET ROLE
108
+ prisma.$queryRawUnsafe("SET ROLE ".concat(pgRole))
109
+ ], (0, toPairs_1["default"])(context).map(function (_a) {
110
+ var key = _a[0], value = _a[1];
111
+ var keySafe = key.replace(/[^a-z_\.]/g, "");
112
+ return prisma.$queryRaw(templateObject_1 || (templateObject_1 = __makeTemplateObject(["SELECT set_config(", ", ", ", true);"], ["SELECT set_config(", ", ", ", true);"])), keySafe, value);
113
+ }), true), [
114
+ // Now call original function
115
+ // Assumptions:
116
+ // - prisma model class is params.model in camelCase
117
+ // - prisma function name is params.action
118
+ prisma[modelName][params.action](params.args),
119
+ // Switch role back to admin user
120
+ prisma.$queryRawUnsafe("SET ROLE none"),
121
+ ], false))];
122
+ case 2:
123
+ txResults = _b.sent();
124
+ queryResults = txResults[txResults.length - 2];
125
+ // This heuristic is used to determine if this is a query for a related entity, and if so, unwraps the results.
126
+ // This mimics the "native" prisma behaviour, where if you query for a related entity, it will return the related entity (or entities) directly, rather than an object with the related entity as a property.
127
+ // See https://prisma.slack.com/archives/CA491RJH0/p1674126834205399
128
+ if (params.args.select) {
129
+ selectKeys = Object.keys(params.args.select);
130
+ if (selectKeys.length === 1 &&
131
+ params.dataPath.length > 0 &&
132
+ selectKeys[0] === params.dataPath[params.dataPath.length - 1] &&
133
+ selectKeys[0] === Object.keys(queryResults)[0]) {
134
+ return [2 /*return*/, queryResults[selectKeys[0]]];
135
+ }
136
+ }
137
+ return [2 /*return*/, queryResults];
138
+ case 3:
139
+ e_1 = _b.sent();
140
+ // Normalize RLS errors to make them a bit more readable.
141
+ if ((_a = e_1.message) === null || _a === void 0 ? void 0 : _a.includes("new row violates row-level security policy for table")) {
142
+ throw new Error("You do not have permission to perform this action.");
143
+ }
144
+ throw e_1;
145
+ case 4: return [2 /*return*/];
146
+ }
147
+ });
148
+ }); });
149
+ };
150
+ exports.setupMiddleware = setupMiddleware;
151
+ var setRLS = function (prisma, table, roleName, operation, expression) { return __awaiter(void 0, void 0, void 0, function () {
152
+ var policyName, rows;
153
+ return __generator(this, function (_a) {
154
+ switch (_a.label) {
155
+ case 0:
156
+ policyName = "".concat(roleName, "_policy");
157
+ return [4 /*yield*/, prisma.$queryRawUnsafe("\n\t\tselect * from pg_catalog.pg_policies where tablename = '".concat(table, "' AND policyname = '").concat(policyName, "';\n\t"))];
158
+ case 1:
159
+ rows = _a.sent();
160
+ if (!(rows.length === 0)) return [3 /*break*/, 6];
161
+ if (!(operation === "INSERT")) return [3 /*break*/, 3];
162
+ return [4 /*yield*/, prisma.$queryRawUnsafe("\n CREATE POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" FOR ").concat(operation, " TO ").concat(roleName, " WITH CHECK (").concat(expression, ");\n "))];
163
+ case 2:
164
+ _a.sent();
165
+ return [3 /*break*/, 5];
166
+ case 3: return [4 /*yield*/, prisma.$queryRawUnsafe("\n CREATE POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" FOR ").concat(operation, " TO ").concat(roleName, " USING (").concat(expression, ");\n "))];
167
+ case 4:
168
+ _a.sent();
169
+ _a.label = 5;
170
+ case 5: return [3 /*break*/, 10];
171
+ case 6:
172
+ if (!(rows[0].qual !== expression)) return [3 /*break*/, 10];
173
+ if (!(operation === "INSERT")) return [3 /*break*/, 8];
174
+ return [4 /*yield*/, prisma.$queryRawUnsafe("\n ALTER POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" TO ").concat(roleName, " WITH CHECK (").concat(expression, ");\n "))];
175
+ case 7:
176
+ _a.sent();
177
+ return [3 /*break*/, 10];
178
+ case 8: return [4 /*yield*/, prisma.$queryRawUnsafe("\n ALTER POLICY ".concat(policyName, " ON \"public\".\"").concat(table, "\" TO ").concat(roleName, " USING (").concat(expression, ");\n "))];
179
+ case 9:
180
+ _a.sent();
181
+ _a.label = 10;
182
+ case 10: return [2 /*return*/];
183
+ }
184
+ });
185
+ }); };
186
+ var createRoles = function (_a) {
187
+ var prisma = _a.prisma, customAbilities = _a.customAbilities, getRoles = _a.getRoles;
188
+ return __awaiter(void 0, void 0, void 0, function () {
189
+ var abilities, models, _i, models_1, model, ability, roles, _b, _c, _d, _e, model, table, _f, _g, _h, _j, slug, ability, roleName, _k, _l, _m, _o, key, role, wildCardAbilities, roleAbilities, rlsRoles;
190
+ return __generator(this, function (_p) {
191
+ switch (_p.label) {
192
+ case 0:
193
+ abilities = {};
194
+ models = prisma._baseDmmf.datamodel.models.map(function (m) { return m.name; });
195
+ for (_i = 0, models_1 = models; _i < models_1.length; _i++) {
196
+ model = models_1[_i];
197
+ abilities[model] = {
198
+ create: {
199
+ description: "Create ".concat(model),
200
+ expression: "true",
201
+ operation: "INSERT",
202
+ model: model,
203
+ slug: "create"
204
+ },
205
+ read: {
206
+ description: "Read ".concat(model),
207
+ expression: "true",
208
+ operation: "SELECT",
209
+ model: model,
210
+ slug: "read"
211
+ },
212
+ update: {
213
+ description: "Update ".concat(model),
214
+ expression: "true",
215
+ operation: "UPDATE",
216
+ model: model,
217
+ slug: "update"
218
+ },
219
+ "delete": {
220
+ description: "Delete ".concat(model),
221
+ expression: "true",
222
+ operation: "DELETE",
223
+ model: model,
224
+ slug: "delete"
225
+ }
226
+ };
227
+ if (customAbilities === null || customAbilities === void 0 ? void 0 : customAbilities[model]) {
228
+ for (ability in customAbilities[model]) {
229
+ abilities[model][ability] = __assign(__assign({}, customAbilities[model][ability]), { model: model, slug: ability });
230
+ }
231
+ }
232
+ }
233
+ roles = getRoles(abilities);
234
+ _b = abilities;
235
+ _c = [];
236
+ for (_d in _b)
237
+ _c.push(_d);
238
+ _e = 0;
239
+ _p.label = 1;
240
+ case 1:
241
+ if (!(_e < _c.length)) return [3 /*break*/, 8];
242
+ _d = _c[_e];
243
+ if (!(_d in _b)) return [3 /*break*/, 7];
244
+ model = _d;
245
+ table = model;
246
+ return [4 /*yield*/, prisma.$queryRawUnsafe("ALTER table \"".concat(table, "\" enable row level security"))];
247
+ case 2:
248
+ _p.sent();
249
+ _f = abilities[model];
250
+ _g = [];
251
+ for (_h in _f)
252
+ _g.push(_h);
253
+ _j = 0;
254
+ _p.label = 3;
255
+ case 3:
256
+ if (!(_j < _g.length)) return [3 /*break*/, 7];
257
+ _h = _g[_j];
258
+ if (!(_h in _f)) return [3 /*break*/, 6];
259
+ slug = _h;
260
+ ability = abilities[model][slug];
261
+ roleName = (0, exports.createAbilityName)(model, slug);
262
+ // Check if role already exists
263
+ return [4 /*yield*/, prisma.$queryRawUnsafe("\n do\n $$\n begin\n if not exists (select * from pg_catalog.pg_roles where rolname = '".concat(roleName, "') then \n create role ").concat(roleName, ";\n GRANT ").concat(ability.operation, " ON \"").concat(table, "\" TO ").concat(roleName, ";\n end if;\n end\n $$\n ;\n "))];
264
+ case 4:
265
+ // Check if role already exists
266
+ _p.sent();
267
+ if (!ability.expression) return [3 /*break*/, 6];
268
+ return [4 /*yield*/, setRLS(prisma, table, roleName, ability.operation, ability.expression)];
269
+ case 5:
270
+ _p.sent();
271
+ _p.label = 6;
272
+ case 6:
273
+ _j++;
274
+ return [3 /*break*/, 3];
275
+ case 7:
276
+ _e++;
277
+ return [3 /*break*/, 1];
278
+ case 8:
279
+ _k = roles;
280
+ _l = [];
281
+ for (_m in _k)
282
+ _l.push(_m);
283
+ _o = 0;
284
+ _p.label = 9;
285
+ case 9:
286
+ if (!(_o < _l.length)) return [3 /*break*/, 14];
287
+ _m = _l[_o];
288
+ if (!(_m in _k)) return [3 /*break*/, 13];
289
+ key = _m;
290
+ role = (0, exports.createRoleName)(key);
291
+ return [4 /*yield*/, prisma.$queryRawUnsafe("\n do\n $$\n begin\n if not exists (select * from pg_catalog.pg_roles where rolname = '".concat(role, "') then \n create role ").concat(role, ";\n end if;\n end\n $$\n ;\n "))];
292
+ case 10:
293
+ _p.sent();
294
+ // Note: We need to GRANT all on schema public so that we can resolve relation queries with prisma, as they will sometimes use a join table.
295
+ // This is not ideal, but because we are using RLS, it's not a security risk. Any table with RLS also needs a corresponding policy for the role to have access.
296
+ return [4 /*yield*/, prisma.$queryRawUnsafe("\n GRANT ALL ON ALL TABLES IN SCHEMA public TO ".concat(role, ";\n "))];
297
+ case 11:
298
+ // Note: We need to GRANT all on schema public so that we can resolve relation queries with prisma, as they will sometimes use a join table.
299
+ // This is not ideal, but because we are using RLS, it's not a security risk. Any table with RLS also needs a corresponding policy for the role to have access.
300
+ _p.sent();
301
+ wildCardAbilities = (0, flatMap_1["default"])(abilities, function (model, modelName) {
302
+ return (0, map_1["default"])(model, function (params, slug) {
303
+ return (0, exports.createAbilityName)(modelName, slug);
304
+ });
305
+ });
306
+ roleAbilities = roles[key];
307
+ rlsRoles = roleAbilities === "*"
308
+ ? wildCardAbilities
309
+ : roleAbilities.map(function (ability) { return (0, exports.createAbilityName)(ability.model, ability.slug); });
310
+ return [4 /*yield*/, prisma.$queryRawUnsafe("GRANT ".concat(rlsRoles.join(", "), " TO ").concat(role))];
311
+ case 12:
312
+ _p.sent();
313
+ _p.label = 13;
314
+ case 13:
315
+ _o++;
316
+ return [3 /*break*/, 9];
317
+ case 14: return [2 /*return*/];
318
+ }
319
+ });
320
+ });
321
+ };
322
+ exports.createRoles = createRoles;
323
+ var setup = function (_a) {
324
+ var prisma = _a.prisma, customAbilities = _a.customAbilities, getRoles = _a.getRoles, getContext = _a.getContext;
325
+ return __awaiter(void 0, void 0, void 0, function () {
326
+ return __generator(this, function (_b) {
327
+ switch (_b.label) {
328
+ case 0: return [4 /*yield*/, (0, exports.createRoles)({ prisma: prisma, customAbilities: customAbilities, getRoles: getRoles })];
329
+ case 1:
330
+ _b.sent();
331
+ (0, exports.setupMiddleware)(prisma, getContext);
332
+ return [2 /*return*/];
333
+ }
334
+ });
335
+ });
336
+ };
337
+ exports.setup = setup;
338
+ var templateObject_1;
339
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ version: "3.7"
2
+ networks:
3
+ internal: {}
4
+ services:
5
+ sut:
6
+ build:
7
+ context: .
8
+ dockerfile: ./Dockerfile.sut
9
+ profiles:
10
+ - with-sut
11
+ depends_on:
12
+ db:
13
+ condition: service_healthy
14
+ ports:
15
+ - 8000:8000
16
+ networks:
17
+ - internal
18
+ environment:
19
+ - DATABASE_URL=postgresql://postgres:postgres@db:5432/yates?connection_limit=30
20
+
21
+ db:
22
+ image: postgres:11
23
+ restart: always
24
+ environment:
25
+ POSTGRES_USER: postgres
26
+ POSTGRES_PASSWORD: postgres
27
+ POSTGRES_DB: yates
28
+ ports:
29
+ - 5432:5432
30
+ networks:
31
+ - internal
32
+ healthcheck:
33
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
34
+ interval: 5s
35
+ timeout: 5s
36
+ retries: 5
@@ -0,0 +1,209 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg version="1.1" width="304.973854px" height="89px" viewBox="0 0 304.973853946393 89.0"
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ xmlns:xlink="http://www.w3.org/1999/xlink">
5
+ <defs>
6
+ <clipPath id="i0">
7
+ <path d="M15.3133386,0 C20.8626677,0 24.1641673,1.86148382 26.9037096,4.53078137 L22.7943962,9.27229676 C20.5114444,7.20007893 18.2284925,5.97079716 15.2782163,5.97079716 C10.3259669,5.97079716 6.74348856,10.0801105 6.74348856,15.1026046 L6.74348856,15.1728493 C6.74348856,20.1953433 10.2205998,24.4100237 15.2782163,24.4100237 C18.6499605,24.4100237 20.687056,23.0753749 23.0051302,20.9680347 L27.1144436,25.1124704 C24.0939227,28.3437253 20.7221784,30.3458365 15.0674822,30.3458365 C6.39226519,30.3808208 0,23.707577 0,15.2782163 L0,15.2079716 C0,6.84885556 6.28689818,0 15.3133386,0 Z"></path>
8
+ </clipPath>
9
+ <clipPath id="i1">
10
+ <path d="M11.1337806,1.77635684e-15 C18.6499605,1.77635684e-15 22.0919495,5.83030781 22.0919495,12.222573 C22.0919495,12.7142857 22.0568272,13.3113654 22.0217048,13.9084451 L6.35714286,13.9084451 C6.98934491,16.7884767 8.99131807,18.2987372 11.8713496,18.2987372 C14.0138122,18.2987372 15.5591949,17.6314128 17.3153118,15.980663 L20.9680347,19.2119179 C18.8606946,21.8109708 15.8401736,23.3914759 11.7659826,23.3914759 C5.02249408,23.3914759 0,18.6499605 0,11.7659826 L0,11.695738 C0,5.26835043 4.56590371,1.77635684e-15 11.1337806,1.77635684e-15 Z M11.1337806,5.09273875 C8.49960537,5.09273875 6.77861089,6.98934491 6.25177585,9.86937648 L15.875296,9.86937648 C15.4889503,7.02446725 13.8030781,5.09273875 11.1337806,5.09273875 Z"></path>
11
+ </clipPath>
12
+ <clipPath id="i2">
13
+ <path d="M13.5923441,0.0105266556 L13.5923441,6.68377054 L13.2411208,6.68377054 C8.99131807,6.68377054 6.39226519,9.24770108 6.39226519,14.6214185 L6.39226519,22.9454122 L0,22.9454122 L0,0.43199469 L6.42738753,0.43199469 L6.42738753,4.96277606 C7.72691397,1.87201048 9.83425414,-0.165085026 13.5923441,0.0105266556 Z"></path>
14
+ </clipPath>
15
+ <clipPath id="i3">
16
+ <path d="M6.39226519,0 L6.39226519,11.3796369 C7.93764799,9.27229676 10.0801105,7.72691397 13.4167324,7.72691397 C18.6499605,7.72691397 23.6724546,11.8362273 23.6724546,19.3524073 L23.6724546,19.4226519 C23.6724546,26.9388319 18.7553275,31.0481452 13.4167324,31.0481452 C10.0098658,31.0481452 7.93764799,29.5027624 6.39226519,27.6764009 L6.39226519,30.6266772 L0,30.6266772 L0,0 L6.39226519,0 Z M11.7659826,13.1357537 C8.78058406,13.1357537 6.32202052,15.5943173 6.32202052,19.3524073 L6.32202052,19.4226519 C6.32202052,23.1456196 8.81570639,25.6393054 11.7659826,25.6393054 C14.7513812,25.6393054 17.2801894,23.1807419 17.2801894,19.4226519 L17.2801894,19.3524073 C17.2801894,15.6294396 14.7513812,13.1357537 11.7659826,13.1357537 Z"></path>
17
+ </clipPath>
18
+ <clipPath id="i4">
19
+ <path d="M13.5923441,0.0105266556 L13.5923441,6.68377054 L13.2411208,6.68377054 C8.99131807,6.68377054 6.39226519,9.24770108 6.39226519,14.6214185 L6.39226519,22.9454122 L0,22.9454122 L0,0.43199469 L6.42738753,0.43199469 L6.42738753,4.96277606 C7.72691397,1.87201048 9.83425414,-0.165085026 13.5923441,0.0105266556 Z"></path>
20
+ </clipPath>
21
+ <clipPath id="i5">
22
+ <path d="M6.39226519,0 L6.39226519,12.538674 C6.39226519,15.5591949 7.83228098,17.1045777 10.2557222,17.1045777 C12.6791634,17.1045777 14.2596685,15.5591949 14.2596685,12.538674 L14.2596685,0 L20.6519337,0 L20.6519337,22.4782952 L14.2596685,22.4782952 L14.2596685,19.2821626 C12.7845304,21.1787687 10.8879242,22.8997632 7.6566693,22.8997632 C2.8097869,22.9348856 0,19.738753 0,14.5757695 L0,0 L6.39226519,0 Z"></path>
23
+ </clipPath>
24
+ <clipPath id="i6">
25
+ <path d="M26.7280979,0 C31.4696133,0 34.3145225,2.84490923 34.3145225,8.25374901 L34.3145225,22.8997632 L27.9222573,22.8997632 L27.9222573,10.3610892 C27.9222573,7.34056827 26.5876085,5.79518548 24.1992897,5.79518548 C21.8109708,5.79518548 20.3358327,7.34056827 20.3358327,10.3610892 L20.3358327,22.8997632 L13.9786898,22.8997632 L13.9786898,10.3610892 C13.9786898,7.34056827 12.644041,5.79518548 10.2557222,5.79518548 C7.86740331,5.79518548 6.39226519,7.34056827 6.39226519,10.3610892 L6.39226519,22.8997632 L0,22.8997632 L0,0.421468035 L6.39226519,0.421468035 L6.39226519,3.61760063 C7.86740331,1.72099448 9.79913181,0 13.0303867,0 C15.980663,0 18.1933702,1.29952644 19.3524073,3.5824783 C21.3192581,1.2644041 23.6724546,0 26.7280979,0 Z"></path>
26
+ </clipPath>
27
+ <clipPath id="i7">
28
+ <path d="M21.5651144,0 C23.6022099,5.02249408 24.7261247,10.5367009 24.7261247,16.296764 C24.7261247,22.3026835 23.4968429,28.0276243 21.2841358,33.2257301 L0,24.1992897 C1.01854775,21.8109708 1.61562747,19.1065509 1.61562747,16.296764 C1.61562747,13.5923441 1.08879242,11.0284136 0.140489345,8.67521705 L21.5651144,0 Z"></path>
29
+ </clipPath>
30
+ <radialGradient id="i8" cx="-17.7731983px" cy="18.397009px" r="44.0006791px" gradientUnits="userSpaceOnUse">
31
+ <stop stop-color="#6633FF" offset="31.74%"></stop>
32
+ <stop stop-color="#8F69FF" offset="44.48%"></stop>
33
+ <stop stop-color="#B79FFF" offset="58.28%"></stop>
34
+ <stop stop-color="#D6C8FF" offset="71.22%"></stop>
35
+ <stop stop-color="#ECE6FF" offset="82.88%"></stop>
36
+ <stop stop-color="#FAF9FF" offset="92.86%"></stop>
37
+ <stop stop-color="#FFFFFF" offset="100%"></stop>
38
+ </radialGradient>
39
+ <clipPath id="i9">
40
+ <path d="M43.4178161,0 C49.4237356,0 55.1486764,1.22928177 60.3467821,3.44198895 C65.5448879,5.6195738 70.1810363,8.78058406 74.1147379,12.7142857 C78.1889289,16.7884767 81.4553062,21.6704815 83.6680134,27.1144436 L62.2433883,35.7896606 C61.2248405,33.2257301 59.6794577,30.9427782 57.7828516,29.0461721 C55.9564901,27.1846882 53.7789052,25.7095501 51.355464,24.6910024 C48.8969005,23.6373323 46.227603,23.0753749 43.4178161,23.0753749 C40.7133962,23.0753749 38.1494656,23.6022099 35.7962691,24.550513 C33.2323386,25.5690608 30.9493867,27.1144436 29.0527805,29.0110497 C27.1912967,30.8374112 25.7161586,33.0149961 24.6976109,35.4384373 L24.6624885,35.4384373 C23.6790631,37.7916338 23.1171057,40.3555643 23.0819834,43.0248619 C23.046861,45.8346488 23.5736961,48.5039463 24.5571215,50.9625099 L24.5219992,51.0327545 L24.5571215,50.9976322 C25.5756693,53.5615627 27.1210521,55.8445146 29.0176582,57.7411208 C30.8440197,59.6026046 33.0216045,61.0777427 35.4450457,62.0962904 C37.9036093,63.1499605 40.5729068,63.7119179 43.3826937,63.7119179 C46.0871136,63.7119179 48.6510442,63.1850829 51.0042407,62.2367798 C53.5681712,61.218232 55.8511231,59.6728493 57.7477292,57.7762431 C59.5740907,55.9498816 61.0843512,53.7722968 62.1028989,51.2786109 L83.3870347,60.3050513 C81.2094498,65.5031571 78.0484396,70.1393054 74.1147379,74.0730071 C70.0405469,78.1471981 65.1585422,81.4135754 59.7145801,83.6262826 C54.692086,85.6633781 49.1778792,86.7872928 43.4178161,86.7872928 C37.4118966,86.7872928 31.6869558,85.558011 26.48885,83.3453039 C21.2907442,81.167719 16.6545959,78.0067088 12.7208942,74.0730071 C8.6467032,69.9988161 5.38032593,65.1168114 3.16761875,59.6728493 L2.92869054,59.0666932 C0.972992292,53.9961757 -0.0947059468,48.4485611 0.00660848467,42.6736385 C0.0768531571,36.9135754 1.30613493,31.4344909 3.4134751,26.4471192 L3.44859743,26.4471192 C5.62618228,21.2841358 8.82231488,16.612865 12.7208942,12.7142857 C16.7950852,8.64009471 21.6770899,5.37371744 27.1210521,3.16101026 C32.1435461,1.12391476 37.6577529,0 43.4178161,0 Z"></path>
41
+ </clipPath>
42
+ <linearGradient id="i10" x1="-0.0457237963px" y1="43.4241326px" x2="83.6590221px" y2="43.4241326px" gradientUnits="userSpaceOnUse">
43
+ <stop stop-color="#6944FF" offset="0%"></stop>
44
+ <stop stop-color="#2592FB" offset="100%"></stop>
45
+ </linearGradient>
46
+ <clipPath id="i11">
47
+ <path d="M20.3358327,0 C23.1456196,0 25.8500395,0.597079716 28.2734807,1.61562747 C30.6969219,2.66929755 32.8745067,4.14443567 34.7008682,5.97079716 C36.035517,7.30544594 37.1945541,8.81570639 38.1077348,10.4664562 C38.4940805,11.2040253 38.8453039,11.9415943 39.1614049,12.7142857 C39.3370166,13.1708761 39.5126283,13.5923441 39.6531176,14.0489345 L40.425809,17.3504341 L43.4463299,30.0998421 L38.2482242,30.0998421 L38.2482242,40.109708 C38.2482242,41.8658248 36.773086,43.2355959 35.0520916,43.1302289 L13.2059984,41.8658248 L12.32794,38.9857932 C9.93962115,37.9672455 7.79715864,36.4921073 6.00591949,34.7008682 C4.10931334,32.804262 2.56393054,30.4861878 1.54538279,27.9573796 C0.526835043,25.4988161 0,22.7943962 0,19.949487 C0.0351223362,17.2450671 0.597079716,14.6811365 1.58050513,12.3630624 L1.61562747,12.3630624 C2.66929755,9.93962115 4.14443567,7.76203631 5.97079716,5.93567482 C7.86740331,4.03906867 10.1854775,2.49368587 12.7142857,1.47513812 C15.0674822,0.526835043 17.6314128,0 20.3358327,0 Z"></path>
48
+ </clipPath>
49
+ <clipPath id="i12">
50
+ <path d="M17.69,6.999 L0.14161703,5.98392627 C0.14161703,5.98392627 0.0713723576,5.24635721 0.0362500213,4.29805413 C0.0261185782,4.07853953 0.017286038,3.84701007 0.0108764514,3.60986661 L0.00441990454,3.32300939 L0.000721028553,3.03322064 L0.000211458928,2.74295829 C0.00860936619,0.763479376 0.270624073,-1.07701417 1.40602113,0.750698171 C2.2489572,2.12046928 4.35629738,3.27950638 6.77973858,4.19268712 C11.3522691,5.94880393 17.0501149,6.92345189 17.6805037,6.99867343 L17.69,6.999 Z"></path>
51
+ </clipPath>
52
+ <clipPath id="i13">
53
+ <path d="M2.49370861,0 C3.87094585,0 4.98741723,1.11647138 4.98741723,2.49370861 C4.98741723,3.87094585 3.87094585,4.98741723 2.49370861,4.98741723 C1.11647138,4.98741723 0,3.87094585 0,2.49370861 C0,1.11647138 1.11647138,0 2.49370861,0 Z"></path>
54
+ </clipPath>
55
+ <clipPath id="i14">
56
+ <path d="M8.08040285,0.994970023 C13.9107107,-1.39334884 20.3029759,0.678868997 23.7449648,5.49062906 C24.0961882,5.94721943 24.2717999,6.47405447 24.3771669,7.00088952 C24.6230232,8.51114998 23.9556988,10.0916551 22.515683,10.9697135 C21.5322576,11.5316709 20.3029759,12.1287506 18.8980824,12.5502186 C17.2122103,13.0770537 15.2453594,13.3580324 13.0677746,13.1472984 C9.76627499,12.8311973 8.08040285,15.5356172 6.81599875,18.2751594 C6.71063174,18.5210158 6.60526473,18.7668722 6.49989772,19.0127285 C5.58671698,21.049824 2.77693008,21.260558 1.61789298,19.4341965 C1.58277065,19.3639519 1.54764831,19.2937072 1.51252598,19.2234625 C1.05593561,18.3454041 0.704712243,17.397101 0.423733553,16.4136756 C-1.22701625,10.1267774 2.07448336,3.45353356 8.08040285,0.994970023 Z"></path>
57
+ </clipPath>
58
+ <radialGradient id="i15" cx="18.5690212px" cy="2.33155053px" r="24.3530234px" gradientUnits="userSpaceOnUse">
59
+ <stop stop-color="#E97655" offset="0%"></stop>
60
+ <stop stop-color="#FF26E2" offset="26.22%"></stop>
61
+ <stop stop-color="#AA31FF" offset="68.3%"></stop>
62
+ <stop stop-color="#3BC8F5" offset="95.76%"></stop>
63
+ </radialGradient>
64
+ <clipPath id="i16">
65
+ <path d="M5.2332281,0 C6.63812155,0 7.69179163,0.351223362 8.46448303,1.01854775 C9.23717443,1.68587214 9.62352013,2.63417522 9.62352013,3.89857932 C9.62352013,5.12786109 9.23717443,6.1112865 8.46448303,6.77861089 C7.69179163,7.48105762 6.60299921,7.79715864 5.2332281,7.79715864 L1.54538279,7.79715864 L1.54538279,13.2059984 L0,13.2059984 L0,0 L5.2332281,0 Z M5.09273875,1.29952644 L1.54538279,1.29952644 L1.54538279,6.53275454 L5.09273875,6.53275454 C7.09471192,6.53275454 8.11325967,5.65469613 8.11325967,3.89857932 C8.11325967,2.17758485 7.09471192,1.29952644 5.09273875,1.29952644 Z"></path>
66
+ </clipPath>
67
+ <clipPath id="i17">
68
+ <path d="M4.32004736,0 C5.19810576,0 5.93567482,0.210734017 6.60299921,0.597079716 C7.2703236,0.983425414 7.76203631,1.54538279 8.148382,2.24782952 C8.49960537,2.95027624 8.67521705,3.79321231 8.67521705,4.74151539 C8.67521705,5.68981847 8.49960537,6.53275454 8.148382,7.23520126 C7.79715864,7.93764799 7.2703236,8.49960537 6.60299921,8.88595107 C5.93567482,9.27229676 5.16298343,9.48303078 4.32004736,9.48303078 C3.44198895,9.48303078 2.66929755,9.27229676 2.0370955,8.88595107 C1.36977111,8.49960537 0.878058406,7.93764799 0.526835043,7.23520126 C0.175611681,6.53275454 0,5.68981847 0,4.74151539 C0,3.79321231 0.175611681,2.95027624 0.526835043,2.24782952 C0.878058406,1.54538279 1.40489345,0.983425414 2.0370955,0.597079716 C2.70441989,0.210734017 3.44198895,0 4.32004736,0 Z M4.32004736,1.22928177 C3.40686661,1.22928177 2.73954223,1.54538279 2.24782952,2.14246251 C1.75611681,2.73954223 1.51026046,3.61760063 1.51026046,4.70639305 C1.51026046,5.83030781 1.75611681,6.70836622 2.24782952,7.30544594 C2.73954223,7.90252565 3.40686661,8.21862668 4.32004736,8.21862668 C5.2332281,8.21862668 5.90055249,7.90252565 6.39226519,7.30544594 C6.8839779,6.70836622 7.12983425,5.83030781 7.12983425,4.70639305 C7.12983425,3.5824783 6.8839779,2.73954223 6.39226519,2.14246251 C5.90055249,1.54538279 5.2332281,1.22928177 4.32004736,1.22928177 Z"></path>
69
+ </clipPath>
70
+ <clipPath id="i18">
71
+ <path d="M1.58050513,0 L4.17955801,7.34056827 L6.81373323,0 L8.11325967,0 L10.7123125,7.34056827 L13.3816101,0 L14.8918706,0 L11.4850039,9.13180742 L10.0098658,9.13180742 L7.44593528,2.07221784 L4.8468824,9.13180742 L3.40686661,9.13180742 L0,0 L1.58050513,0 Z"></path>
72
+ </clipPath>
73
+ <clipPath id="i19">
74
+ <path d="M4.32004736,0 C5.54932912,0 6.4976322,0.386345699 7.16495659,1.19415943 C7.86740331,2.00197316 8.18350434,3.09076559 8.18350434,4.4605367 L8.18350434,5.09273875 L1.51026046,5.09273875 C1.54538279,6.14640884 1.86148382,6.95422257 2.38831886,7.51617995 C2.91515391,8.043015 3.65272297,8.32399369 4.63614838,8.32399369 C5.68981847,8.32399369 6.67324388,7.97277032 7.55130229,7.23520126 L8.043015,8.32399369 C7.6566693,8.71033938 7.12983425,8.99131807 6.4976322,9.20205209 C5.86543015,9.41278611 5.2332281,9.51815312 4.60102605,9.51815312 C3.16101026,9.51815312 2.0370955,9.09668508 1.22928177,8.25374901 C0.421468035,7.41081294 0,6.25177585 0,4.77663773 C0,3.82833465 0.175611681,3.02052092 0.526835043,2.28295185 C0.878058406,1.58050513 1.40489345,1.01854775 2.07221784,0.597079716 C2.73954223,0.210734017 3.47711129,0 4.32004736,0 Z M4.32004736,1.19415943 C3.51223362,1.22928177 2.88003157,1.47513812 2.38831886,1.96685083 C1.89660616,2.45856354 1.61562747,3.16101026 1.54538279,4.074191 L6.84885556,4.074191 C6.81373323,3.16101026 6.60299921,2.4234412 6.14640884,1.93172849 C5.72494081,1.44001579 5.09273875,1.19415943 4.32004736,1.19415943 Z"></path>
75
+ </clipPath>
76
+ <clipPath id="i20">
77
+ <path d="M5.02249408,0 L5.16298343,1.33464878 L4.17955801,1.44001579 C3.23125493,1.51026046 2.56393054,1.82636148 2.14246251,2.31807419 C1.72099448,2.8097869 1.51026046,3.44198895 1.51026046,4.17955801 L1.51026046,9.37766377 L0,9.37766377 L0,0.245856354 L1.47513812,0.245856354 L1.47513812,1.82636148 C2.00197316,0.772691397 2.98539858,0.175611681 4.49565904,0.0351223362 L5.02249408,0 Z"></path>
78
+ </clipPath>
79
+ <clipPath id="i21">
80
+ <path d="M4.32004736,0 C5.54932912,0 6.4976322,0.386345699 7.16495659,1.19415943 C7.86740331,2.00197316 8.18350434,3.09076559 8.18350434,4.4605367 L8.18350434,5.09273875 L1.51026046,5.09273875 C1.54538279,6.14640884 1.86148382,6.95422257 2.38831886,7.51617995 C2.91515391,8.043015 3.65272297,8.32399369 4.63614838,8.32399369 C5.68981847,8.32399369 6.67324388,7.97277032 7.55130229,7.23520126 L8.043015,8.32399369 C7.6566693,8.71033938 7.12983425,8.99131807 6.4976322,9.20205209 C5.86543015,9.41278611 5.2332281,9.51815312 4.60102605,9.51815312 C3.16101026,9.51815312 2.0370955,9.09668508 1.22928177,8.25374901 C0.421468035,7.41081294 0,6.25177585 0,4.77663773 C0,3.82833465 0.175611681,3.02052092 0.526835043,2.28295185 C0.878058406,1.58050513 1.40489345,1.01854775 2.07221784,0.597079716 C2.73954223,0.210734017 3.47711129,0 4.32004736,0 Z M4.35516969,1.19415943 C3.54735596,1.22928177 2.88003157,1.47513812 2.4234412,1.96685083 C1.93172849,2.45856354 1.6507498,3.16101026 1.58050513,4.074191 L6.8839779,4.074191 C6.84885556,3.16101026 6.63812155,2.4234412 6.18153118,1.93172849 C5.76006314,1.44001579 5.12786109,1.19415943 4.35516969,1.19415943 Z"></path>
81
+ </clipPath>
82
+ <clipPath id="i22">
83
+ <path d="M8.60497238,0 L8.60497238,13.2059984 L7.09471192,13.2059984 L7.09471192,11.5552486 C6.81373323,12.117206 6.42738753,12.5737964 5.90055249,12.8898974 C5.37371744,13.2059984 4.74151539,13.3464878 4.03906867,13.3464878 C3.23125493,13.3464878 2.52880821,13.1357537 1.93172849,12.7494081 C1.33464878,12.3630624 0.842936069,11.801105 0.491712707,11.0635359 C0.140489345,10.3610892 0,9.51815312 0,8.56985004 C0,7.62154696 0.175611681,6.81373323 0.491712707,6.1112865 C0.842936069,5.40883978 1.29952644,4.8468824 1.93172849,4.4605367 C2.52880821,4.074191 3.26637727,3.86345699 4.03906867,3.86345699 C4.74151539,3.86345699 5.37371744,4.00394633 5.90055249,4.32004736 C6.42738753,4.63614838 6.84885556,5.09273875 7.09471192,5.65469613 L7.09471192,0 L8.60497238,0 Z M4.32004736,5.16298343 C3.44198895,5.16298343 2.73954223,5.47908445 2.24782952,6.07616417 C1.75611681,6.67324388 1.51026046,7.51617995 1.51026046,8.60497238 C1.51026046,9.72888713 1.75611681,10.5718232 2.24782952,11.2040253 C2.73954223,11.8362273 3.44198895,12.117206 4.32004736,12.117206 C5.19810576,12.117206 5.86543015,11.801105 6.35714286,11.2040253 C6.84885556,10.6069455 7.09471192,9.72888713 7.09471192,8.64009471 C7.09471192,7.51617995 6.84885556,6.67324388 6.35714286,6.07616417 C5.86543015,5.47908445 5.19810576,5.16298343 4.32004736,5.16298343 Z"></path>
84
+ </clipPath>
85
+ <clipPath id="i23">
86
+ <path d="M1.51026046,0 L1.51026046,5.65469613 C1.79123915,5.09273875 2.17758485,4.63614838 2.70441989,4.32004736 C3.23125493,4.00394633 3.86345699,3.86345699 4.56590371,3.86345699 C5.33859511,3.82833465 6.04104183,4.03906867 6.67324388,4.42541436 C7.2703236,4.81176006 7.76203631,5.37371744 8.11325967,6.07616417 C8.46448303,6.77861089 8.60497238,7.62154696 8.60497238,8.5347277 C8.60497238,9.48303078 8.42936069,10.3259669 8.11325967,11.0284136 C7.76203631,11.7308603 7.30544594,12.2928177 6.67324388,12.7142857 C6.07616417,13.1006314 5.33859511,13.3113654 4.56590371,13.3113654 C3.86345699,13.3113654 3.23125493,13.1708761 2.70441989,12.8547751 C2.17758485,12.538674 1.75611681,12.0820837 1.51026046,11.5201263 L1.51026046,13.2059984 L0,13.2059984 L0,0 L1.51026046,0 Z M4.21468035,5.09273875 C3.30149961,5.09273875 2.63417522,5.40883978 2.17758485,6.00591949 C1.68587214,6.60299921 1.44001579,7.44593528 1.44001579,8.56985004 C1.44001579,9.6937648 1.68587214,10.5367009 2.17758485,11.1337806 C2.66929755,11.7308603 3.33662194,12.0469613 4.21468035,12.0469613 C5.09273875,12.0469613 5.83030781,11.7659826 6.28689818,11.1337806 C6.77861089,10.5015785 7.02446725,9.65864246 7.02446725,8.5347277 C7.02446725,7.44593528 6.77861089,6.60299921 6.28689818,6.00591949 C5.79518548,5.40883978 5.12786109,5.09273875 4.21468035,5.09273875 Z"></path>
87
+ </clipPath>
88
+ <clipPath id="i24">
89
+ <path d="M9.27229676,0 L3.86345699,12.5035517 L2.31807419,12.5035517 L3.86345699,9.02644041 L0,0.0351223362 L1.61562747,0.0351223362 L4.67127072,7.48105762 L7.76203631,0 L9.27229676,0 Z"></path>
90
+ </clipPath>
91
+ </defs>
92
+ <g transform="translate(0.0 2.212707182320422)">
93
+ <g transform="translate(99.12184131813683 27.746645619573805)">
94
+ <g transform="translate(0.0 0.7375690607734846)">
95
+ <g clip-path="url(#i0)">
96
+ <polygon points="0,0 27.1144436,0 27.1144436,30.3459734 0,30.3459734 0,0" stroke="none" fill="#ffffff"></polygon>
97
+ </g>
98
+ </g>
99
+ <g transform="translate(30.064719810575298 7.726913970007889)">
100
+ <g clip-path="url(#i1)">
101
+ <polygon points="0,1.77635684e-15 22.0919495,1.77635684e-15 22.0919495,23.3914759 0,23.3914759 0,1.77635684e-15" stroke="none" fill="#ffffff"></polygon>
102
+ </g>
103
+ </g>
104
+ <g transform="translate(56.863062352013 7.681264978172578)">
105
+ <g clip-path="url(#i2)">
106
+ <polygon points="0,-8.8817842e-16 13.5923441,-8.8817842e-16 13.5923441,22.9454122 0,22.9454122 0,-8.8817842e-16" stroke="none" fill="#ffffff"></polygon>
107
+ </g>
108
+ </g>
109
+ <g transform="translate(72.70323599052881 7.726913970007889)">
110
+ <g clip-path="url(#i1)">
111
+ <polygon points="0,1.77635684e-15 22.0919495,1.77635684e-15 22.0919495,23.3914759 0,23.3914759 0,1.77635684e-15" stroke="none" fill="#ffffff"></polygon>
112
+ </g>
113
+ </g>
114
+ <g transform="translate(99.5015785319656 0.0)">
115
+ <g clip-path="url(#i3)">
116
+ <polygon points="0,0 23.6724546,0 23.6724546,31.0481452 0,31.0481452 0,0" stroke="none" fill="#ffffff"></polygon>
117
+ </g>
118
+ </g>
119
+ <g transform="translate(127.81018153117661 7.681264978172578)">
120
+ <g clip-path="url(#i4)">
121
+ <polygon points="0,-8.8817842e-16 13.5923441,-8.8817842e-16 13.5923441,22.9454122 0,22.9454122 0,-8.8817842e-16" stroke="none" fill="#ffffff"></polygon>
122
+ </g>
123
+ </g>
124
+ <g transform="translate(145.230860299921 8.113259668508306)">
125
+ <g clip-path="url(#i5)">
126
+ <polygon points="0,0 20.6519337,0 20.6519337,22.9000497 0,22.9000497 0,0" stroke="none" fill="#ffffff"></polygon>
127
+ </g>
128
+ </g>
129
+ <g transform="translate(171.5374901341756 7.691791633780598)">
130
+ <g clip-path="url(#i6)">
131
+ <polygon points="0,0 34.3145225,0 34.3145225,22.8997632 0,22.8997632 0,0" stroke="none" fill="#ffffff"></polygon>
132
+ </g>
133
+ </g>
134
+ </g>
135
+ <g transform="translate(62.102898934554105 27.114443567482283)">
136
+ <g clip-path="url(#i7)">
137
+ <polygon points="0,0 24.7261247,0 24.7261247,33.2257301 0,33.2257301 0,0" stroke="none" fill="url(#i8)"></polygon>
138
+ </g>
139
+ </g>
140
+ <g clip-path="url(#i9)">
141
+ <polygon points="-9.79424875e-15,0 83.6680134,0 83.6680134,86.7872928 -9.79424875e-15,86.7872928 -9.79424875e-15,0" stroke="none" fill="url(#i10)"></polygon>
142
+ </g>
143
+ <g transform="translate(23.046861049787367 23.25098658247831)">
144
+ <g clip-path="url(#i11)">
145
+ <polygon points="0,0 43.4463299,0 43.4463299,43.1359242 0,43.1359242 0,0" stroke="none" fill="#FFFFFF"></polygon>
146
+ </g>
147
+ <g transform="translate(6.460244347461412 5.7485185326409045)">
148
+ <g transform="translate(11.415897107141062 30.379236335155156)">
149
+ <g clip-path="url(#i12)">
150
+ <polygon points="1.99840144e-15,-5.44009282e-15 17.7379075,-5.44009282e-15 17.7379075,7.00338308 1.99840144e-15,7.00338308 1.99840144e-15,-5.44009282e-15" stroke="none" fill="#D1D3D3"></polygon>
151
+ </g>
152
+ </g>
153
+ <g transform="translate(20.25341824134574 14.357157473675366) rotate(-16.66631689028721)">
154
+ <g clip-path="url(#i13)">
155
+ <polygon points="0,0 4.98741723,0 4.98741723,4.98741723 0,4.98741723 0,0" stroke="none" fill="#000000"></polygon>
156
+ </g>
157
+ </g>
158
+ <g clip-path="url(#i14)">
159
+ <polygon points="-3.55271368e-15,0 24.4271591,0 24.4271591,20.6819431 -3.55271368e-15,20.6819431 -3.55271368e-15,0" stroke="none" fill="url(#i15)"></polygon>
160
+ </g>
161
+ </g>
162
+ </g>
163
+ </g>
164
+ <g transform="translate(101.82626120763962 -7.105427357601002e-15)">
165
+ <g clip-path="url(#i16)">
166
+ <polygon points="0,0 9.62352013,0 9.62352013,13.2059984 0,13.2059984 0,0" stroke="none" fill="#cbc7da"></polygon>
167
+ </g>
168
+ <g transform="translate(11.06353591160224 3.8985793212312387)">
169
+ <g clip-path="url(#i17)">
170
+ <polygon points="0,0 8.67521705,0 8.67521705,9.48303078 0,9.48303078 0,0" stroke="none" fill="#cbc7da"></polygon>
171
+ </g>
172
+ </g>
173
+ <g transform="translate(20.897790055248514 4.109313338595107)">
174
+ <g clip-path="url(#i18)">
175
+ <polygon points="0,0 14.8918706,0 14.8918706,9.13180742 0,9.13180742 0,0" stroke="none" fill="#cbc7da"></polygon>
176
+ </g>
177
+ </g>
178
+ <g transform="translate(37.01894238358273 3.82833464877661)">
179
+ <g clip-path="url(#i19)">
180
+ <polygon points="0,0 8.18350434,0 8.18350434,9.51815312 0,9.51815312 0,0" stroke="none" fill="#cbc7da"></polygon>
181
+ </g>
182
+ </g>
183
+ <g transform="translate(47.450276243094024 3.863456985003932)">
184
+ <g clip-path="url(#i20)">
185
+ <polygon points="0,0 5.16298343,0 5.16298343,9.37766377 0,9.37766377 0,0" stroke="none" fill="#cbc7da"></polygon>
186
+ </g>
187
+ </g>
188
+ <g transform="translate(53.31570639305482 3.82833464877661)">
189
+ <g clip-path="url(#i21)">
190
+ <polygon points="0,0 8.18350434,0 8.18350434,9.51815312 0,9.51815312 0,0" stroke="none" fill="#cbc7da"></polygon>
191
+ </g>
192
+ </g>
193
+ <g transform="translate(63.2553275453829 0.0)">
194
+ <g clip-path="url(#i22)">
195
+ <polygon points="0,0 8.60497238,0 8.60497238,13.3464878 0,13.3464878 0,0" stroke="none" fill="#cbc7da"></polygon>
196
+ </g>
197
+ </g>
198
+ <g transform="translate(79.55209155485403 0.03512233622727301)">
199
+ <g clip-path="url(#i23)">
200
+ <polygon points="0,0 8.60497238,0 8.60497238,13.3113654 0,13.3113654 0,0" stroke="none" fill="#cbc7da"></polygon>
201
+ </g>
202
+ </g>
203
+ <g transform="translate(89.070244672455 4.109313338595107)">
204
+ <g clip-path="url(#i24)">
205
+ <polygon points="0,0 9.27229676,0 9.27229676,12.5035517 0,12.5035517 0,0" stroke="none" fill="#cbc7da"></polygon>
206
+ </g>
207
+ </g>
208
+ </g>
209
+ </svg>
Binary file
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ preset: "ts-jest",
3
+ testEnvironment: "node",
4
+ verbose: true,
5
+ modulePaths: ["node_modules", "<rootDir>"],
6
+ testPathIgnorePatterns: ["<rootDir>/dist/", "<rootDir>/node_modules/"],
7
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@cerebruminc/yates",
3
+ "version": "1.0.0",
4
+ "description": "Role based access control for Prisma Apps",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "generate": "prisma generate",
8
+ "build": "tsc",
9
+ "test": "rome ci src",
10
+ "test:integration": "jest test/integration",
11
+ "test:compose:integration": "docker-compose --profile with-sut up db sut --exit-code-from sut",
12
+ "setup": "prisma generate && prisma migrate dev",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "author": "Cerebrum <hello@cerebrum.com> (https://cerebrum.com)",
16
+ "license": "MIT",
17
+ "devDependencies": {
18
+ "@prisma/client": "^4.0.0",
19
+ "@types/jest": "^29.2.6",
20
+ "@types/lodash": "^4.14.191",
21
+ "jest": "^29.3.1",
22
+ "prisma": "^4.9.0",
23
+ "rome": "^11.0.0",
24
+ "ts-jest": "^29.0.5",
25
+ "typescript": "^4.9.4"
26
+ },
27
+ "dependencies": {
28
+ "lodash": "^4.17.21"
29
+ },
30
+ "peerDependencies": {
31
+ "@prisma/client": "^4.0.0",
32
+ "prisma": "^4.9.0"
33
+ }
34
+ }
@@ -0,0 +1,31 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "User" (
6
+ "id" SERIAL NOT NULL,
7
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
+ "email" TEXT NOT NULL,
9
+ "name" TEXT,
10
+ "role" "Role" NOT NULL DEFAULT 'USER',
11
+
12
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
13
+ );
14
+
15
+ -- CreateTable
16
+ CREATE TABLE "Post" (
17
+ "id" SERIAL NOT NULL,
18
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ "updatedAt" TIMESTAMP(3) NOT NULL,
20
+ "published" BOOLEAN NOT NULL DEFAULT false,
21
+ "title" VARCHAR(255) NOT NULL,
22
+ "authorId" INTEGER,
23
+
24
+ CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
25
+ );
26
+
27
+ -- CreateIndex
28
+ CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
29
+
30
+ -- AddForeignKey
31
+ ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (i.e. Git)
3
+ provider = "postgresql"
@@ -0,0 +1,34 @@
1
+ // This is a sample Prisma schema file used for development and testing
2
+ // It it is taken from https://www.prisma.io/docs/concepts/components/prisma-schema and is not intended to be used in production
3
+ datasource db {
4
+ provider = "postgresql"
5
+ url = env("DATABASE_URL")
6
+ }
7
+
8
+ generator client {
9
+ provider = "prisma-client-js"
10
+ }
11
+
12
+ model User {
13
+ id Int @id @default(autoincrement())
14
+ createdAt DateTime @default(now())
15
+ email String @unique
16
+ name String?
17
+ role Role @default(USER)
18
+ posts Post[]
19
+ }
20
+
21
+ model Post {
22
+ id Int @id @default(autoincrement())
23
+ createdAt DateTime @default(now())
24
+ updatedAt DateTime @updatedAt
25
+ published Boolean @default(false)
26
+ title String @db.VarChar(255)
27
+ author User? @relation(fields: [authorId], references: [id])
28
+ authorId Int?
29
+ }
30
+
31
+ enum Role {
32
+ USER
33
+ ADMIN
34
+ }
package/rome.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "formatter": {
3
+ "enabled": true,
4
+ "indentStyle": "tab",
5
+ "lineWidth": 120
6
+ },
7
+ "linter": {
8
+ "enabled": true,
9
+ "rules": {
10
+ "suspicious": {
11
+ "noExplicitAny": "off"
12
+ }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,29 @@
1
+ import { PrismaClient } from "@prisma/client";
2
+ import { setup } from "../../src";
3
+
4
+ describe("setup", () => {
5
+ describe("params.getRoles()", () => {
6
+ it("should provide a set of built-in abilities for CRUD operations", async () => {
7
+ const prisma = new PrismaClient();
8
+
9
+ const getRoles = jest.fn((_abilities) => {
10
+ return {
11
+ USER: "*",
12
+ } as any;
13
+ });
14
+
15
+ await setup({
16
+ prisma,
17
+ getRoles,
18
+ getContext: () => null,
19
+ });
20
+
21
+ expect(getRoles.mock.calls).toHaveLength(1);
22
+ const abilities = getRoles.mock.calls[0][0];
23
+
24
+ expect(Object.keys(abilities)).toStrictEqual(["User", "Post"]);
25
+ expect(Object.keys(abilities.User)).toStrictEqual(["create", "read", "update", "delete"]);
26
+ expect(Object.keys(abilities.Post)).toStrictEqual(["create", "read", "update", "delete"]);
27
+ });
28
+ });
29
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "lib": ["es2020"],
5
+ "module": "CommonJS",
6
+ "outDir": "./dist",
7
+ "sourceMap": true,
8
+ "strict": true,
9
+ "useUnknownInCatchVariables": false,
10
+ "declaration": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }