@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 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
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "main": "dist/index.js",
4
4
  "types": "dist/index.d.ts",
5
5
  "module": "index.ts",
6
- "version": "0.0.9",
6
+ "version": "0.0.10",
7
7
  "scripts": {
8
8
  "compile": "tsc -p tsconfig.build.json"
9
9
  },