@sladkoff/kysely-access-control 0.0.6

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.
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ This package contains some utilities for implementing a permission system on top of the
2
+ [Kysely](https://github.com/koskimas/kysely) query builder.
3
+
4
+ It exposes two interfaces, a low-level interface accessible via `createAccessControlPlugin` and
5
+ and a higher level interface that is similar to Postgres's internal permissions
6
+ accessible via `createKyselyGrantGuard`.
7
+
8
+ It uses `bun` for package installation, monorepo management, building, and running scripts and tests,
9
+ but exports packages in a way that is compatible with Node or Deno normally.
10
+
11
+ # Motivation
12
+
13
+ Implementing permissions at the query builder layer makes more sense than in *each query*:
14
+ 1. **DRY-er**: Common use cases like filtering a table or omitting a column are just specified once, instead of in every query in your application.
15
+ 2. **Separation of concerns**: Maintain a part of your application responsible for generating different guards for different users and ensure that your core application logic is not polluted with permission checks, and doesn't need to change when permissions or new roles are created.
16
+ 3. **Harder to forget**: No more odd bugs where you forget to add a check for `.is_deleted` or `.tenant_id = ?`
17
+
18
+ Even though PostgreSQL has a fully featured permission system, implementing permissions at the query builder layer
19
+ can makes more sense than in *the database* itself:
20
+ 1. **Dynamically generate context specific permissions**: Postgres permissions are static, and so you can't, for example, generate permissions based on the current context / user role / action matrix. Although you can use a role per user approach, that role controls those users permissions in any context.
21
+ 3. **No security definer escape**: When using database level permissions, it's common to use security definer functions as an escape hatch. When you do, you're back to manually re-implementing parts of the permissions you want to keep.
22
+ 3. **More control**: Postgres, for example, has no deny rules, and so it can be easy to accidentally grant permissions that leak when additive roles combine.
23
+
24
+
25
+ # High Level Grants Usage
26
+
27
+ Construct a `Grant` with the following type, like:
28
+ ```typescript
29
+ type Grant = {
30
+ on: Table;
31
+ for: 'select' | 'insert' | 'update' | 'delete' | 'all'
32
+ columns?: string[] // all columns are allowed if blank
33
+ where?: (
34
+ eb: ExpressionBuilder<KyselyDatabase, TableName>
35
+ ) => ExpressionWrapper<KyselyDatabase, TableName, SqlBool>;
36
+ whereType?: "permissive" | "restrictive";
37
+ }
38
+ ```
39
+
40
+ `Grant.where` and `Grant.whereType` function similar to Postgres [row level security](https://www.postgresql.org/docs/current/sql-createpolicy.html).
41
+
42
+ You can check a list of grants into your codebase, like:
43
+ ```typescript
44
+ // in some file db.ts
45
+ import { createKyselyGrantGuard, createAccessControlPlugin } from 'kysely-access-control'
46
+
47
+ const getSharedGrants = (currentUserId) => [
48
+ {
49
+ on: 'posts',
50
+ for: 'select'
51
+ },
52
+ {
53
+ on: 'comments',
54
+ for: 'select'
55
+ },
56
+ {
57
+ on: 'posts',
58
+ for: 'all',
59
+ where: (eb) => eb.eq('author_id', currentUserId)
60
+ },
61
+ {
62
+ on: 'comments',
63
+ for: 'all',
64
+ where: (eb) => eb.eq('author_id', currentUserId)
65
+ }
66
+ ]
67
+
68
+ const adminGrants = [
69
+ {
70
+ on: 'accounts',
71
+ for: 'all',
72
+ }
73
+ ]
74
+
75
+ const query = (userId, isAdmin) => {
76
+ return db.withPlugin(createAccessControlPlugin(
77
+ createKyselyGrantGuard(
78
+ getSharedGrants(userId).concat(isAdmin ? adminGrants : [])
79
+ )
80
+ )
81
+ }
82
+
83
+ // in some api.ts
84
+ import { query } from './db.ts'
85
+
86
+ // in some request handler
87
+ // this query will have permissions enforced
88
+ await query(req.user.id, req.user.isAdmin).selectFrom('posts').select(['id']).execute();
89
+ ```
90
+
91
+ Or you can generate them from a database, storing them in some `grants` table, or
92
+ anything else you can think of.
93
+
94
+ In my projects, I'm constructing the plugin in response to each request. In one, I'm doing it in a [tRPC middleware](https://trpc.io/docs/server/middlewares) and adding it to the RPC's context.
95
+
96
+
97
+ ### Only Table/Column Grants
98
+
99
+ Currently only table x column permissions are implemented, i.e. all grants look like:
100
+ ```sql
101
+ grant select (id, first_name, last_name) on person to a;
102
+ ```
103
+
104
+ There is no intent to implement schema level ownership or other higher level permissions.
105
+ If you want a user to be able to access everything, just skip the `.withPlugin()` call.
106
+
107
+
108
+ # Lower Level Access Control Usage
109
+
110
+ ```typescript
111
+ import { createAccessControlPlugin, KyselyAccessControlGuard, Allow, Deny, Update, Delete, ColumnInUpdateSet } from 'kysely-access-control';
112
+ import { Database } from './my-kysely-types.ts'
113
+
114
+ // Define your guard
115
+ const guard: KyselyAccessControlGuard<Database> = {
116
+ table: (table, statementType, usageContext) => {
117
+ // table.name is restricted to keyof Database
118
+ if (table.name === 'events' && statementType === Delete) {
119
+ return Deny;
120
+ }
121
+
122
+ return Allow;
123
+ },
124
+ column: (table, column, statementType, usageContext) => {
125
+ // Control if the column can be inserted, updated independently
126
+ if (table.name === 'events' && column.name === 'is_deleted' && statementType === Update && usageContext === ColumnInUpdateSet) {
127
+ return Deny;
128
+ }
129
+
130
+ return Allow;
131
+ }
132
+ }
133
+
134
+ // When executing a query...
135
+ const events = await db
136
+ .withPlugin(createAccessControlPlugin(guard))
137
+ .updateTable('events)
138
+ .set({ is_deleted: false })
139
+ .execute();
140
+ // throws 'UPDATE denied on events.is_deleted'
141
+ ```
142
+
143
+ # Limitations
144
+
145
+ ## No Enforcement of Raw SQL
146
+
147
+ `kysely-access-control` works by operating on the internal `OperationNode`s used in Kysely's query builder. As a result, anything [specified in raw SQL](https://kysely-org.github.io/kysely-apidoc/interfaces/Sql.html) can't be enforced.
148
+
149
+ There are definitely legitimate uses that require raw SQL, but try to use it only when necessary in order to maintain most of
150
+ the benefits of `kysely-access-control`.
151
+
152
+ For example,
153
+ ```typescript
154
+ db.selectFrom('person')
155
+ .select(({ fn, val, ref }) => [ fn<string>('concat', [ref('first_name'), val(' '), ref('last_name')]) ])
156
+ ```
157
+
158
+ Enforces column permissions, whereas:
159
+ ```typescript
160
+ db.selectFrom('person')
161
+ .select(sql<string>`concat(first_name, ' ', last_name)`)
162
+ ```
163
+
164
+ enforces only table permissions, and:
165
+ ```typescript
166
+ sql`select concat(first_name, ' ', last_name) from person`
167
+ ```
168
+
169
+ enforces nothing.
170
+
171
+ ## No RLS on Insert (or Check for Update out of RLS)
172
+
173
+ `kysely-access-control`'s RLS works by adding user supplied expression's as where clauses in the right places. As a result, it is only capable of implementing
174
+ the `USING` part of traditional RLS, and not the `WITH CHECK` part.
175
+
176
+ As a result, we can't check that a new row version (whether inserted or updated) matches the conditions specified.
177
+
178
+ ## Types May Be Incorrect
179
+
180
+ If you use `kysely-access-control` to restrict access to a column, the query return types may still portray
181
+ that column as being present (and potentially even not null), even though it will be undefined in the actual result.
182
+
183
+ ## Joins May Fail Where You Don't Expect
184
+
185
+ Even if a foreign key is not null, if you join to a table with a `where` guard on it, the join may fail
186
+ because the context does not permit the user to see the joined row.
187
+
188
+ This is true for Postgres RLS as well.
189
+
190
+ ## Top Level `.selectAll()` is not allowed
191
+
192
+ While `kysely-access-control` allows usage of `.selectAll()` in subqueries, it does not allow it at the top level
193
+ because it would circumvent column permissions controls.
194
+
195
+ Unfortunately, even those you provide the column list to Kysely as a type, that type is not inspectable by the plugin
196
+ system (or at all by the runtime), and as a result we cannot do the sensible thing of replacing a `.selectAll()` with a
197
+ select of all columns.
198
+
199
+ # Features
200
+
201
+ ## Table/Column Statement Type + Context Controls
202
+
203
+ `createAccessControlPlugin` allows you to control access to tables and columns based on the statement type and context.
204
+ For example, you can allow a user to select from a table, but not update it, or allow a user to update a table, but not set a particular column.
205
+
206
+ For full controls, see the types of the guard:
207
+ ```typescript
208
+ type FullKyselyAccessControlGuard<KyselyDatabase> = {
209
+ table: (
210
+ table: TableNodeTableWithKeyOf<KyselyDatabase>,
211
+ statementType: StatementType,
212
+ tableUsageContext: TableUsageContext
213
+ ) => TableGuardResult<KyselyDatabase>;
214
+
215
+ column: (
216
+ table: TableNodeTableWithKeyOf<KyselyDatabase>,
217
+ column: ColumnNode["column"],
218
+ statementType: StatementType,
219
+ columnUsageContext: ColumnUsageContext
220
+ ) => ColumnGuardResult;
221
+ };
222
+
223
+ export enum StatementType {
224
+ Select = "select",
225
+ Insert = "insert",
226
+ Update = "update",
227
+ Delete = "delete",
228
+ }
229
+
230
+ export enum ColumnUsageContext {
231
+ ColumnInSelectOrReturning = "column-in-select-or-returning",
232
+ ColumnInWhereOrJoin = "column-in-where-or-join",
233
+ ColumnInUpdateSet = "column-in-update-set",
234
+ ColumnInInsert = "column-in-insert",
235
+ }
236
+
237
+ export enum TableUsageContext {
238
+ TableTopLevel = "table-top-level",
239
+ TableInJoin = "table-in-join",
240
+ }
241
+ ```
242
+
243
+ ## RLS in Select/Update/Delete
244
+
245
+ In addition to returning a simple `Allow` token to allow access, you can also return a tuple where the second
246
+ argument is a Kysely where clause to be added to the query.
247
+
248
+ For example, you can implement RLS like so:
249
+ ```typescript
250
+ const guard: KyselyAccessControlGuard = {
251
+ table: (table) => {
252
+ if (table.name === 'people') {
253
+ return [
254
+ Allow,
255
+ expressionBuilder<Database, 'people'>().eb('is_deleted', 'is', false);
256
+ ];
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ Now, any query that targets the `people` table will have `is_deleted` is false inlined as a where clause.
263
+
264
+ ## Column Omission vs Erroring
265
+
266
+ At column level select statements, you can choose `Omit` as a third option to `Allow` vs. `Deny`.
267
+
268
+ If you choose this option, the column you select will be omitted from the query, and the query will still succeed.
269
+
270
+ This also works for `returning` clauses as well, whether they are on a top level insert, update, or delete statement.
271
+
272
+ # Contributing
273
+
274
+ The most helpful form of contribution right now would be additional tests on complex queries in your
275
+ actual applications.
276
+
277
+ Currently, `kysely-access-control` has not been tested to properly enforce permissions with every type of SQL query
278
+ Kysely itself can generate.
279
+
280
+ However, it has been programmed to throw errors if it encounters a query type that is not yet implemented,
281
+ and it should generate these errors even if you don't enforce any particularly complex permission on them.
282
+
283
+ For any of these failures, it is possible to make `kysely-access-control` work, it just requires a few more `if`s, so
284
+ please open an issue.
@@ -0,0 +1,2 @@
1
+ export * from "./src/kyselyAccessControl";
2
+ export * from "./src/kyselyGrants";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./src/kyselyAccessControl"), exports);
18
+ __exportStar(require("./src/kyselyGrants"), exports);
@@ -0,0 +1,40 @@
1
+ import { ColumnNode, ExpressionWrapper, KyselyPlugin, TableNode, SqlBool } from "kysely";
2
+ export declare const Allow: "allow";
3
+ export declare const Deny: "deny";
4
+ export declare const Omit: "omit";
5
+ type TAllow = typeof Allow;
6
+ type TDeny = typeof Deny;
7
+ type TOmit = typeof Omit;
8
+ export declare enum StatementType {
9
+ Select = "select",
10
+ Insert = "insert",
11
+ Update = "update",
12
+ Delete = "delete"
13
+ }
14
+ export declare enum ColumnUsageContext {
15
+ ColumnInSelectOrReturning = "column-in-select-or-returning",
16
+ ColumnInWhereOrJoin = "column-in-where-or-join",
17
+ ColumnInUpdateSet = "column-in-update-set",
18
+ ColumnInInsert = "column-in-insert"
19
+ }
20
+ export declare enum TableUsageContext {
21
+ TableTopLevel = "table-top-level",
22
+ TableInJoin = "table-in-join"
23
+ }
24
+ type TableGuardResult<KyselyDatabase> = TAllow | [TAllow, ExpressionWrapper<KyselyDatabase, any, SqlBool>] | TDeny | [TDeny, string];
25
+ type ColumnGuardResult = TAllow | TOmit | TDeny | [TDeny, string];
26
+ type TableNodeTable = TableNode["table"];
27
+ type TableNodeTableIdentifierWithNamesAsKeyOf<KyselyDatabase> = Omit<TableNodeTable["identifier"], "name"> & {
28
+ name: keyof KyselyDatabase;
29
+ };
30
+ type TableNodeTableWithKeyOf<KyselyDatabase> = Omit<TableNodeTable, "identifier"> & {
31
+ identifier: TableNodeTableIdentifierWithNamesAsKeyOf<KyselyDatabase>;
32
+ };
33
+ export declare const throwIfDenyWithReason: (guardResult: ColumnGuardResult | TableGuardResult<unknown>, coreErrorString: string) => void;
34
+ type FullKyselyAccessControlGuard<KyselyDatabase = unknown> = {
35
+ table: (table: TableNodeTableWithKeyOf<KyselyDatabase>, statementType: StatementType, tableUsageContext: TableUsageContext) => TableGuardResult<KyselyDatabase>;
36
+ column: (table: TableNodeTableWithKeyOf<KyselyDatabase>, column: ColumnNode["column"], statementType: StatementType, columnUsageContext: ColumnUsageContext) => ColumnGuardResult;
37
+ };
38
+ export type KyselyAccessControlGuard<KyselyDatabase = unknown> = Partial<FullKyselyAccessControlGuard<KyselyDatabase>>;
39
+ export declare const createAccessControlPlugin: <KyselyDatabase = unknown>(guard: Partial<FullKyselyAccessControlGuard<KyselyDatabase>>) => KyselyPlugin;
40
+ export {};
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createAccessControlPlugin = exports.throwIfDenyWithReason = exports.TableUsageContext = exports.ColumnUsageContext = exports.StatementType = exports.Omit = exports.Deny = exports.Allow = void 0;
7
+ const kysely_1 = require("kysely");
8
+ const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
9
+ exports.Allow = "allow";
10
+ exports.Deny = "deny";
11
+ exports.Omit = "omit";
12
+ var StatementType;
13
+ (function (StatementType) {
14
+ StatementType["Select"] = "select";
15
+ StatementType["Insert"] = "insert";
16
+ StatementType["Update"] = "update";
17
+ StatementType["Delete"] = "delete";
18
+ })(StatementType || (exports.StatementType = StatementType = {}));
19
+ var ColumnUsageContext;
20
+ (function (ColumnUsageContext) {
21
+ ColumnUsageContext["ColumnInSelectOrReturning"] = "column-in-select-or-returning";
22
+ ColumnUsageContext["ColumnInWhereOrJoin"] = "column-in-where-or-join";
23
+ ColumnUsageContext["ColumnInUpdateSet"] = "column-in-update-set";
24
+ ColumnUsageContext["ColumnInInsert"] = "column-in-insert";
25
+ })(ColumnUsageContext || (exports.ColumnUsageContext = ColumnUsageContext = {}));
26
+ var TableUsageContext;
27
+ (function (TableUsageContext) {
28
+ TableUsageContext["TableTopLevel"] = "table-top-level";
29
+ TableUsageContext["TableInJoin"] = "table-in-join";
30
+ })(TableUsageContext || (exports.TableUsageContext = TableUsageContext = {}));
31
+ const throwIfDenyWithReason = (guardResult, coreErrorString) => {
32
+ if (guardResult === exports.Deny) {
33
+ throw new Error(coreErrorString);
34
+ }
35
+ if (guardResult[0] === exports.Deny) {
36
+ throw new Error(`${coreErrorString}: ${guardResult[1]}`);
37
+ }
38
+ };
39
+ exports.throwIfDenyWithReason = throwIfDenyWithReason;
40
+ const createAccessControlPlugin = (guard) => {
41
+ // 2 things are accomplished in this translation into fullGuard
42
+ // 1. Default guards are provided if the user does not provide either .table or .column
43
+ // 2. We lose table and column keyof typings so that we can safely call these guards internally
44
+ // without extra coercion
45
+ const fullGuard = Object.assign({ table: () => {
46
+ return exports.Allow;
47
+ }, column: () => {
48
+ return exports.Allow;
49
+ } }, guard);
50
+ class Transformer extends kysely_1.OperationNodeTransformer {
51
+ getParentNode() {
52
+ return this.nodeStack[this.nodeStack.length - 2]; // last element is the current one, one before is the parent
53
+ }
54
+ isAChildOf(nodeType) {
55
+ return this.nodeStack.find((node) => nodeType.is(node)) !== undefined;
56
+ }
57
+ /**
58
+ * Enforce update on a table
59
+ * - enforces whether the table is allowed to be updated
60
+ * - enforce the target columns to be updated
61
+ */
62
+ transformUpdateQuery(node) {
63
+ var _a, _b;
64
+ const tableNode = node.table;
65
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(tableNode), "kysely-access-control: only table nodes are supported for update queries");
66
+ const guardResult = fullGuard.table(tableNode.table, StatementType.Update, TableUsageContext.TableTopLevel);
67
+ (0, exports.throwIfDenyWithReason)(guardResult, `UPDATE denied on table ${((_a = tableNode.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${tableNode.table.schema.name}.` : ""}${tableNode.table.identifier.name}`);
68
+ // Enforce column permissions
69
+ if (node.updates) {
70
+ for (const columnUpdateNode of node.updates) {
71
+ const column = columnUpdateNode.column;
72
+ const guardResult = fullGuard.column(tableNode.table, column.column, StatementType.Update, ColumnUsageContext.ColumnInUpdateSet);
73
+ (0, exports.throwIfDenyWithReason)(guardResult, `UPDATE denied on column ${((_b = tableNode.table.schema) === null || _b === void 0 ? void 0 : _b.name)
74
+ ? `${tableNode.table.schema.name}.`
75
+ : ""}${tableNode.table.identifier.name}.${column.column.name}`);
76
+ if (guardResult === exports.Omit) {
77
+ throw new Error(`Omit is not supported in update set: got Omit for ${column.column.name}`);
78
+ }
79
+ }
80
+ }
81
+ // Must be allow
82
+ return super.transformUpdateQuery(node);
83
+ }
84
+ /**
85
+ * Enforce insert on a table
86
+ * - enforces whether the table is allowed to be inserted into
87
+ *
88
+ * Enforcement of returning limitations is handled in transformReturning
89
+ */
90
+ transformInsertQuery(node) {
91
+ var _a, _b;
92
+ const tableNode = node.into;
93
+ const columns = node.columns;
94
+ const guardResult = fullGuard.table(tableNode.table, StatementType.Insert, TableUsageContext.TableTopLevel);
95
+ (0, exports.throwIfDenyWithReason)(guardResult, `INSERT denied on table ${((_a = tableNode.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${tableNode.table.schema.name}.` : ""}${tableNode.table.identifier.name}`);
96
+ // Skip column enforcement if there are none
97
+ if (columns === undefined) {
98
+ return super.transformInsertQuery(node);
99
+ }
100
+ const transformedColumns = [];
101
+ for (const column of columns) {
102
+ const guardResult = fullGuard.column(tableNode.table, column.column, StatementType.Insert, ColumnUsageContext.ColumnInInsert);
103
+ (0, exports.throwIfDenyWithReason)(guardResult, `INSERT denied on column ${((_b = tableNode.table.schema) === null || _b === void 0 ? void 0 : _b.name)
104
+ ? `${tableNode.table.schema.name}.`
105
+ : ""}${tableNode.table.identifier.name}.${column.column.name}`);
106
+ if (guardResult === exports.Omit) {
107
+ continue;
108
+ }
109
+ transformedColumns.push(column);
110
+ }
111
+ return super.transformInsertQuery(Object.assign(Object.assign({}, node), { columns: transformedColumns }));
112
+ }
113
+ /**
114
+ * Handles enforcement of column permissions in returning
115
+ * for insert/update/delete
116
+ */
117
+ transformReturning(node) {
118
+ // Check whether it's insert, update, or delete via node stack
119
+ const parentNode = this.getParentNode();
120
+ const mode = kysely_1.InsertQueryNode.is(parentNode)
121
+ ? StatementType.Insert
122
+ : kysely_1.UpdateQueryNode.is(parentNode)
123
+ ? StatementType.Update
124
+ : kysely_1.DeleteQueryNode.is(parentNode)
125
+ ? StatementType.Delete
126
+ : undefined;
127
+ (0, tiny_invariant_1.default)(mode !== undefined, `kysely-access-control: returning must be used with insert, update, or delete. kind was ${parentNode.kind}`);
128
+ const { selections } = node;
129
+ const [statementType, tableNode] = kysely_1.InsertQueryNode.is(parentNode)
130
+ ? [StatementType.Insert, parentNode.into]
131
+ : kysely_1.UpdateQueryNode.is(parentNode)
132
+ ? [StatementType.Update, parentNode.table]
133
+ : kysely_1.DeleteQueryNode.is(parentNode)
134
+ ? [StatementType.Delete, parentNode.from.froms[0]]
135
+ : [undefined, undefined];
136
+ // Only inserting into a table is supported
137
+ (0, tiny_invariant_1.default)(statementType !== undefined, "kysely-access-control: currently only insert/update/delete returning is supported");
138
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(tableNode), "kysely-access-control: currently only update/delete from a table");
139
+ const transformedSelections = this._transformSelections(selections.slice(), tableNode, false, statementType);
140
+ const transformedNode = Object.assign(Object.assign({}, node), { selections: transformedSelections });
141
+ return super.transformReturning(transformedNode);
142
+ }
143
+ /**
144
+ * Enforce delete on a table
145
+ * - enforces whether the table is allowed to be deleted from
146
+ */
147
+ transformDeleteQuery(node) {
148
+ var _a;
149
+ // Ensure only 1 from and that its a table
150
+ (0, tiny_invariant_1.default)(node.from.froms.length === 1, "kysely-access-control: can only delete from one table at a time");
151
+ const tableNode = node.from.froms[0];
152
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(tableNode), "kysely-access-control: can only delete from tables");
153
+ const guardResult = fullGuard.table(tableNode.table, StatementType.Delete, TableUsageContext.TableTopLevel);
154
+ (0, exports.throwIfDenyWithReason)(guardResult, `DELETE denied on table ${((_a = tableNode.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${tableNode.table.schema.name}.` : ""}${tableNode.table.identifier.name}`);
155
+ // Must be allow - TODO add RLS
156
+ return super.transformDeleteQuery(node);
157
+ }
158
+ /**
159
+ * In transformSelectQuery, we:
160
+ * - throw if any columns are selected that shouldn't be
161
+ * - omit any columns we should omit (throwing if selectAll is used)
162
+ */
163
+ transformSelectQuery(node) {
164
+ var _a;
165
+ const { from: fromNode, selections, joins, where } = node;
166
+ if (!fromNode) {
167
+ // This covers queries such as select 1, or select following only by subselects
168
+ // We do nothing here
169
+ return super.transformSelectQuery(node);
170
+ }
171
+ (0, tiny_invariant_1.default)(fromNode.froms.length === 1, "kysely-access-control: there must be exactly one from node when not joining");
172
+ const tableNode = fromNode.froms[0];
173
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(tableNode), "kysely-access-control: currently only select from table/view is supported");
174
+ (0, tiny_invariant_1.default)(selections !== undefined, "kysely-access-control: selections should be defined");
175
+ const table = tableNode.table;
176
+ const guardResult = fullGuard.table(table, StatementType.Select, TableUsageContext.TableTopLevel);
177
+ (0, exports.throwIfDenyWithReason)(guardResult, `SELECT denied on table ${((_a = table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${table.schema.name}.` : ""}${table.identifier.name}`);
178
+ /* COLUMN ENFORCEMENT */
179
+ // Some selected columns include a table, some don't
180
+ // If there's no joins and therefore only one valid relation to reference
181
+ // we can assume that the column's table is the same as the fromNode's table
182
+ //
183
+ // If there is a join, we require that the user specifies the table
184
+ // Even though kysely's type system and SQL engines can resolve the reference,
185
+ // We cannot
186
+ const hasJoin = joins !== undefined && joins.length > 0;
187
+ const transformedSelections = this._transformSelections(selections.slice(), tableNode, hasJoin, StatementType.Select);
188
+ const newNode = Object.assign(Object.assign({}, node), { selections: transformedSelections, where: this._transformWhere(guardResult, node.where) });
189
+ return super.transformSelectQuery(newNode);
190
+ }
191
+ /**
192
+ * Next 3 methods enforce table level permissions
193
+ * included Allow/Deny and row level permissions
194
+ * for the 3 different types of joins:
195
+ * - select * from x join y on x.key = y.key
196
+ * - update x from y where x.key = y.key
197
+ * - delete from x using y where x.key = y.key
198
+ */
199
+ transformJoin(node) {
200
+ var _a;
201
+ const tableNode = node.table;
202
+ if (!kysely_1.TableNode.is(tableNode)) {
203
+ // If it's not a table node (it's an alias node with a subselect, etc.)
204
+ // Any enforcement needed will happen on those components
205
+ return super.transformJoin(node);
206
+ }
207
+ const guardResult = fullGuard.table(tableNode.table, StatementType.Select, TableUsageContext.TableInJoin);
208
+ (0, exports.throwIfDenyWithReason)(guardResult, `JOIN denied on table ${((_a = tableNode.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${tableNode.table.schema.name}.` : ""}${tableNode.table.identifier.name}`);
209
+ if (guardResult === exports.Allow) {
210
+ return super.transformJoin(node);
211
+ }
212
+ // If RLS is applied, replace the table node with a select node that has the RLS applied inline
213
+ // This means replacing the "table" with an AliasNode of a SelectQueryNode + identifier with the same name
214
+ // Fortunately, our top level transformSelectQueryBuilder will handle applying RLS
215
+ // We just need to transform it to a SelectQueryBuilder with an alias so that those
216
+ // transformations can happen
217
+ const newTable = kysely_1.AliasNode.create(kysely_1.SelectQueryNode.cloneWithSelections(kysely_1.SelectQueryNode.createFrom([tableNode]), [kysely_1.SelectionNode.createSelectAll()]), kysely_1.IdentifierNode.create(tableNode.table.identifier.name));
218
+ return super.transformJoin(Object.assign(Object.assign({}, node), { table: newTable }));
219
+ }
220
+ transformFrom(node) {
221
+ const parentNode = this.getParentNode();
222
+ if (!kysely_1.UpdateQueryNode.is(parentNode)) {
223
+ return super.transformFrom(node);
224
+ }
225
+ const newFroms = node.froms.map((from) => {
226
+ var _a;
227
+ if (!kysely_1.TableNode.is(from)) {
228
+ // Only guard tables - non tables (subselects) will be handled further down in
229
+ // the internal SelectQueryNode
230
+ return from;
231
+ }
232
+ const guardResult = fullGuard.table(from.table, StatementType.Update, TableUsageContext.TableInJoin);
233
+ (0, exports.throwIfDenyWithReason)(guardResult, `JOIN denied on table ${((_a = from.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${from.table.schema.name}.` : ""}${from.table.identifier.name}`);
234
+ if (guardResult === exports.Allow) {
235
+ return from;
236
+ }
237
+ // Must be an RLS case
238
+ // Again, don't worry about the where clauses
239
+ // those will be handled by the internal SelectQueryNode
240
+ return kysely_1.AliasNode.create(kysely_1.SelectQueryNode.cloneWithSelections(kysely_1.SelectQueryNode.createFrom([from]), [kysely_1.SelectionNode.createSelectAll()]), kysely_1.IdentifierNode.create(from.table.identifier.name));
241
+ });
242
+ return super.transformFrom(Object.assign(Object.assign({}, node), { froms: newFroms }));
243
+ }
244
+ transformUsing(node) {
245
+ const parentNode = this.getParentNode();
246
+ if (!kysely_1.DeleteQueryNode.is(parentNode)) {
247
+ return super.transformUsing(node);
248
+ }
249
+ const newTables = node.tables.map((table) => {
250
+ var _a;
251
+ if (!kysely_1.TableNode.is(table)) {
252
+ // Only guard tables - non tables (subselects) will be handled further down in
253
+ // the internal SelectQueryNode
254
+ return table;
255
+ }
256
+ const guardResult = fullGuard.table(table.table, StatementType.Delete, TableUsageContext.TableInJoin);
257
+ (0, exports.throwIfDenyWithReason)(guardResult, `JOIN denied on table ${((_a = table.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${table.table.schema.name}.` : ""}${table.table.identifier.name}`);
258
+ if (guardResult === exports.Allow) {
259
+ return table;
260
+ }
261
+ // Must be an RLS case
262
+ // Again, don't worry about the where clauses
263
+ // those will be handled by the internal SelectQueryNode
264
+ return kysely_1.AliasNode.create(kysely_1.SelectQueryNode.cloneWithSelections(kysely_1.SelectQueryNode.createFrom([table]), [kysely_1.SelectionNode.createSelectAll()]), kysely_1.IdentifierNode.create(table.table.identifier.name));
265
+ });
266
+ return super.transformUsing(Object.assign(Object.assign({}, node), { tables: newTables }));
267
+ }
268
+ /**
269
+ * Enforce column permissions in update set clause
270
+ */
271
+ // protected transform
272
+ /**
273
+ * Enforce column permissions in where clause
274
+ * These are always wrapped in a reference node
275
+ *
276
+ * Reference nodes are also used in select statements, so we return early
277
+ * if we're not in a recursion with a WhereNode parent
278
+ */
279
+ transformReference(node) {
280
+ var _a;
281
+ const isAChildOfWhere = this.isAChildOf(kysely_1.WhereNode);
282
+ const isAChildOfJoin = this.isAChildOf(kysely_1.JoinNode);
283
+ if (!isAChildOfWhere && !isAChildOfJoin) {
284
+ return super.transformReference(node);
285
+ }
286
+ // If it's a child of where, then it's a column reference
287
+ // being used in a filter statement, so we call the guard with those parameters
288
+ // However, the table may not be specified, and so we need to search up the stack
289
+ // to something that has the table specified
290
+ // The entity with the specified table should be the one that is the parent of the where node
291
+ // but it could be an insert/update/delete or select statement
292
+ // TODO - we're calling the table with the wrong column here because there could be a top level join
293
+ // Need to refactor this to up front decide if the table specified is required
294
+ let tableNode;
295
+ const tableNodeSpecifiedWithColumn = node.table;
296
+ if (!tableNodeSpecifiedWithColumn) {
297
+ // If it's a child of join, we need the table specified
298
+ // It can't be inferred
299
+ if (!isAChildOfWhere) {
300
+ throw new Error("kysely-access-control: could not find table node for column reference in join");
301
+ }
302
+ const reversedStack = this.nodeStack.slice().reverse();
303
+ const idxOfWhere = reversedStack.findIndex((node) => kysely_1.WhereNode.is(node));
304
+ const idxOfParent = idxOfWhere + 1;
305
+ const parentOfWhere = reversedStack[idxOfParent];
306
+ (0, tiny_invariant_1.default)(parentOfWhere !== undefined, "kysely-access-control: could not find parent of where node");
307
+ const hasJoins = this._topLevelHasMoreThanOneTable(parentOfWhere);
308
+ if (hasJoins) {
309
+ throw new Error("kysely-access-control: if joins are present, each column reference in where must specify the table");
310
+ }
311
+ const foundTableNode = this._getTableNodeFromTopLevelQueryNode(parentOfWhere);
312
+ (0, tiny_invariant_1.default)(foundTableNode !== undefined, "kysely-access-control: could not find table node for column reference in filter statement");
313
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(foundTableNode), "kysely-access-control: node for column reference in filter statement must be a table node");
314
+ tableNode = foundTableNode;
315
+ }
316
+ else {
317
+ tableNode = tableNodeSpecifiedWithColumn;
318
+ }
319
+ const columnNode = node.column;
320
+ (0, tiny_invariant_1.default)(kysely_1.ColumnNode.is(columnNode), "kysely-access-control: select all in filter statement is not supported");
321
+ const guardResult = fullGuard.column(tableNode.table, columnNode.column, StatementType.Select, ColumnUsageContext.ColumnInWhereOrJoin);
322
+ (0, exports.throwIfDenyWithReason)(guardResult, `FILTER denied on column ${((_a = tableNode.table.schema) === null || _a === void 0 ? void 0 : _a.name) ? `${tableNode.table.schema.name}.` : ""}${tableNode.table.identifier.name}.${columnNode.column.name}`);
323
+ // Must be allow now
324
+ return super.transformReference(node);
325
+ }
326
+ /*
327
+ * From here on down there are utility methods that are not directly called by the Kysely plugin machinery
328
+ */
329
+ /**
330
+ * Get whether an SelectQueryNode, UpdateQueryNode, or DeleteQueryNode has more than one table in reference scope
331
+ * for select and where clauses
332
+ */
333
+ _topLevelHasMoreThanOneTable(node) {
334
+ if (kysely_1.UpdateQueryNode.is(node)) {
335
+ const fromJoin = node.from;
336
+ if (fromJoin && fromJoin.froms.length > 0) {
337
+ return true;
338
+ }
339
+ return false;
340
+ }
341
+ if (kysely_1.SelectQueryNode.is(node)) {
342
+ const join = node.joins;
343
+ return !!join && join.length > 0;
344
+ }
345
+ if (kysely_1.DeleteQueryNode.is(node)) {
346
+ const using = node.using;
347
+ if (using && using.tables && using.tables.length > 0) {
348
+ return true;
349
+ }
350
+ return false;
351
+ }
352
+ throw new Error("_topLevelHasMoreThanOneTable called with something that is not a select, update, or delete query");
353
+ }
354
+ /**
355
+ * Get the table node for a top level query type (select, update, delete, or insert)
356
+ */
357
+ _getTableNodeFromTopLevelQueryNode(node) {
358
+ if (kysely_1.UpdateQueryNode.is(node)) {
359
+ (0, tiny_invariant_1.default)(node.table !== undefined && kysely_1.TableNode.is(node.table), "kysely-access-control: update query must have a table");
360
+ return node.table;
361
+ }
362
+ if (kysely_1.SelectQueryNode.is(node)) {
363
+ (0, tiny_invariant_1.default)(node.from !== undefined && node.from.froms.length === 1, "kysely-access-control: select query must have exactly one from");
364
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(node.from.froms[0]), "kysely-access-control: select query must have a table");
365
+ return node.from.froms[0];
366
+ }
367
+ if (kysely_1.DeleteQueryNode.is(node)) {
368
+ (0, tiny_invariant_1.default)(node.from !== undefined && node.from.froms.length === 1, "kysely-access-control: delete query must have exactly one from");
369
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(node.from.froms[0]), "kysely-access-control: delete query must have a table");
370
+ return node.from.froms[0];
371
+ }
372
+ if (kysely_1.InsertQueryNode.is(node)) {
373
+ (0, tiny_invariant_1.default)(kysely_1.TableNode.is(node.into), "kysely-access-control: insert query must have a table");
374
+ return node.into;
375
+ }
376
+ throw new Error("_getTopLevelTableNode called with something that is not a select, update, delete, or insert query");
377
+ }
378
+ /**
379
+ * Common utility used in transformSelectQuery, transformReturning, etc.
380
+ * that enforces column select permissions
381
+ */
382
+ _transformSelections(selections, tableNode, scopedHasMoreThanOneTable, statementType) {
383
+ var _a;
384
+ const transformedSelections = [];
385
+ // We only allow a select all IF it's inside of a join
386
+ // otherwise, we require that the user specifies the columns
387
+ const selectAllIsAllowed = this.isAChildOf(kysely_1.JoinNode) ||
388
+ this.isAChildOf(kysely_1.FromNode) ||
389
+ this.isAChildOf(kysely_1.UsingNode);
390
+ if (selectAllIsAllowed) {
391
+ return selections.slice();
392
+ }
393
+ for (const selectionNode of selections) {
394
+ const { selection } = selectionNode;
395
+ // Handle SelectQueryNode selections (from jsonObjectFrom, jsonArrayFrom, etc.)
396
+ if (kysely_1.SelectQueryNode.is(selection)) {
397
+ // For subquery selections, recursively transform the subquery to apply access control
398
+ // The subquery transformation will be handled by the parent transformer
399
+ // We just need to ensure the subquery gets processed, so we pass it through
400
+ // The parent transformer will call transformSelectQuery on the subquery
401
+ transformedSelections.push(selectionNode);
402
+ continue;
403
+ }
404
+ // Handle SelectAllNode selections (from selectAll())
405
+ if (selection.kind === "SelectAllNode") {
406
+ throw new Error("kysely-access-control: .selectAll() is not supported");
407
+ }
408
+ // Handle ReferenceNode selections (column references)
409
+ if (kysely_1.ReferenceNode.is(selection)) {
410
+ const { table: columnIncludedTableNode, column: columnNode } = selection;
411
+ (0, tiny_invariant_1.default)(columnNode.kind !== "SelectAllNode", "kysely-access-control: .selectAll() is not supported");
412
+ let tableNodeToUseForColumn = tableNode;
413
+ if (scopedHasMoreThanOneTable) {
414
+ (0, tiny_invariant_1.default)(columnIncludedTableNode !== undefined, `kysely-access-control: table must be specified for each column when joining - could not infer table for ${columnNode.column.name}`);
415
+ tableNodeToUseForColumn = columnIncludedTableNode;
416
+ }
417
+ const guardResult = fullGuard.column(tableNodeToUseForColumn.table, columnNode.column, statementType, ColumnUsageContext.ColumnInSelectOrReturning);
418
+ (0, exports.throwIfDenyWithReason)(guardResult, `SELECT denied on column ${((_a = tableNodeToUseForColumn.table.schema) === null || _a === void 0 ? void 0 : _a.name)
419
+ ? `${tableNodeToUseForColumn.table.schema.name}.`
420
+ : ""}${tableNodeToUseForColumn.table.identifier.name}.${columnNode.column.name}`);
421
+ if (guardResult === exports.Omit) {
422
+ continue;
423
+ }
424
+ transformedSelections.push(selectionNode);
425
+ continue;
426
+ }
427
+ // For other selection types (expressions, raw builders, etc.), allow them to pass through
428
+ // These will be transformed by the parent transformer and don't directly reference main table columns
429
+ transformedSelections.push(selectionNode);
430
+ }
431
+ return transformedSelections;
432
+ }
433
+ _transformWhere(guardResult, nodeWhere) {
434
+ if (guardResult === exports.Allow) {
435
+ return nodeWhere;
436
+ }
437
+ const guardWhereUnguarded = guardResult[0] === exports.Allow ? guardResult[1] : undefined;
438
+ (0, tiny_invariant_1.default)(guardWhereUnguarded !== undefined &&
439
+ typeof guardWhereUnguarded === "object" &&
440
+ "toOperationNode" in guardWhereUnguarded, "kysely-access-control: returned where must be an expression wrapper");
441
+ const guardWhere = kysely_1.WhereNode.create(guardWhereUnguarded.toOperationNode());
442
+ const newWhere = guardWhere && nodeWhere
443
+ ? kysely_1.WhereNode.create(kysely_1.AndNode.create(guardWhere.where, nodeWhere.where))
444
+ : guardWhere || nodeWhere;
445
+ return super.transformWhere(newWhere);
446
+ }
447
+ }
448
+ const plugin = {
449
+ transformQuery: (args) => {
450
+ const transformer = new Transformer();
451
+ return transformer.transformNode(args.node);
452
+ },
453
+ transformResult: (args) => {
454
+ return Promise.resolve(args.result);
455
+ },
456
+ };
457
+ return plugin;
458
+ };
459
+ exports.createAccessControlPlugin = createAccessControlPlugin;
@@ -0,0 +1,26 @@
1
+ import { ExpressionBuilder, ExpressionWrapper, SqlBool } from "kysely";
2
+ import { ColumnUsageContext, StatementType } from "./kyselyAccessControl";
3
+ type GrantWithoutWhereClause<KyselyDatabase, TableName extends keyof KyselyDatabase> = {
4
+ table: TableName;
5
+ schema?: string;
6
+ for: "all" | "select" | "update" | "insert" | "delete";
7
+ columns?: (keyof KyselyDatabase[TableName])[];
8
+ };
9
+ type GrantWithWhereClause<KyselyDatabase, TableName extends keyof KyselyDatabase> = GrantWithoutWhereClause<KyselyDatabase, TableName> & {
10
+ where: (eb: ExpressionBuilder<KyselyDatabase, TableName>) => ExpressionWrapper<KyselyDatabase, TableName, SqlBool>;
11
+ whereType?: "permissive" | "restrictive";
12
+ };
13
+ export type Grant<KyselyDatabase, TableName extends keyof KyselyDatabase> = GrantWithWhereClause<KyselyDatabase, TableName> | GrantWithoutWhereClause<KyselyDatabase, TableName>;
14
+ export declare const createKyselyGrantGuard: <KyselyDatabase>(grants: Grant<KyselyDatabase, any>[]) => Partial<{
15
+ table: (table: Omit<import("kysely/dist/cjs/operation-node/schemable-identifier-node").SchemableIdentifierNode, "identifier"> & {
16
+ identifier: Omit<import("kysely").IdentifierNode, "name"> & {
17
+ name: never;
18
+ };
19
+ }, statementType: StatementType, tableUsageContext: import("./kyselyAccessControl").TableUsageContext) => "allow" | "deny" | ["deny", string] | ["allow", ExpressionWrapper<unknown, any, SqlBool>];
20
+ column: (table: Omit<import("kysely/dist/cjs/operation-node/schemable-identifier-node").SchemableIdentifierNode, "identifier"> & {
21
+ identifier: Omit<import("kysely").IdentifierNode, "name"> & {
22
+ name: never;
23
+ };
24
+ }, column: import("kysely").IdentifierNode, statementType: StatementType, columnUsageContext: ColumnUsageContext) => "allow" | "deny" | "omit" | ["deny", string];
25
+ }>;
26
+ export {};
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createKyselyGrantGuard = void 0;
4
+ const kysely_1 = require("kysely");
5
+ const kyselyAccessControl_1 = require("./kyselyAccessControl");
6
+ const isGrantWithWhereClause = (grant) => "where" in grant;
7
+ const createKyselyGrantGuard = (grants) => {
8
+ const guard = {
9
+ table: (table, statementType) => {
10
+ const allowGrants = grants.filter((grant) => {
11
+ var _a;
12
+ return ((grant.schema === undefined || grant.schema === ((_a = table.schema) === null || _a === void 0 ? void 0 : _a.name)) &&
13
+ grant.table === table.identifier.name &&
14
+ (grant.for === "all" || grant.for === statementType));
15
+ });
16
+ if (allowGrants.length === 0) {
17
+ return kyselyAccessControl_1.Deny;
18
+ }
19
+ // Now that we know we're allowing, we create all RLS
20
+ const grantsWithWheres = allowGrants.filter(isGrantWithWhereClause);
21
+ const grantsWithRestrictiveWheres = grantsWithWheres.filter((grant) => grant.whereType === "restrictive");
22
+ // Permissive is the default
23
+ const grantsWithPermissiveWheres = grantsWithWheres.filter((grant) => grant.whereType !== "restrictive");
24
+ if (grantsWithRestrictiveWheres.length === 0 &&
25
+ grantsWithPermissiveWheres.length === 0) {
26
+ return kyselyAccessControl_1.Allow;
27
+ }
28
+ if (grantsWithPermissiveWheres.length === 0 &&
29
+ grantsWithRestrictiveWheres.length > 0) {
30
+ // No rows will be returned - see https://www.postgresql.org/docs/current/sql-createpolicy.html
31
+ return [kyselyAccessControl_1.Allow, (0, kysely_1.expressionBuilder)().lit(false)];
32
+ }
33
+ const permissiveEbs = (0, kysely_1.expressionBuilder)().or(grantsWithPermissiveWheres.map((grant) => grant.where((0, kysely_1.expressionBuilder)())));
34
+ if (grantsWithRestrictiveWheres.length > 0) {
35
+ // And the or of the permissives and the and of the restrictives
36
+ return [
37
+ kyselyAccessControl_1.Allow,
38
+ (0, kysely_1.expressionBuilder)().and([
39
+ permissiveEbs,
40
+ (0, kysely_1.expressionBuilder)().and(grantsWithRestrictiveWheres.map((grant) => grant.where((0, kysely_1.expressionBuilder)()))),
41
+ ]),
42
+ ];
43
+ }
44
+ else {
45
+ return [kyselyAccessControl_1.Allow, permissiveEbs];
46
+ }
47
+ },
48
+ column: (table, column, statementType, columnUsageContext) => {
49
+ const allowGrant = grants.find((grant) => {
50
+ var _a;
51
+ const rightSchemaAndTableAndColumns = (grant.schema === undefined || grant.schema === ((_a = table.schema) === null || _a === void 0 ? void 0 : _a.name)) &&
52
+ grant.table === table.identifier.name &&
53
+ (grant.columns === undefined ||
54
+ (Array.isArray(grant.columns) &&
55
+ // TODO - retype this when column node is typed
56
+ grant.columns.includes(column.name)));
57
+ if (!rightSchemaAndTableAndColumns) {
58
+ return false;
59
+ }
60
+ if (grant.for === "all") {
61
+ return true;
62
+ }
63
+ if (grant.for === kyselyAccessControl_1.StatementType.Select &&
64
+ [
65
+ kyselyAccessControl_1.ColumnUsageContext.ColumnInSelectOrReturning,
66
+ kyselyAccessControl_1.ColumnUsageContext.ColumnInWhereOrJoin,
67
+ ].includes(columnUsageContext)) {
68
+ return true;
69
+ }
70
+ if (grant.for === kyselyAccessControl_1.StatementType.Update &&
71
+ kyselyAccessControl_1.ColumnUsageContext.ColumnInUpdateSet === columnUsageContext) {
72
+ return true;
73
+ }
74
+ if (grant.for === kyselyAccessControl_1.StatementType.Insert &&
75
+ kyselyAccessControl_1.ColumnUsageContext.ColumnInInsert === columnUsageContext) {
76
+ return true;
77
+ }
78
+ return false;
79
+ });
80
+ if (!allowGrant) {
81
+ return kyselyAccessControl_1.Deny;
82
+ }
83
+ return kyselyAccessControl_1.Allow;
84
+ },
85
+ };
86
+ return guard;
87
+ };
88
+ exports.createKyselyGrantGuard = createKyselyGrantGuard;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@sladkoff/kysely-access-control",
3
+ "main": "dist/index.js",
4
+ "types": "dist/index.d.ts",
5
+ "module": "index.ts",
6
+ "version": "0.0.6",
7
+ "scripts": {
8
+ "compile": "tsc -p tsconfig.build.json"
9
+ },
10
+ "devDependencies": {
11
+ "bun-types": "latest",
12
+ "typescript": "^5.2.2"
13
+ },
14
+ "peerDependencies": {
15
+ "typescript": "^5.0.0",
16
+ "kysely": "^0.26.3"
17
+ },
18
+ "dependencies": {
19
+ "tiny-invariant": "^1.3.1"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "require": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git@github.com:ben-pr-p/kysely-access-control.git"
35
+ },
36
+ "author": "Ben Packer <ben.paul.ryan.packer@gmail.com>",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/ben-pr-p/kysely-access-control/issues"
40
+ },
41
+ "homepage": "https://github.com/ben-pr-p/kysely-access-control"
42
+ }