Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10360491
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/policy-types/same-as-for-resource-in-field.subtest.ts b/src/app/policy-types/same-as-for-resource-in-field.subtest.ts
index 7c232336..6b98261c 100644
--- a/src/app/policy-types/same-as-for-resource-in-field.subtest.ts
+++ b/src/app/policy-types/same-as-for-resource-in-field.subtest.ts
@@ -1,373 +1,502 @@
import assert from "assert";
import Policy from "../../chip-types/policy";
import {
ActionName,
App,
Collection,
FieldTypes,
Policies,
Context,
} from "../../main";
import { assertThrowsAsync } from "../../test_utils/assert-throws-async";
import MockRestApi from "../../test_utils/rest-api";
import { TestAppType } from "../../test_utils/test-app";
import { withRunningApp } from "../../test_utils/with-test-app";
import Matches from "../base-chips/special_filters/matches";
const extend = (
policies: {
[action_name in ActionName]?: Policy;
}
) => (t: TestAppType) => {
const Numbers = new (class extends Collection {
name = "numbers";
fields = {
number: new FieldTypes.Int(),
};
named_filters = {
greater_than_1: new Matches("numbers", {
number: { ">": 1 },
}),
};
policies = policies;
})();
const NumberNotes = new (class extends Collection {
name = "number-notes";
fields = {
note: new FieldTypes.Text(),
number: new FieldTypes.SingleReference("numbers"),
};
policies = {
create: new Policies.SameAsForResourceInField({
action_name: "create",
field: "number",
collection_name: "number-notes",
}),
show: new Policies.SameAsForResourceInField({
action_name: "show",
field: "number",
collection_name: "number-notes",
}),
edit: new Policies.SameAsForResourceInField({
action_name: "edit",
field: "number",
collection_name: "number-notes",
}),
list: new Policies.SameAsForResourceInField({
action_name: "list",
field: "number",
collection_name: "number-notes",
}),
};
})();
return class extends t {
collections = {
...t.BaseCollections,
"number-notes": NumberNotes,
numbers: Numbers,
};
};
};
describe("SameAsForResourceInField", () => {
const sessions: { [username: string]: any } = {};
const numbers: number[] = [];
async function setup(app: App, rest_api: MockRestApi) {
numbers.splice(0, numbers.length); // to clear the array;
const password = "password";
for (const username of ["alice", "bob"]) {
await app.collections.users.suCreate({
username,
password,
email: `${username}@example.com`,
roles: [],
});
sessions[username] = await rest_api.login({
username,
password,
});
}
for (const n of [0, 1, 2]) {
numbers.push(
(
await rest_api.post(
"/api/v1/collections/numbers",
{
number: n,
},
sessions.alice
)
).id
);
}
}
async function post_number_notes(rest_api: MockRestApi, user: string) {
const notes = [];
for (const number of numbers) {
notes.push(
await rest_api.post(
"/api/v1/collections/number-notes",
{
note: `Lorem ipsum ${notes.length + 1}`,
number: number,
},
sessions[user]
)
);
}
return notes;
}
it("returns everything for number-notes referring to own numbers", () =>
withRunningApp(
extend({
create: new Policies.Public(),
show: new Policies.Owner(),
}),
async ({ app, rest_api }) => {
await setup(app, rest_api);
const posted_notes = await post_number_notes(rest_api, "alice");
const { items: got_notes } = await rest_api.get(
"/api/v1/collections/number-notes",
sessions.alice
);
assert.strictEqual(got_notes.length, posted_notes.length);
}
));
it("returns nothing for number-notes referring to other user's numbers", () =>
withRunningApp(
extend({
create: new Policies.Public(),
show: new Policies.Owner(),
list: new Policies.Owner(),
}),
async ({ app, rest_api }) => {
await setup(app, rest_api);
await post_number_notes(rest_api, "alice");
const { items: got_notes } = await rest_api.get(
"/api/v1/collections/number-notes",
sessions.bob
);
assert.strictEqual(got_notes.length, 0);
}
));
it("returns item for number-notes referring to numbers with complex access strategy", () =>
withRunningApp(
extend({
create: new Policies.LoggedIn(),
list: new Policies.Or([
new Policies.Owner(),
new Policies.If([
"numbers",
"greater_than_1",
Policies.Public,
]),
]),
}),
async ({ app, rest_api }) => {
await setup(app, rest_api);
await post_number_notes(rest_api, "alice");
const { items: got_notes } = await rest_api.get(
"/api/v1/collections/number-notes",
sessions.bob
);
assert.strictEqual(got_notes.length, 1);
}
));
it("doesn't allow to edit number-notes referring to other user's numbers", () =>
withRunningApp(
extend({
create: new Policies.LoggedIn(),
edit: new Policies.Owner(),
show: new Policies.Owner(),
}),
async ({ app, rest_api }) => {
await setup(app, rest_api);
const posted_notes = await post_number_notes(rest_api, "alice");
await assertThrowsAsync(() =>
rest_api.patch(
`/api/v1/collections/number-notes/${
posted_notes[0].id as string
}`,
{ note: "Lorem ipsumm" },
sessions.bob
)
);
}
));
it("works on reverse single reference fields", () =>
withRunningApp(
(t) =>
class extends t {
collections = {
...t.BaseCollections,
organizations: new (class extends Collection {
fields = {
name: new FieldTypes.Text(),
user_assignments: new FieldTypes.ReverseSingleReference(
{
referencing_collection:
"user_organizations",
referencing_field: "organization",
}
),
};
policies = {
list: new Policies.Or([
new Policies.SameAsForResourceInField({
collection_name: "organizations",
action_name: "show",
field: "user_assignments",
}),
new Policies.Roles(["admin"]),
]),
};
})(),
user_organizations: new (class extends Collection {
fields = {
user: new FieldTypes.SingleReference("users"),
organization: new FieldTypes.SingleReference(
"organizations"
),
};
policies = {
show: new Policies.UserReferencedInField(
"user"
),
};
})(),
projects: new (class extends Collection {
fields = {
name: new FieldTypes.Text(),
organization: new FieldTypes.SingleReference(
"organizations"
),
};
policies = {
list: new Policies.SameAsForResourceInField({
collection_name: "projects",
field: "organization",
action_name: "list",
}),
show: new Policies.SameAsForResourceInField({
collection_name: "projects",
field: "organization",
action_name: "list",
}),
edit: new Policies.SameAsForResourceInField({
collection_name: "projects",
field: "organization",
action_name: "list",
}),
};
})(),
};
},
async ({ app }) => {
const user1 = await app.collections.users.suCreate({
username: "user1",
email: "any@example.com",
password: "user1user1",
roles: null,
});
const user1_context = new Context(app, Date.now(), user1.id);
const user2 = await app.collections.users.suCreate({
username: "user2",
email: "any@example.com",
password: "user2user2",
roles: null,
});
const user2_context = new Context(app, Date.now(), user2.id);
const org1 = await app.collections.organizations.suCreate({
name: "org1",
});
await app.collections.user_organizations.suCreate({
user: user1.id,
organization: org1.id,
});
const org2 = await app.collections.organizations.suCreate({
name: "org2",
});
await app.collections.user_organizations.suCreate({
user: user2.id,
organization: org2.id,
});
const project = await app.collections.projects.suCreate({
organization: org1.id,
name: "project1",
});
assert.strictEqual(
(
await app.collections.organizations
.list(user1_context)
.fetch()
).items.length,
1
);
assert.strictEqual(
(
await app.collections.organizations
.list(user2_context)
.fetch()
).items.length,
1
);
const {
items: projects1,
} = await app.collections.projects.list(user1_context).fetch();
assert.strictEqual(projects1.length, 1);
assert.strictEqual(projects1[0].get("name"), "project1");
assert.strictEqual(projects1[0].id, project.id);
assert.strictEqual(projects1[0].get("organization"), org1.id);
assert.strictEqual(
(await app.collections.projects.list(user2_context).fetch())
.items.length,
0
);
await assertThrowsAsync(
async () =>
app.collections.projects.getByID(
user2_context,
project.id
),
(e) => {
assert.notStrictEqual(e, null);
}
);
await assertThrowsAsync(async () =>
(await app.collections.projects.suGetByID(project.id))
.set("name", "takeover")
.save(user2_context)
);
}
));
+
+ it("behaves properly for SingleReference field", async () =>
+ withRunningApp(
+ (test_app) =>
+ class extends test_app {
+ collections = {
+ ...App.BaseCollections,
+ organizations: new (class extends Collection {
+ fields = {
+ name: new FieldTypes.Text(),
+ user_assignments: new FieldTypes.ReverseSingleReference(
+ {
+ referencing_collection:
+ "user-organization",
+ referencing_field: "organization",
+ }
+ ),
+ };
+
+ defaultPolicy = new Policies.Roles(["admin"]);
+
+ policies = {
+ list: new Policies.Or([
+ new Policies.SameAsForResourceInField({
+ collection_name: "organizations",
+ action_name: "list",
+ field: "user_assignments",
+ }),
+ ]),
+ };
+ })(),
+ "user-organization": new (class UserOrganization extends Collection {
+ fields = {
+ organization: new FieldTypes.SingleReference(
+ "organizations"
+ ),
+ user: new FieldTypes.SingleReference("users"),
+ };
+
+ defaultPolicy = new Policies.Roles(["admin"]);
+
+ policies = {
+ list: new Policies.Or([
+ new Policies.UserReferencedInField("user"),
+ new Policies.Roles(["admin"]),
+ ]),
+ };
+ })(),
+ projects: new (class Projects extends Collection {
+ fields = {
+ name: FieldTypes.Required(
+ new FieldTypes.Text()
+ ),
+ latest_job: new FieldTypes.SingleReference(
+ "jobs"
+ ),
+ organization: new FieldTypes.SingleReference(
+ "organizations"
+ ),
+ };
+ policies = {
+ list: new Policies.SameAsForResourceInField({
+ collection_name: "projects",
+ field: "organization",
+ action_name: "list",
+ }),
+ create: new Policies.Roles(["admin"]),
+ edit: new Policies.Roles(["admin"]),
+ };
+ })(),
+ jobs: new (class Job extends Collection {
+ fields = {
+ project: new FieldTypes.SingleReference(
+ "projects"
+ ),
+ result: new FieldTypes.Text(),
+ };
+
+ policies = {
+ list: new Policies.SameAsForResourceInField({
+ collection_name: "jobs",
+ action_name: "list",
+ field: "project",
+ }),
+ create: new Policies.Super(),
+ edit: new Policies.Super(),
+ };
+ })(),
+ };
+ },
+ async ({ app }) => {
+ const user = await app.collections.users.suCreate({
+ username: "author",
+ email: "author@example.com",
+ password: "anyanyanyany",
+ roles: [],
+ });
+ const organization = await app.collections.organizations.suCreate(
+ { name: "org" }
+ );
+ await app.collections["user-organization"].suCreate({
+ user: user.id,
+ organization: organization.id,
+ });
+ const project = await app.collections.projects.suCreate({
+ name: "any",
+ organization: organization.id,
+ });
+ const job = await app.collections.jobs.suCreate({
+ project: project.id,
+ result: "I'm a job",
+ });
+ const invisible_project = await app.collections.projects.suCreate(
+ {
+ name: "invisible",
+ }
+ );
+ await app.collections.jobs.suCreate({
+ project: invisible_project.id,
+ result: "I'm an invisible job",
+ });
+ const {
+ items: user_visible_jobs,
+ } = await app.collections.jobs
+ .list(new Context(app, Date.now(), user.id))
+ .fetch();
+ assert.strictEqual(user_visible_jobs.length, 1);
+ }
+ ));
});
diff --git a/src/datastore/query-step.ts b/src/datastore/query-step.ts
index 50ad76f7..5d31c526 100644
--- a/src/datastore/query-step.ts
+++ b/src/datastore/query-step.ts
@@ -1,329 +1,335 @@
import object_hash from "object-hash";
import transformObject from "../utils/transform-object";
import negate_stage from "./negate-stage";
import QueryStage, { MatchBody } from "./query-stage";
export default abstract class QueryStep {
body: any;
hash(): string {
return QueryStep.hashBody(this.body);
}
static fromStage(
stage: QueryStage,
unwind = true,
rehash = false
): QueryStep[] {
if (stage.$lookup) {
const clonedStageBody = { ...stage.$lookup };
clonedStageBody.unwind = unwind;
return [Lookup.fromBody(clonedStageBody, rehash)];
} else if (stage.$match) {
return Object.keys(stage.$match).map(
(field) => new Match({ [field]: stage?.$match?.[field] })
);
} else if (stage.$group) {
return [new Group(stage.$group)];
} else if (stage.$unwind) {
return [new Unwind(stage.$unwind)];
}
throw new Error("Unsupported stage: " + JSON.stringify(stage));
}
static hashBody(body: any) {
return object_hash(body, {
algorithm: "md5",
excludeKeys: (key) => key === "as",
});
}
abstract getUsedFields(): string[];
abstract getCost(): number;
abstract negate(): QueryStep;
abstract prefix(prefix: string): QueryStep;
abstract toPipeline(): QueryStage[];
abstract renameField(old_name: string, new_name: string): void;
}
export class Match extends QueryStep {
body: MatchBody;
constructor(body: MatchBody) {
super();
this.body = body;
if (!body) {
throw new Error("no body!");
}
}
toPipeline(): [QueryStage] {
return [{ $match: this.body }];
}
pushStage(pipeline: QueryStage[]): QueryStage[] {
pipeline.push({ $match: this.body });
return pipeline;
}
getUsedFields(): string[] {
return getAllKeys(this.body)
.map((path) => path.split("."))
.reduce((acc, fields) =>
acc.concat(fields.filter((field) => !field.startsWith("$")))
);
}
getCost(): number {
return this.body.$or ? 2 : 0;
}
negate(): Match {
return new Match(negate_stage(this.body as QueryStage));
}
prefix(prefix: string): Match {
const prop_regex = /^[a-z0-9_]/;
const ret: MatchBody = {};
for (const [prop, value] of Object.entries(this.body)) {
const new_prop =
prop_regex.test(prop) && !Array.isArray(value)
? prefix + "." + prop
: prop;
if (prop == "$or" || prop == "$and" || prop == "$nor") {
const new_values = (value as MatchBody[]).map(
(match_body) => new Match(match_body).prefix(prefix).body
);
ret[new_prop] = new_values;
} else if (prop === "$in") {
ret[new_prop] = (value as string[]).map((v) =>
v.replace(/^\$/, "$" + prefix + ".")
);
} else if (value instanceof Object) {
ret[new_prop] = new Match(value as MatchBody).prefix(
prefix
).body;
} else {
if (typeof value === "string") {
ret[new_prop] = value.startsWith("$")
? value.replace("$", "$" + prefix + ".")
: value;
} else {
ret[new_prop] = value;
}
}
}
return new Match(ret);
}
renameField(old_name: string, new_name: string): void {
this.body = transformObject(
this.body,
(prop) => {
if (prop === old_name) {
return new_name;
}
if (prop.split(".")[0] === old_name) {
return [new_name, ...prop.split(".").slice(1)].join(".");
}
return prop;
},
(prop, value) => value
);
}
}
function getAllKeys(obj: any): string[] {
return Object.keys(obj).reduce((acc, key) => {
if (obj[key] instanceof Object) {
acc.push(...getAllKeys(obj[key]));
}
if (!Array.isArray(obj)) {
acc.push(key);
}
return acc;
}, [] as string[]);
}
export type SimpleLookupBodyInput = {
from: string;
localField: string;
foreignField: string;
unwind?: boolean;
as?: string;
};
export type SimpleLookupBody = SimpleLookupBodyInput & { as: string };
export type ComplexLookupBodyInput = {
from: string;
let: Record<string, string>;
pipeline: QueryStage[];
unwind?: boolean;
as?: string;
};
export type ComplexLookupBody = ComplexLookupBodyInput & { as: string };
export type LookupBody = ComplexLookupBody | SimpleLookupBody;
export type LookupBodyInput = ComplexLookupBodyInput | SimpleLookupBodyInput;
export abstract class Lookup extends QueryStep {
abstract getUsedFields(): string[];
body: LookupBody;
unwind: boolean;
constructor(
body: SimpleLookupBodyInput | ComplexLookupBodyInput,
rehash = false
) {
super();
let hash: string = body.as || Lookup.hashBody(body);
if (!body.as || rehash) {
hash = Lookup.hashBody(body);
}
this.body = {
...body,
as: hash,
};
this.unwind = body.unwind || false;
}
static hashBody(body: LookupBodyInput): string {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { as, ...rest } = body;
return QueryStep.hashBody(rest);
}
getCost(): number {
return 8;
}
negate(): QueryStep {
return this;
}
hash(): string {
if (!this.body.as) {
throw new Error(
"Cannot hash a lookup step without an `as` property"
);
}
return this.body.as;
}
static isComplexBody(
body: ComplexLookupBodyInput | SimpleLookupBodyInput
): body is ComplexLookupBodyInput {
return Object.prototype.hasOwnProperty.call(body, "let") as boolean;
}
static fromBody(
body: ComplexLookupBodyInput | SimpleLookupBodyInput,
rehash = false
): Lookup {
if (Lookup.isComplexBody(body)) {
return new ComplexLookup(body, rehash);
} else {
return new SimpleLookup(body, rehash);
}
}
toPipeline(): QueryStage[] {
const ret = { $lookup: { ...this.body } };
delete ret.$lookup.unwind;
return [
ret,
...(this.body.unwind ? [{ $unwind: `$${this.body.as}` }] : []),
];
}
renameField(old_name: string, new_name: string) {
Function.prototype(); //noop
}
}
export class SimpleLookup extends Lookup {
unwind: boolean;
body: SimpleLookupBody;
used_fields: string[];
getUsedFields(): string[] {
return this.body.localField.split(".");
}
prefix(prefix: string) {
- return this;
+ return new SimpleLookup({
+ from: this.body.from,
+ localField: `${prefix}.${this.body.localField}`,
+ foreignField: this.body.foreignField,
+ unwind: this.unwind,
+ as: `${prefix}.${this.body.as}`,
+ });
}
}
export class ComplexLookup extends Lookup {
body: ComplexLookupBody;
getUsedFields(): string[] {
return Object.values(this.body.let).map((entry) =>
entry.replace(/\$/g, "")
);
}
prefix(prefix: string): Lookup {
const ret = new ComplexLookup({
from: this.body.from,
let: Object.fromEntries(
Object.entries(this.body.let).map(([key, value]) => [
key,
value.replace("$", "$" + prefix + "."),
])
),
pipeline: this.body.pipeline,
// .map((stage) =>
// QueryStep.fromStage(stage).map((step) =>
// step.prefix(prefix)
// )
// )
// .reduce((acc, cur) => acc.concat(cur))
// .map((step) => step.toPipeline())
// .reduce((acc, cur) => acc.concat(cur)),
as: prefix + "." + this.body.as,
});
return ret;
}
}
abstract class SimpleQueryStep<T> {
abstract getStageName: () => string;
constructor(public body: T) {}
prefix(): SimpleQueryStep<T> {
return this;
}
getCost() {
return 2;
}
toPipeline(): QueryStage[] {
return [{ [this.getStageName()]: this.body }];
}
hash() {
return object_hash(this.body);
}
getUsedFields() {
return [];
}
negate() {
return this;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
renameField(_: string, __: string): void {}
}
export class Group extends SimpleQueryStep<{ _id: any; [key: string]: any }> {
getStageName = () => "$group";
}
export class Unwind extends SimpleQueryStep<string> {
getStageName = () => "$unwind";
renameField(old_name: string, new_name: string): void {
if ((this.body = "$" + old_name)) {
this.body = "$" + new_name;
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 8, 06:54 (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034248
Default Alt Text
(21 KB)
Attached To
Mode
rS Sealious
Attached
Detach File
Event Timeline
Log In to Comment