Page MenuHomeSealhub

No OneTemporary

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

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)

Event Timeline