@rebasepro/server-mongo 0.17.3 → 0.18.1
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/LICENSE +0 -1
- package/README.md +4 -0
- package/dist/MongoBootstrapper.d.ts +0 -1
- package/dist/auth/ensure-collections.d.ts +0 -1
- package/dist/auth/services.d.ts +0 -1
- package/dist/connection.d.ts +0 -1
- package/dist/db/MongoConditionBuilder.d.ts +0 -1
- package/dist/db/MongoDataService.d.ts +0 -1
- package/dist/db/securityRuleFilter.d.ts +0 -1
- package/dist/factory.d.ts +0 -1
- package/dist/history/ensure-history-collection.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +83 -79
- package/dist/index.es.js.map +1 -1
- package/dist/schema/plan-schema-change.d.ts +0 -1
- package/dist/services/MongoDriver.d.ts +0 -1
- package/dist/services/MongoHistoryService.d.ts +0 -1
- package/dist/services/MongoRealtimeService.d.ts +0 -1
- package/dist/websocket.d.ts +0 -1
- package/package.json +28 -24
- package/dist/MongoBootstrapper.d.ts.map +0 -1
- package/dist/auth/ensure-collections.d.ts.map +0 -1
- package/dist/auth/services.d.ts.map +0 -1
- package/dist/connection.d.ts.map +0 -1
- package/dist/db/MongoConditionBuilder.d.ts.map +0 -1
- package/dist/db/MongoDataService.d.ts.map +0 -1
- package/dist/db/securityRuleFilter.d.ts.map +0 -1
- package/dist/factory.d.ts.map +0 -1
- package/dist/history/ensure-history-collection.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/schema/plan-schema-change.d.ts.map +0 -1
- package/dist/services/MongoDriver.d.ts.map +0 -1
- package/dist/services/MongoHistoryService.d.ts.map +0 -1
- package/dist/services/MongoRealtimeService.d.ts.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/src/MongoBootstrapper.ts +0 -204
- package/src/auth/ensure-collections.ts +0 -153
- package/src/auth/services.ts +0 -866
- package/src/connection.ts +0 -60
- package/src/db/MongoConditionBuilder.ts +0 -348
- package/src/db/MongoDataService.ts +0 -412
- package/src/db/securityRuleFilter.ts +0 -398
- package/src/factory.ts +0 -331
- package/src/history/ensure-history-collection.ts +0 -22
- package/src/index.ts +0 -25
- package/src/schema/plan-schema-change.ts +0 -159
- package/src/services/MongoDriver.ts +0 -950
- package/src/services/MongoHistoryService.ts +0 -186
- package/src/services/MongoRealtimeService.ts +0 -592
- package/src/websocket.ts +0 -387
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Row security for MongoDB.
|
|
3
|
-
*
|
|
4
|
-
* MongoDB has no RLS, so this driver enforces `securityRules` in-process. That
|
|
5
|
-
* makes the translation from a rule to a query the enforcement boundary, and it
|
|
6
|
-
* has exactly one safe failure mode: refuse.
|
|
7
|
-
*
|
|
8
|
-
* Two properties this file exists to hold:
|
|
9
|
-
*
|
|
10
|
-
* 1. **One predicate, one implementation.** The rules are compiled through the
|
|
11
|
-
* same {@link securityRuleToConditions} the Postgres DDL generator and the
|
|
12
|
-
* admin UI's `checkOperation` use, so "what does this rule mean" is answered
|
|
13
|
-
* in one place. The previous translator re-parsed the raw SQL itself and
|
|
14
|
-
* recognised four shapes — a second, smaller parser that disagreed with the
|
|
15
|
-
* first about the same rule.
|
|
16
|
-
* 2. **Fail closed, out loud.** An expression with no MongoDB equivalent (raw
|
|
17
|
-
* SQL, a membership subquery, a negated row predicate) used to become `{}` —
|
|
18
|
-
* "match every document". It now raises {@link SECURITY_RULE_UNSUPPORTED},
|
|
19
|
-
* the same shape the REST layer uses to refuse bulk writes this driver
|
|
20
|
-
* cannot perform: a request that cannot be authorized is not served.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { Document, Filter } from "mongodb";
|
|
24
|
-
import {
|
|
25
|
-
ANONYMOUS_USER_ID,
|
|
26
|
-
CollectionConfig,
|
|
27
|
-
PolicyExpression,
|
|
28
|
-
PolicyOperand,
|
|
29
|
-
SecurityOperation,
|
|
30
|
-
SecurityRule,
|
|
31
|
-
User,
|
|
32
|
-
isAnonymousUid
|
|
33
|
-
} from "@rebasepro/types";
|
|
34
|
-
import { securityRuleToConditions } from "@rebasepro/common";
|
|
35
|
-
import { ApiError } from "@rebasepro/server";
|
|
36
|
-
|
|
37
|
-
/** Matches every document. */
|
|
38
|
-
const MATCH_ALL: Filter<Document> = {};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Matches no document. A distinct object rather than a `false` sentinel so it
|
|
42
|
-
* can be nested inside `$and`/`$or` like any other filter; identity is what
|
|
43
|
-
* {@link isMatchNone} tests, so never mutate or copy it.
|
|
44
|
-
*/
|
|
45
|
-
const MATCH_NONE: Filter<Document> = { _id: { $exists: false } };
|
|
46
|
-
|
|
47
|
-
/** The expression has no MongoDB equivalent — the caller must refuse. */
|
|
48
|
-
const UNTRANSLATABLE = "untranslatable" as const;
|
|
49
|
-
|
|
50
|
-
type TranslationResult = Filter<Document> | typeof UNTRANSLATABLE;
|
|
51
|
-
|
|
52
|
-
function isMatchAll(f: TranslationResult): boolean {
|
|
53
|
-
return f !== UNTRANSLATABLE && f !== MATCH_NONE && Object.keys(f).length === 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isMatchNone(f: TranslationResult): boolean {
|
|
57
|
-
return f === MATCH_NONE;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The error code a caller sees when a rule cannot be honoured. */
|
|
61
|
-
export const SECURITY_RULE_UNSUPPORTED = "SECURITY_RULE_UNSUPPORTED";
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The refusal. Names the collection, the clause and the expression, because the
|
|
65
|
-
* only useful thing an operator can do with this is rewrite that rule — or move
|
|
66
|
-
* the collection to an engine that can enforce it.
|
|
67
|
-
*/
|
|
68
|
-
export function securityRuleUnsupported(
|
|
69
|
-
collectionSlug: string,
|
|
70
|
-
clause: "using" | "withCheck",
|
|
71
|
-
detail: string
|
|
72
|
-
): ApiError {
|
|
73
|
-
return ApiError.internal(
|
|
74
|
-
`This collection's data source (MongoDB) cannot enforce a security rule on "${collectionSlug}": ` +
|
|
75
|
-
`the \`${clause}\` expression ${detail} has no MongoDB equivalent. The request was refused rather ` +
|
|
76
|
-
"than served without row authorization. Express the rule with `access`, `ownerField`, `roles`, or a " +
|
|
77
|
-
"structured `condition`/`check`, or move this collection to a Postgres data source.",
|
|
78
|
-
SECURITY_RULE_UNSUPPORTED
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Describe an expression well enough for the refusal message to be actionable. */
|
|
83
|
-
function describe(expr: PolicyExpression): string {
|
|
84
|
-
switch (expr.kind) {
|
|
85
|
-
case "raw":
|
|
86
|
-
return `\`${expr.sql}\``;
|
|
87
|
-
case "existsIn":
|
|
88
|
-
return `a membership subquery over \`${expr.collection}\``;
|
|
89
|
-
case "not":
|
|
90
|
-
return "a negated row predicate";
|
|
91
|
-
default:
|
|
92
|
-
return `a \`${expr.kind}\` node`;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The first node of an expression tree this driver cannot translate, if any. */
|
|
97
|
-
function findUntranslatable(expr: PolicyExpression, hasRow: boolean): PolicyExpression | undefined {
|
|
98
|
-
switch (expr.kind) {
|
|
99
|
-
case "and":
|
|
100
|
-
case "or": {
|
|
101
|
-
for (const operand of expr.operands) {
|
|
102
|
-
const found = findUntranslatable(operand, hasRow);
|
|
103
|
-
if (found) return found;
|
|
104
|
-
}
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
case "not":
|
|
108
|
-
// Only decidable without the row when the operand is: negating a
|
|
109
|
-
// column predicate in MongoDB (`$nor`) also matches documents that
|
|
110
|
-
// lack the column, which SQL's three-valued logic would exclude.
|
|
111
|
-
return hasRow ? findUntranslatable(expr.operand, hasRow) : (referencesField(expr.operand) ? expr : undefined);
|
|
112
|
-
case "compare":
|
|
113
|
-
return operandUntranslatable(expr.left) || operandUntranslatable(expr.right) ? expr : undefined;
|
|
114
|
-
case "existsIn":
|
|
115
|
-
case "raw":
|
|
116
|
-
return expr;
|
|
117
|
-
default:
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function operandUntranslatable(operand: PolicyOperand): boolean {
|
|
123
|
-
return operand.kind === "outerField";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function referencesField(expr: PolicyExpression): boolean {
|
|
127
|
-
switch (expr.kind) {
|
|
128
|
-
case "and":
|
|
129
|
-
case "or":
|
|
130
|
-
return expr.operands.some(referencesField);
|
|
131
|
-
case "not":
|
|
132
|
-
return referencesField(expr.operand);
|
|
133
|
-
case "compare":
|
|
134
|
-
return expr.left.kind === "field" || expr.right.kind === "field" ||
|
|
135
|
-
expr.left.kind === "outerField" || expr.right.kind === "outerField";
|
|
136
|
-
default:
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The acting user, as the policy model sees them. */
|
|
142
|
-
interface PolicyUserContext {
|
|
143
|
-
uid: string;
|
|
144
|
-
roles: string[];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function userContext(user: User | undefined): PolicyUserContext {
|
|
148
|
-
// The sentinel, not an empty string: `rebase.uid()` is never NULL for a
|
|
149
|
-
// request that came from a client, and an `ownerField` rule compared
|
|
150
|
-
// against `undefined` would become `{ owner: undefined }` — which MongoDB
|
|
151
|
-
// reads as `{ owner: null }` and matches every document that has no owner.
|
|
152
|
-
return {
|
|
153
|
-
uid: user?.uid || ANONYMOUS_USER_ID,
|
|
154
|
-
roles: user?.roles ?? []
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const COMPARE_TO_MONGO = {
|
|
159
|
-
eq: "$eq",
|
|
160
|
-
neq: "$ne",
|
|
161
|
-
lt: "$lt",
|
|
162
|
-
lte: "$lte",
|
|
163
|
-
gt: "$gt",
|
|
164
|
-
gte: "$gte"
|
|
165
|
-
} as const;
|
|
166
|
-
|
|
167
|
-
const INVERTED_COMPARE = {
|
|
168
|
-
eq: "eq",
|
|
169
|
-
neq: "neq",
|
|
170
|
-
lt: "gt",
|
|
171
|
-
lte: "gte",
|
|
172
|
-
gt: "lt",
|
|
173
|
-
gte: "lte"
|
|
174
|
-
} as const;
|
|
175
|
-
|
|
176
|
-
type ResolvedOperand =
|
|
177
|
-
| { kind: "field"; name: string }
|
|
178
|
-
| { kind: "value"; value: unknown }
|
|
179
|
-
| { kind: "unknown" };
|
|
180
|
-
|
|
181
|
-
function resolveOperand(operand: PolicyOperand, ctx: PolicyUserContext): ResolvedOperand {
|
|
182
|
-
switch (operand.kind) {
|
|
183
|
-
case "literal":
|
|
184
|
-
return { kind: "value", value: operand.value };
|
|
185
|
-
case "authUid":
|
|
186
|
-
return { kind: "value", value: ctx.uid };
|
|
187
|
-
case "authRoles":
|
|
188
|
-
return { kind: "value", value: ctx.roles };
|
|
189
|
-
case "field":
|
|
190
|
-
return { kind: "field", name: operand.name };
|
|
191
|
-
case "outerField":
|
|
192
|
-
return { kind: "unknown" };
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Translate one {@link PolicyExpression} into a MongoDB filter, or
|
|
198
|
-
* {@link UNTRANSLATABLE}.
|
|
199
|
-
*
|
|
200
|
-
* The JavaScript twin of `evaluatePolicy`, one level up: where that decides a
|
|
201
|
-
* single row, this narrows a query. `"unknown"` there and `UNTRANSLATABLE` here
|
|
202
|
-
* are the same condition, and both are resolved fail-closed by their callers.
|
|
203
|
-
*/
|
|
204
|
-
export function policyToMongoFilter(expr: PolicyExpression, user: User | undefined): TranslationResult {
|
|
205
|
-
const ctx = userContext(user);
|
|
206
|
-
|
|
207
|
-
switch (expr.kind) {
|
|
208
|
-
case "true":
|
|
209
|
-
return MATCH_ALL;
|
|
210
|
-
case "false":
|
|
211
|
-
return MATCH_NONE;
|
|
212
|
-
case "and": {
|
|
213
|
-
const parts = expr.operands.map(o => policyToMongoFilter(o, user));
|
|
214
|
-
// Kleene AND: a `false` operand settles the conjunction even when a
|
|
215
|
-
// sibling is untranslatable, which is what keeps a role-scoped raw
|
|
216
|
-
// rule from refusing requests it does not even apply to.
|
|
217
|
-
if (parts.some(isMatchNone)) return MATCH_NONE;
|
|
218
|
-
if (parts.some(p => p === UNTRANSLATABLE)) return UNTRANSLATABLE;
|
|
219
|
-
const clauses = (parts as Filter<Document>[]).filter(p => !isMatchAll(p));
|
|
220
|
-
if (clauses.length === 0) return MATCH_ALL;
|
|
221
|
-
if (clauses.length === 1) return clauses[0];
|
|
222
|
-
return { $and: clauses } as Filter<Document>;
|
|
223
|
-
}
|
|
224
|
-
case "or": {
|
|
225
|
-
const parts = expr.operands.map(o => policyToMongoFilter(o, user));
|
|
226
|
-
if (parts.some(isMatchAll)) return MATCH_ALL;
|
|
227
|
-
if (parts.some(p => p === UNTRANSLATABLE)) return UNTRANSLATABLE;
|
|
228
|
-
const clauses = (parts as Filter<Document>[]).filter(p => !isMatchNone(p));
|
|
229
|
-
if (clauses.length === 0) return MATCH_NONE;
|
|
230
|
-
if (clauses.length === 1) return clauses[0];
|
|
231
|
-
return { $or: clauses } as Filter<Document>;
|
|
232
|
-
}
|
|
233
|
-
case "not": {
|
|
234
|
-
const inner = policyToMongoFilter(expr.operand, user);
|
|
235
|
-
// Constant-folded only. See `findUntranslatable` for why a negated
|
|
236
|
-
// column predicate is refused instead of becoming `$nor`.
|
|
237
|
-
if (isMatchAll(inner)) return MATCH_NONE;
|
|
238
|
-
if (isMatchNone(inner)) return MATCH_ALL;
|
|
239
|
-
return UNTRANSLATABLE;
|
|
240
|
-
}
|
|
241
|
-
case "compare": {
|
|
242
|
-
const left = resolveOperand(expr.left, ctx);
|
|
243
|
-
const right = resolveOperand(expr.right, ctx);
|
|
244
|
-
if (left.kind === "unknown" || right.kind === "unknown") return UNTRANSLATABLE;
|
|
245
|
-
|
|
246
|
-
if (left.kind === "field" && right.kind === "value") {
|
|
247
|
-
return { [left.name]: { [COMPARE_TO_MONGO[expr.op]]: right.value } } as Filter<Document>;
|
|
248
|
-
}
|
|
249
|
-
if (left.kind === "value" && right.kind === "field") {
|
|
250
|
-
return { [right.name]: { [COMPARE_TO_MONGO[INVERTED_COMPARE[expr.op]]]: left.value } } as Filter<Document>;
|
|
251
|
-
}
|
|
252
|
-
if (left.kind === "field" && right.kind === "field") {
|
|
253
|
-
return { $expr: { [COMPARE_TO_MONGO[expr.op]]: [`$${left.name}`, `$${right.name}`] } } as Filter<Document>;
|
|
254
|
-
}
|
|
255
|
-
// Both sides are known values — the comparison is a constant.
|
|
256
|
-
if (left.kind === "value" && right.kind === "value") {
|
|
257
|
-
return compareValues(expr.op, left.value, right.value);
|
|
258
|
-
}
|
|
259
|
-
return UNTRANSLATABLE;
|
|
260
|
-
}
|
|
261
|
-
case "rolesOverlap":
|
|
262
|
-
return expr.roles.some(r => r === "public" || ctx.roles.includes(r)) ? MATCH_ALL : MATCH_NONE;
|
|
263
|
-
case "rolesContain":
|
|
264
|
-
return expr.roles.every(r => r === "public" || ctx.roles.includes(r)) ? MATCH_ALL : MATCH_NONE;
|
|
265
|
-
case "authenticated":
|
|
266
|
-
return !isAnonymousUid(ctx.uid) ? MATCH_ALL : MATCH_NONE;
|
|
267
|
-
case "serverContext":
|
|
268
|
-
// A scoped driver is always acting for a user, never the server
|
|
269
|
-
// context — the same answer `evaluatePolicy` gives.
|
|
270
|
-
return MATCH_NONE;
|
|
271
|
-
case "existsIn":
|
|
272
|
-
case "raw":
|
|
273
|
-
return UNTRANSLATABLE;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function compareValues(op: keyof typeof COMPARE_TO_MONGO, a: unknown, b: unknown): TranslationResult {
|
|
278
|
-
if (op === "eq") return a === b ? MATCH_ALL : MATCH_NONE;
|
|
279
|
-
if (op === "neq") return a !== b ? MATCH_ALL : MATCH_NONE;
|
|
280
|
-
if ((typeof a === "string" && typeof b === "string") || (typeof a === "number" && typeof b === "number")) {
|
|
281
|
-
const decided = op === "lt" ? a < b : op === "lte" ? a <= b : op === "gt" ? a > b : a >= b;
|
|
282
|
-
return decided ? MATCH_ALL : MATCH_NONE;
|
|
283
|
-
}
|
|
284
|
-
return UNTRANSLATABLE;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** The rules that apply to `targetOperation`, mirroring `checkOperation`. */
|
|
288
|
-
function applicableRules(collection: CollectionConfig | undefined, targetOperation: SecurityOperation): SecurityRule[] {
|
|
289
|
-
const rules = collection?.securityRules;
|
|
290
|
-
if (!rules || rules.length === 0) return [];
|
|
291
|
-
return rules.filter((rule: SecurityRule) => {
|
|
292
|
-
const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
293
|
-
return ops.includes(targetOperation) || ops.includes("all");
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Which clause of a rule constrains `targetOperation` — Postgres's own split. */
|
|
298
|
-
function clauseFor(targetOperation: SecurityOperation): "using" | "withCheck" {
|
|
299
|
-
return targetOperation === "insert" ? "withCheck" : "using";
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Refuse up front when this collection's rules cannot be enforced for
|
|
304
|
-
* `targetOperation`.
|
|
305
|
-
*
|
|
306
|
-
* The row-in-hand paths (`fetchOne`, `save`, `delete`) resolve an undecidable
|
|
307
|
-
* rule through `checkOperation`'s `onUnknown: "deny"`, which is safe but
|
|
308
|
-
* indistinguishable from a plain "you may not do that". Calling this first
|
|
309
|
-
* turns the same condition into the refusal an operator can act on.
|
|
310
|
-
*/
|
|
311
|
-
export function assertSecurityRulesEnforceable(
|
|
312
|
-
collection: CollectionConfig | undefined,
|
|
313
|
-
targetOperation: SecurityOperation
|
|
314
|
-
): void {
|
|
315
|
-
for (const rule of applicableRules(collection, targetOperation)) {
|
|
316
|
-
const conditions = securityRuleToConditions(rule);
|
|
317
|
-
const clauses: ("using" | "withCheck")[] = targetOperation === "insert"
|
|
318
|
-
? ["withCheck"]
|
|
319
|
-
: targetOperation === "update" ? ["using", "withCheck"] : ["using"];
|
|
320
|
-
for (const clause of clauses) {
|
|
321
|
-
const expr = clause === "using" ? conditions.usingExpr : conditions.withCheckExpr;
|
|
322
|
-
if (!expr) continue;
|
|
323
|
-
// `hasRow: true` — these callers evaluate against a fetched row, so
|
|
324
|
-
// only the nodes no JavaScript evaluator can decide are refused.
|
|
325
|
-
const offending = findUntranslatable(expr, true);
|
|
326
|
-
if (offending) {
|
|
327
|
-
throw securityRuleUnsupported(collection?.slug ?? "unknown", clause, describe(offending));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Build the MongoDB filter that narrows a query to the rows `user` may see
|
|
335
|
-
* under `collection`'s security rules.
|
|
336
|
-
*
|
|
337
|
-
* Returns `null` when no row can qualify (the caller answers with an empty
|
|
338
|
-
* result), `{}` when the rules impose no narrowing, and throws
|
|
339
|
-
* {@link SECURITY_RULE_UNSUPPORTED} when a rule cannot be translated.
|
|
340
|
-
*/
|
|
341
|
-
export function buildMongoFilterFromSecurityRules<M extends Record<string, any>>(
|
|
342
|
-
collection: CollectionConfig<M> | undefined,
|
|
343
|
-
user: User | undefined,
|
|
344
|
-
targetOperation: SecurityOperation
|
|
345
|
-
): Filter<Document> | null {
|
|
346
|
-
const rules = applicableRules(collection as CollectionConfig | undefined, targetOperation);
|
|
347
|
-
if (!collection?.securityRules || collection.securityRules.length === 0) {
|
|
348
|
-
return MATCH_ALL;
|
|
349
|
-
}
|
|
350
|
-
// Rules exist but none covers this operation — Postgres denies, so do we.
|
|
351
|
-
if (rules.length === 0) return null;
|
|
352
|
-
|
|
353
|
-
const clause = clauseFor(targetOperation);
|
|
354
|
-
const permissive: Filter<Document>[] = [];
|
|
355
|
-
const restrictive: Filter<Document>[] = [];
|
|
356
|
-
|
|
357
|
-
for (const rule of rules) {
|
|
358
|
-
const conditions = securityRuleToConditions(rule);
|
|
359
|
-
const expr = clause === "using" ? conditions.usingExpr : conditions.withCheckExpr;
|
|
360
|
-
// A null clause denies, matching Postgres's `USING (false)`.
|
|
361
|
-
const filter = expr === null ? MATCH_NONE : policyToMongoFilter(expr, user);
|
|
362
|
-
if (filter === UNTRANSLATABLE) {
|
|
363
|
-
const offending = expr === null ? undefined : findUntranslatable(expr, false);
|
|
364
|
-
throw securityRuleUnsupported(
|
|
365
|
-
collection.slug,
|
|
366
|
-
clause,
|
|
367
|
-
offending ? describe(offending) : "this rule"
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
if ((rule.mode || "permissive") === "restrictive") {
|
|
371
|
-
restrictive.push(filter);
|
|
372
|
-
} else {
|
|
373
|
-
permissive.push(filter);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// No permissive rule can grant → nothing is visible, exactly as
|
|
378
|
-
// `checkOperation` returns false when `hasPermissive` is false.
|
|
379
|
-
if (permissive.length === 0) return null;
|
|
380
|
-
|
|
381
|
-
const parts: Filter<Document>[] = [];
|
|
382
|
-
if (!permissive.some(isMatchAll)) {
|
|
383
|
-
// A permissive rule that matches nothing contributes nothing to the
|
|
384
|
-
// union; if that is all of them, nothing is visible.
|
|
385
|
-
const granting = permissive.filter(p => !isMatchNone(p));
|
|
386
|
-
if (granting.length === 0) return null;
|
|
387
|
-
parts.push(granting.length === 1 ? granting[0] : ({ $or: granting } as Filter<Document>));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
for (const rf of restrictive) {
|
|
391
|
-
if (isMatchNone(rf)) return null;
|
|
392
|
-
if (!isMatchAll(rf)) parts.push(rf);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (parts.length === 0) return MATCH_ALL;
|
|
396
|
-
if (parts.length === 1) return parts[0];
|
|
397
|
-
return { $and: parts } as Filter<Document>;
|
|
398
|
-
}
|