@sladkoff/kysely-access-control 0.0.9 → 0.0.10
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 +40 -0
- package/dist/src/kyselyAccessControl.d.ts +18 -1
- package/dist/src/kyselyAccessControl.js +51 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,6 +215,8 @@ For example, the following pattern will not work:
|
|
|
215
215
|
|
|
216
216
|
Use the full table name without aliases in subqueries to ensure proper permission enforcement.
|
|
217
217
|
|
|
218
|
+
**Note**: If you need to use table aliases in a subquery and want to bypass access control, you can use `bypassAccessControl()` to skip the checks, but this should be done with caution as it may introduce security risks.
|
|
219
|
+
|
|
218
220
|
# Features
|
|
219
221
|
|
|
220
222
|
## Table/Column Statement Type + Context Controls
|
|
@@ -288,6 +290,44 @@ If you choose this option, the column you select will be omitted from the query,
|
|
|
288
290
|
|
|
289
291
|
This also works for `returning` clauses as well, whether they are on a top level insert, update, or delete statement.
|
|
290
292
|
|
|
293
|
+
## Bypassing Access Control for Subqueries
|
|
294
|
+
|
|
295
|
+
In some cases, you may want to embed a subquery that should bypass access control checks. This is useful when:
|
|
296
|
+
|
|
297
|
+
1. **Embedding queries from a different context**: You want to include data from a table that the current user doesn't have access to, but you've already validated the access at a higher level.
|
|
298
|
+
2. **Performance optimization**: You want to avoid redundant access control checks on subqueries that you know are safe.
|
|
299
|
+
3. **Complex query patterns**: You're building complex queries where some subqueries need different access control behavior.
|
|
300
|
+
|
|
301
|
+
Use the `bypassAccessControl()` helper function to mark a query builder so that its subquery will skip access control enforcement:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { bypassAccessControl, createAccessControlPlugin } from 'kysely-access-control';
|
|
305
|
+
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
306
|
+
|
|
307
|
+
const plugin = createAccessControlPlugin(guard);
|
|
308
|
+
|
|
309
|
+
// In a query with access control
|
|
310
|
+
const result = await db
|
|
311
|
+
.withPlugin(plugin)
|
|
312
|
+
.selectFrom("person")
|
|
313
|
+
.select((qb) => {
|
|
314
|
+
// This subquery will bypass access control checks
|
|
315
|
+
const rsvps = bypassAccessControl(qb.selectFrom("rsvp").select("id"));
|
|
316
|
+
|
|
317
|
+
return [
|
|
318
|
+
"person.first_name",
|
|
319
|
+
"person.last_name",
|
|
320
|
+
jsonArrayFrom(rsvps).as("rsvps"),
|
|
321
|
+
];
|
|
322
|
+
})
|
|
323
|
+
.execute();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Important Notes:**
|
|
327
|
+
- `bypassAccessControl()` only affects the specific query builder it wraps. Other parts of the query still have access control enforced.
|
|
328
|
+
- Use this feature carefully - bypassing access control can introduce security vulnerabilities if not used correctly.
|
|
329
|
+
- The function works by marking the query builder, so it must be called before the query builder is used in `jsonArrayFrom`/`jsonObjectFrom` or other subquery contexts.
|
|
330
|
+
|
|
291
331
|
# Contributing
|
|
292
332
|
|
|
293
333
|
The most helpful form of contribution right now would be additional tests on complex queries in your
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import { ColumnNode, ExpressionWrapper, KyselyPlugin, TableNode, SqlBool } from "kysely";
|
|
1
|
+
import { ColumnNode, ExpressionWrapper, KyselyPlugin, TableNode, OperationNode, SqlBool } from "kysely";
|
|
2
2
|
export declare const Allow: "allow";
|
|
3
3
|
export declare const Deny: "deny";
|
|
4
4
|
export declare const Omit: "omit";
|
|
5
|
+
/**
|
|
6
|
+
* Marks a query builder to bypass access control when used in subqueries.
|
|
7
|
+
* Use this for query builders created from `db` (without plugin) that you want
|
|
8
|
+
* to embed via jsonArrayFrom/jsonObjectFrom.
|
|
9
|
+
*
|
|
10
|
+
* This works by wrapping the query builder in a proxy that intercepts
|
|
11
|
+
* `.toOperationNode()` and marks the resulting node in a WeakMap.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const rsvps = bypassAccessControl(db.selectFrom("rsvp").select("id"));
|
|
16
|
+
* return [jsonArrayFrom(rsvps).as("rsvps")];
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function bypassAccessControl<T extends {
|
|
20
|
+
toOperationNode(): OperationNode;
|
|
21
|
+
}>(qb: T): T;
|
|
5
22
|
type TAllow = typeof Allow;
|
|
6
23
|
type TDeny = typeof Deny;
|
|
7
24
|
type TOmit = typeof Omit;
|
|
@@ -3,12 +3,56 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
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;
|
|
6
|
+
exports.createAccessControlPlugin = exports.throwIfDenyWithReason = exports.TableUsageContext = exports.ColumnUsageContext = exports.StatementType = exports.bypassAccessControl = exports.Omit = exports.Deny = exports.Allow = void 0;
|
|
7
7
|
const kysely_1 = require("kysely");
|
|
8
8
|
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
|
|
9
9
|
exports.Allow = "allow";
|
|
10
10
|
exports.Deny = "deny";
|
|
11
11
|
exports.Omit = "omit";
|
|
12
|
+
// WeakMap to track OperationNodes that should bypass access control
|
|
13
|
+
const bypassAccessControlNodes = new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Marks a query builder to bypass access control when used in subqueries.
|
|
16
|
+
* Use this for query builders created from `db` (without plugin) that you want
|
|
17
|
+
* to embed via jsonArrayFrom/jsonObjectFrom.
|
|
18
|
+
*
|
|
19
|
+
* This works by wrapping the query builder in a proxy that intercepts
|
|
20
|
+
* `.toOperationNode()` and marks the resulting node in a WeakMap.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const rsvps = bypassAccessControl(db.selectFrom("rsvp").select("id"));
|
|
25
|
+
* return [jsonArrayFrom(rsvps).as("rsvps")];
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function bypassAccessControl(qb) {
|
|
29
|
+
return new Proxy(qb, {
|
|
30
|
+
get(target, prop) {
|
|
31
|
+
if (prop === "toOperationNode") {
|
|
32
|
+
return () => {
|
|
33
|
+
const node = target.toOperationNode();
|
|
34
|
+
// Mark the node to bypass access control in WeakMap
|
|
35
|
+
bypassAccessControlNodes.set(node, true);
|
|
36
|
+
return node;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const value = target[prop];
|
|
40
|
+
// If it's a function that returns a query builder, wrap it too
|
|
41
|
+
if (typeof value === "function" && prop !== "toOperationNode") {
|
|
42
|
+
return (...args) => {
|
|
43
|
+
const result = value.apply(target, args);
|
|
44
|
+
// If the result is a query builder-like object, wrap it
|
|
45
|
+
if (result && typeof result === "object" && "toOperationNode" in result) {
|
|
46
|
+
return bypassAccessControl(result);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
exports.bypassAccessControl = bypassAccessControl;
|
|
12
56
|
var StatementType;
|
|
13
57
|
(function (StatementType) {
|
|
14
58
|
StatementType["Select"] = "select";
|
|
@@ -164,6 +208,12 @@ const createAccessControlPlugin = (guard) => {
|
|
|
164
208
|
transformSelectQuery(node) {
|
|
165
209
|
var _a;
|
|
166
210
|
const { from: fromNode, selections, joins, where } = node;
|
|
211
|
+
// Skip access control for nested subqueries that were explicitly marked to bypass
|
|
212
|
+
// This happens when a query builder is wrapped with bypassAccessControl()
|
|
213
|
+
if (bypassAccessControlNodes.has(node)) {
|
|
214
|
+
// This subquery was marked to bypass access control, skip enforcement
|
|
215
|
+
return super.transformSelectQuery(node);
|
|
216
|
+
}
|
|
167
217
|
if (!fromNode) {
|
|
168
218
|
// This covers queries such as select 1, or select following only by subselects
|
|
169
219
|
// We do nothing here
|