chore: Typescript database models (#2886)

closes #2798
This commit is contained in:
Tom Moor
2022-01-06 18:24:28 -08:00
committed by GitHub
parent d3cbf250e6
commit b20a341f0c
207 changed files with 5624 additions and 5315 deletions

View File

@@ -1,7 +1,5 @@
import { ApiKey, User, Team } from "@server/models";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createApiKey", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false;
@@ -9,6 +7,7 @@ allow(User, "createApiKey", Team, (user, team) => {
});
allow(User, ["read", "update", "delete"], ApiKey, (user, apiKey) => {
if (!apiKey) return false;
if (user.isViewer) return false;
return user && user.id === apiKey.userId;
});

View File

@@ -1,7 +1,5 @@
import { Attachment, User, Team } from "@server/models";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createAttachment", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false;

View File

@@ -1,8 +1,6 @@
import { AuthenticationProvider, User, Team } from "@server/models";
import { AdminRequiredError } from "../errors";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createAuthenticationProvider", Team, (actor, team) => {
if (!team || actor.teamId !== team.id) return false;
@@ -17,7 +15,7 @@ allow(
AuthenticationProvider,
(actor, authenticationProvider) =>
actor && actor.teamId === authenticationProvider.teamId
actor && actor.teamId === authenticationProvider?.teamId
);
allow(
@@ -26,7 +24,7 @@ allow(
AuthenticationProvider,
(actor, authenticationProvider) => {
if (actor.teamId !== authenticationProvider.teamId) return false;
if (actor.teamId !== authenticationProvider?.teamId) return false;
if (actor.isAdmin) return true;
throw AdminRequiredError();

13
server/policies/cancan.ts Normal file
View File

@@ -0,0 +1,13 @@
import CanCan from "cancan";
const cancan = new CanCan();
export const _can = cancan.can;
export const _authorize = cancan.authorize;
export const _cannot = cancan.cannot;
export const _abilities = cancan.abilities;
export const allow = cancan.allow;

View File

@@ -4,6 +4,7 @@ import { flushdb } from "@server/test/support";
import { serialize } from "./index";
beforeEach(() => flushdb());
describe("read_write permission", () => {
it("should allow read write permissions for team member", async () => {
const team = await buildTeam();
@@ -25,7 +26,7 @@ describe("read_write permission", () => {
const user = await buildUser({
teamId: team.id,
});
let collection = await buildCollection({
const collection = await buildCollection({
teamId: team.id,
permission: "read_write",
});
@@ -36,15 +37,16 @@ describe("read_write permission", () => {
permission: "read",
});
// reload to get membership
collection = await Collection.scope({
const reloaded = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collection.id);
const abilities = serialize(user, collection);
const abilities = serialize(user, reloaded);
expect(abilities.read).toEqual(true);
expect(abilities.update).toEqual(true);
expect(abilities.share).toEqual(true);
});
});
describe("read permission", () => {
it("should allow read permissions for team member", async () => {
const team = await buildTeam();
@@ -66,7 +68,7 @@ describe("read permission", () => {
const user = await buildUser({
teamId: team.id,
});
let collection = await buildCollection({
const collection = await buildCollection({
teamId: team.id,
permission: "read",
});
@@ -77,15 +79,16 @@ describe("read permission", () => {
permission: "read_write",
});
// reload to get membership
collection = await Collection.scope({
const reloaded = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collection.id);
const abilities = serialize(user, collection);
const abilities = serialize(user, reloaded);
expect(abilities.read).toEqual(true);
expect(abilities.update).toEqual(true);
expect(abilities.share).toEqual(true);
});
});
describe("no permission", () => {
it("should allow no permissions for team member", async () => {
const team = await buildTeam();
@@ -107,7 +110,7 @@ describe("no permission", () => {
const user = await buildUser({
teamId: team.id,
});
let collection = await buildCollection({
const collection = await buildCollection({
teamId: team.id,
permission: null,
});
@@ -118,10 +121,10 @@ describe("no permission", () => {
permission: "read_write",
});
// reload to get membership
collection = await Collection.scope({
const reloaded = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collection.id);
const abilities = serialize(user, collection);
const abilities = serialize(user, reloaded);
expect(abilities.read).toEqual(true);
expect(abilities.update).toEqual(true);
expect(abilities.share).toEqual(true);

View File

@@ -1,10 +1,8 @@
import invariant from "invariant";
import { concat, some } from "lodash";
import { some } from "lodash";
import { Collection, User, Team } from "@server/models";
import { AdminRequiredError } from "../errors";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createCollection", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false;
@@ -34,10 +32,10 @@ allow(User, "read", Collection, (user, collection) => {
collection.memberships,
"membership should be preloaded, did you forget withMembership scope?"
);
const allMemberships = concat(
collection.memberships,
collection.collectionGroupMemberships
);
const allMemberships = [
...collection.memberships,
...collection.collectionGroupMemberships,
];
return some(allMemberships, (m) =>
["read", "read_write", "maintainer"].includes(m.permission)
);
@@ -56,10 +54,10 @@ allow(User, "share", Collection, (user, collection) => {
collection.memberships,
"membership should be preloaded, did you forget withMembership scope?"
);
const allMemberships = concat(
collection.memberships,
collection.collectionGroupMemberships
);
const allMemberships = [
...collection.memberships,
...collection.collectionGroupMemberships,
];
return some(allMemberships, (m) =>
["read_write", "maintainer"].includes(m.permission)
);
@@ -77,10 +75,10 @@ allow(User, ["publish", "update"], Collection, (user, collection) => {
collection.memberships,
"membership should be preloaded, did you forget withMembership scope?"
);
const allMemberships = concat(
collection.memberships,
collection.collectionGroupMemberships
);
const allMemberships = [
...collection.memberships,
...collection.collectionGroupMemberships,
];
return some(allMemberships, (m) =>
["read_write", "maintainer"].includes(m.permission)
);
@@ -98,10 +96,10 @@ allow(User, "delete", Collection, (user, collection) => {
collection.memberships,
"membership should be preloaded, did you forget withMembership scope?"
);
const allMemberships = concat(
collection.memberships,
collection.collectionGroupMemberships
);
const allMemberships = [
...collection.memberships,
...collection.collectionGroupMemberships,
];
return some(allMemberships, (m) =>
["read_write", "maintainer"].includes(m.permission)
);

View File

@@ -8,6 +8,7 @@ import { flushdb } from "@server/test/support";
import { serialize } from "./index";
beforeEach(() => flushdb());
describe("read_write collection", () => {
it("should allow read write permissions for team member", async () => {
const team = await buildTeam();
@@ -33,6 +34,7 @@ describe("read_write collection", () => {
expect(abilities.move).toEqual(true);
});
});
describe("read collection", () => {
it("should allow read only permissions permissions for team member", async () => {
const team = await buildTeam();
@@ -58,6 +60,7 @@ describe("read collection", () => {
expect(abilities.move).toEqual(false);
});
});
describe("private collection", () => {
it("should allow no permissions for team member", async () => {
const team = await buildTeam();

View File

@@ -1,8 +1,7 @@
import invariant from "invariant";
import { Document, Revision, User, Team } from "@server/models";
import policy from "./policy";
const { allow, cannot } = policy;
import { NavigationNode } from "~/types";
import { allow, _cannot as cannot } from "./cancan";
allow(User, "createDocument", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false;
@@ -10,6 +9,8 @@ allow(User, "createDocument", Team, (user, team) => {
});
allow(User, ["read", "download"], Document, (user, document) => {
if (!document) return false;
// existence of collection option is not required here to account for share tokens
if (document.collection && cannot(user, "read", document.collection)) {
return false;
@@ -19,6 +20,7 @@ allow(User, ["read", "download"], Document, (user, document) => {
});
allow(User, ["star", "unstar"], Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
@@ -31,8 +33,13 @@ allow(User, ["star", "unstar"], Document, (user, document) => {
});
allow(User, "share", Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
invariant(
document.collection,
"collection is missing, did you forget to include in the query scope?"
);
if (cannot(user, "share", document.collection)) {
return false;
@@ -42,6 +49,7 @@ allow(User, "share", Document, (user, document) => {
});
allow(User, "update", Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
@@ -53,6 +61,7 @@ allow(User, "update", Document, (user, document) => {
});
allow(User, "createChildDocument", Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
@@ -66,6 +75,7 @@ allow(User, "createChildDocument", Document, (user, document) => {
});
allow(User, "move", Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (!document.publishedAt) return false;
@@ -78,6 +88,7 @@ allow(User, "move", Document, (user, document) => {
});
allow(User, ["pin", "unpin"], Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
@@ -91,6 +102,7 @@ allow(User, ["pin", "unpin"], Document, (user, document) => {
});
allow(User, ["pinToHome"], Document, (user, document) => {
if (!document) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
if (document.template) return false;
@@ -100,8 +112,9 @@ allow(User, ["pinToHome"], Document, (user, document) => {
});
allow(User, "delete", Document, (user, document) => {
if (user.isViewer) return false;
if (!document) return false;
if (document.deletedAt) return false;
if (user.isViewer) return false;
// allow deleting document without a collection
if (document.collection && cannot(user, "update", document.collection)) {
@@ -121,8 +134,9 @@ allow(User, "delete", Document, (user, document) => {
});
allow(User, "permanentDelete", Document, (user, document) => {
if (user.isViewer) return false;
if (!document) return false;
if (!document.deletedAt) return false;
if (user.isViewer) return false;
// allow deleting document without a collection
if (document.collection && cannot(user, "update", document.collection)) {
@@ -133,8 +147,9 @@ allow(User, "permanentDelete", Document, (user, document) => {
});
allow(User, "restore", Document, (user, document) => {
if (user.isViewer) return false;
if (!document) return false;
if (!document.deletedAt) return false;
if (user.isViewer) return false;
if (document.collection && cannot(user, "update", document.collection)) {
return false;
@@ -144,6 +159,7 @@ allow(User, "restore", Document, (user, document) => {
});
allow(User, "archive", Document, (user, document) => {
if (!document) return false;
if (!document.publishedAt) return false;
if (document.archivedAt) return false;
if (document.deletedAt) return false;
@@ -156,6 +172,7 @@ allow(User, "archive", Document, (user, document) => {
});
allow(User, "unarchive", Document, (user, document) => {
if (!document) return false;
invariant(
document.collection,
"collection is missing, did you forget to include in the query scope?"
@@ -170,10 +187,11 @@ allow(
Document,
"restore",
Revision,
(document, revision) => document.id === revision.documentId
(document, revision) => document.id === revision?.documentId
);
allow(User, "unpublish", Document, (user, document) => {
if (!document) return false;
invariant(
document.collection,
"collection is missing, did you forget to include in the query scope?"
@@ -183,16 +201,14 @@ allow(User, "unpublish", Document, (user, document) => {
if (cannot(user, "update", document.collection)) return false;
const documentID = document.id;
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documents' implicitly has an 'any' type... Remove this comment to see the full error message
const hasChild = (documents) =>
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
const hasChild = (documents: NavigationNode[]): boolean =>
documents.some((doc) => {
if (doc.id === documentID) return doc.children.length > 0;
return hasChild(doc.children);
});
return (
!hasChild(document.collection.documentStructure) &&
!hasChild(document.collection.documentStructure || []) &&
user.teamId === document.teamId
);
});

View File

@@ -1,8 +1,6 @@
import { Group, User, Team } from "@server/models";
import { AdminRequiredError } from "../errors";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createGroup", Team, (actor, team) => {
if (!team || actor.isViewer || actor.teamId !== team.id) return false;

View File

@@ -3,12 +3,14 @@ import { flushdb } from "@server/test/support";
import { serialize } from "./index";
beforeEach(() => flushdb());
it("should serialize policy", async () => {
const user = await buildUser();
const response = serialize(user, user);
expect(response.update).toEqual(true);
expect(response.delete).toEqual(true);
});
it("should serialize domain policies on Team", async () => {
const team = await buildTeam();
const user = await buildUser({

View File

@@ -6,7 +6,7 @@ import {
Document,
Group,
} from "@server/models";
import policy from "./policy";
import { _abilities, _can, _cannot, _authorize } from "./cancan";
import "./apiKey";
import "./attachment";
import "./authenticationProvider";
@@ -21,19 +21,26 @@ import "./user";
import "./team";
import "./group";
const { can, abilities } = policy;
type Policy = Record<string, boolean>;
// this should not be needed but is a workaround for this TypeScript issue:
// https://github.com/microsoft/TypeScript/issues/36931
export const authorize: typeof _authorize = _authorize;
export const can = _can;
export const cannot = _cannot;
export const abilities = _abilities;
/*
* Given a user and a model output an object which describes the actions the
* user may take against the model. This serialized policy is used for testing
* and sent in API responses to allow clients to adjust which UI is displayed.
*/
export function serialize(
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
model: User,
// @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message
target: Attachment | Team | Collection | Document | Group
target: Attachment | Team | Collection | Document | User | Group | null
): Policy {
const output = {};
abilities.forEach((ability) => {
@@ -51,5 +58,3 @@ export function serialize(
});
return output;
}
export default policy;

View File

@@ -1,8 +1,6 @@
import { Integration, User, Team } from "@server/models";
import { AdminRequiredError } from "../errors";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createIntegration", Team, (actor, team) => {
if (!team || actor.isViewer || actor.teamId !== team.id) return false;
@@ -15,7 +13,7 @@ allow(
User,
"read",
Integration,
(user, integration) => user.teamId === integration.teamId
(user, integration) => user.teamId === integration?.teamId
);
allow(User, ["update", "delete"], Integration, (user, integration) => {

View File

@@ -1,7 +1,5 @@
import { NotificationSetting, Team, User } from "@server/models";
import policy from "./policy";
const { allow } = policy;
import { allow } from "./cancan";
allow(User, "createNotificationSetting", Team, (user, team) => {
if (!team || user.teamId !== team.id) return false;
@@ -12,5 +10,5 @@ allow(
User,
["read", "update", "delete"],
NotificationSetting,
(user, setting) => user && user.id === setting.userId
(user, setting) => user && user.id === setting?.userId
);

View File

@@ -1,9 +1,9 @@
import { User, Pin } from "@server/models";
import policy from "./policy";
import { allow } from "./cancan";
const { allow } = policy;
allow(User, ["update", "delete"], Pin, (user, pin) => {
if (user.teamId === pin.teamId && user.isAdmin) return true;
return false;
});
allow(
User,
["update", "delete"],
Pin,
(user, pin) => user.teamId === pin?.teamId && user.isAdmin
);

View File

@@ -1,3 +0,0 @@
import CanCan from "cancan";
export default new CanCan();

View File

@@ -1,8 +1,9 @@
import { SearchQuery, User } from "@server/models";
import policy from "./policy";
import { allow } from "./cancan";
const { allow } = policy;
allow(User, ["read", "delete"], SearchQuery, (user, searchQuery) => {
return user && user.id === searchQuery.userId;
});
allow(
User,
["read", "delete"],
SearchQuery,
(user, searchQuery) => user && user.id === searchQuery?.userId
);

View File

@@ -1,14 +1,11 @@
import { Share, User } from "@server/models";
import { AdminRequiredError } from "../errors";
import policy from "./policy";
import { allow, _cannot as cannot } from "./cancan";
const { allow, cannot } = policy;
allow(User, "read", Share, (user, share) => {
return user.teamId === share.teamId;
});
allow(User, "read", Share, (user, share) => user.teamId === share?.teamId);
allow(User, "update", Share, (user, share) => {
if (!share) return false;
if (user.isViewer) return false;
// only the user who can share the document publicly can update the share.
@@ -17,8 +14,9 @@ allow(User, "update", Share, (user, share) => {
});
allow(User, "revoke", Share, (user, share) => {
if (!share) return false;
if (user.isViewer) return false;
if (!share || user.teamId !== share.teamId) return false;
if (user.teamId !== share.teamId) return false;
if (user.id === share.userId) return true;
if (user.isAdmin) return true;

View File

@@ -1,9 +1,7 @@
import { Team, User } from "@server/models";
import policy from "./policy";
import { allow } from "./cancan";
const { allow } = policy;
allow(User, "read", Team, (user, team) => team && user.teamId === team.id);
allow(User, "read", Team, (user, team) => user.teamId === team?.id);
allow(User, "share", Team, (user, team) => {
if (!team || user.isViewer || user.teamId !== team.id) return false;

View File

@@ -1,13 +1,11 @@
import { User, Team } from "@server/models";
import { AdminRequiredError } from "../errors";
import policy from "./policy";
import { allow } from "./cancan";
const { allow } = policy;
allow(
User,
"read",
User,
(actor, user) => user && user.teamId === actor.teamId
);