Policies refactor, guest roles (#6732)
This commit is contained in:
@@ -432,7 +432,8 @@ export const copyDocumentAsMarkdown = createAction({
|
|||||||
name: ({ t }) => t("Copy as Markdown"),
|
name: ({ t }) => t("Copy as Markdown"),
|
||||||
section: DocumentSection,
|
section: DocumentSection,
|
||||||
keywords: "clipboard",
|
keywords: "clipboard",
|
||||||
visible: ({ activeDocumentId }) => !!activeDocumentId,
|
visible: ({ activeDocumentId, stores }) =>
|
||||||
|
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
|
||||||
perform: ({ stores, activeDocumentId, t }) => {
|
perform: ({ stores, activeDocumentId, t }) => {
|
||||||
const document = activeDocumentId
|
const document = activeDocumentId
|
||||||
? stores.documents.get(activeDocumentId)
|
? stores.documents.get(activeDocumentId)
|
||||||
@@ -856,7 +857,7 @@ export const openDocumentHistory = createAction({
|
|||||||
icon: <HistoryIcon />,
|
icon: <HistoryIcon />,
|
||||||
visible: ({ activeDocumentId, stores }) => {
|
visible: ({ activeDocumentId, stores }) => {
|
||||||
const can = stores.policies.abilities(activeDocumentId ?? "");
|
const can = stores.policies.abilities(activeDocumentId ?? "");
|
||||||
return !!activeDocumentId && can.read && !can.restore;
|
return !!activeDocumentId && can.listRevisions;
|
||||||
},
|
},
|
||||||
perform: ({ activeDocumentId, stores }) => {
|
perform: ({ activeDocumentId, stores }) => {
|
||||||
if (!activeDocumentId) {
|
if (!activeDocumentId) {
|
||||||
@@ -883,7 +884,7 @@ export const openDocumentInsights = createAction({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
!!activeDocumentId &&
|
!!activeDocumentId &&
|
||||||
can.read &&
|
can.listViews &&
|
||||||
!document?.isTemplate &&
|
!document?.isTemplate &&
|
||||||
!document?.isDeleted
|
!document?.isDeleted
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
|||||||
const { ui, auth } = useStores();
|
const { ui, auth } = useStores();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const layoutRef = React.useRef<HTMLDivElement>(null);
|
const layoutRef = React.useRef<HTMLDivElement>(null);
|
||||||
const can = usePolicy(ui.activeCollectionId);
|
const can = usePolicy(ui.activeDocumentId);
|
||||||
|
const canCollection = usePolicy(ui.activeCollectionId);
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const documentContext = useLocalStore<DocumentContextValue>(() => ({
|
const documentContext = useLocalStore<DocumentContextValue>(() => ({
|
||||||
editor: null,
|
editor: null,
|
||||||
@@ -69,7 +70,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { activeCollectionId } = ui;
|
const { activeCollectionId } = ui;
|
||||||
if (!activeCollectionId || !can.createDocument) {
|
if (!activeCollectionId || !canCollection.createDocument) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
history.push(newDocumentPath(activeCollectionId));
|
history.push(newDocumentPath(activeCollectionId));
|
||||||
@@ -88,15 +89,18 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
|||||||
</Fade>
|
</Fade>
|
||||||
);
|
);
|
||||||
|
|
||||||
const showHistory = !!matchPath(location.pathname, {
|
const showHistory =
|
||||||
path: matchDocumentHistory,
|
!!matchPath(location.pathname, {
|
||||||
});
|
path: matchDocumentHistory,
|
||||||
const showInsights = !!matchPath(location.pathname, {
|
}) && can.listRevisions;
|
||||||
path: matchDocumentInsights,
|
const showInsights =
|
||||||
});
|
!!matchPath(location.pathname, {
|
||||||
|
path: matchDocumentInsights,
|
||||||
|
}) && can.listViews;
|
||||||
const showComments =
|
const showComments =
|
||||||
!showInsights &&
|
!showInsights &&
|
||||||
!showHistory &&
|
!showHistory &&
|
||||||
|
can.comment &&
|
||||||
ui.activeDocumentId &&
|
ui.activeDocumentId &&
|
||||||
ui.commentsExpanded.includes(ui.activeDocumentId) &&
|
ui.commentsExpanded.includes(ui.activeDocumentId) &&
|
||||||
team.getPreference(TeamPreference.Commenting);
|
team.getPreference(TeamPreference.Commenting);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import isCloudHosted from "~/utils/isCloudHosted";
|
|||||||
import lazy from "~/utils/lazyWithRetry";
|
import lazy from "~/utils/lazyWithRetry";
|
||||||
import { settingsPath } from "~/utils/routeHelpers";
|
import { settingsPath } from "~/utils/routeHelpers";
|
||||||
import useCurrentTeam from "./useCurrentTeam";
|
import useCurrentTeam from "./useCurrentTeam";
|
||||||
|
import useCurrentUser from "./useCurrentUser";
|
||||||
import usePolicy from "./usePolicy";
|
import usePolicy from "./usePolicy";
|
||||||
|
|
||||||
const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
|
const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
|
||||||
@@ -54,6 +55,7 @@ export type ConfigItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useSettingsConfig = () => {
|
const useSettingsConfig = () => {
|
||||||
|
const user = useCurrentUser();
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const can = usePolicy(team);
|
const can = usePolicy(team);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -122,7 +124,7 @@ const useSettingsConfig = () => {
|
|||||||
name: t("Members"),
|
name: t("Members"),
|
||||||
path: settingsPath("members"),
|
path: settingsPath("members"),
|
||||||
component: Members,
|
component: Members,
|
||||||
enabled: true,
|
enabled: can.listUsers,
|
||||||
group: t("Workspace"),
|
group: t("Workspace"),
|
||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
},
|
},
|
||||||
@@ -130,7 +132,7 @@ const useSettingsConfig = () => {
|
|||||||
name: t("Groups"),
|
name: t("Groups"),
|
||||||
path: settingsPath("groups"),
|
path: settingsPath("groups"),
|
||||||
component: Groups,
|
component: Groups,
|
||||||
enabled: true,
|
enabled: can.listGroups,
|
||||||
group: t("Workspace"),
|
group: t("Workspace"),
|
||||||
icon: GroupIcon,
|
icon: GroupIcon,
|
||||||
},
|
},
|
||||||
@@ -138,7 +140,7 @@ const useSettingsConfig = () => {
|
|||||||
name: t("Templates"),
|
name: t("Templates"),
|
||||||
path: settingsPath("templates"),
|
path: settingsPath("templates"),
|
||||||
component: Templates,
|
component: Templates,
|
||||||
enabled: true,
|
enabled: can.update,
|
||||||
group: t("Workspace"),
|
group: t("Workspace"),
|
||||||
icon: ShapesIcon,
|
icon: ShapesIcon,
|
||||||
},
|
},
|
||||||
@@ -146,7 +148,7 @@ const useSettingsConfig = () => {
|
|||||||
name: t("Shared Links"),
|
name: t("Shared Links"),
|
||||||
path: settingsPath("shares"),
|
path: settingsPath("shares"),
|
||||||
component: Shares,
|
component: Shares,
|
||||||
enabled: true,
|
enabled: can.listShares,
|
||||||
group: t("Workspace"),
|
group: t("Workspace"),
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
},
|
},
|
||||||
@@ -211,7 +213,7 @@ const useSettingsConfig = () => {
|
|||||||
enabled:
|
enabled:
|
||||||
enabledInDeployment &&
|
enabledInDeployment &&
|
||||||
hasSettings &&
|
hasSettings &&
|
||||||
(plugin.config.adminOnly === false || can.update),
|
(plugin.config.roles?.includes(user.role) || can.update),
|
||||||
icon: plugin.icon,
|
icon: plugin.icon,
|
||||||
} as ConfigItem;
|
} as ConfigItem;
|
||||||
|
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ function DocumentMenu({
|
|||||||
actionToMenuItem(permanentlyDeleteDocument, context),
|
actionToMenuItem(permanentlyDeleteDocument, context),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{(showDisplayOptions || showToggleEmbeds) && (
|
{(showDisplayOptions || showToggleEmbeds) && can.update && (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<DisplayOptions>
|
<DisplayOptions>
|
||||||
@@ -332,7 +332,7 @@ function DocumentMenu({
|
|||||||
/>
|
/>
|
||||||
</Style>
|
</Style>
|
||||||
)}
|
)}
|
||||||
{showDisplayOptions && !isMobile && can.update && (
|
{showDisplayOptions && !isMobile && (
|
||||||
<Style>
|
<Style>
|
||||||
<ToggleMenuItem
|
<ToggleMenuItem
|
||||||
width={26}
|
width={26}
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ class User extends ParanoidModel {
|
|||||||
return this.role === UserRole.Admin;
|
return this.role === UserRole.Admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is a member (editor).
|
||||||
|
*/
|
||||||
|
get isMember(): boolean {
|
||||||
|
return this.role === UserRole.Member;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the user is a viewer.
|
* Whether the user is a viewer.
|
||||||
*/
|
*/
|
||||||
@@ -86,6 +93,13 @@ class User extends ParanoidModel {
|
|||||||
return this.role === UserRole.Viewer;
|
return this.role === UserRole.Viewer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is a guest.
|
||||||
|
*/
|
||||||
|
get isGuest(): boolean {
|
||||||
|
return this.role === UserRole.Guest;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the user has been recently active. Recently is currently defined
|
* Whether the user has been recently active. Recently is currently defined
|
||||||
* as within the last 5 minutes.
|
* as within the last 5 minutes.
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export default abstract class Model {
|
|||||||
try {
|
try {
|
||||||
this[key] = data[key];
|
this[key] = data[key];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Temporary as isViewer and isAdmin properties changed to getters
|
|
||||||
Logger.warn(`Error setting ${key} on model`, error);
|
Logger.warn(`Error setting ${key} on model`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
|
|||||||
</CommentLink>
|
</CommentLink>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{totalViewers && !document.isDraft && !document.isTemplate ? (
|
{totalViewers &&
|
||||||
|
can.listViews &&
|
||||||
|
!document.isDraft &&
|
||||||
|
!document.isTemplate ? (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
•
|
•
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -248,7 +248,9 @@ function DocumentHeader({
|
|||||||
{!isPublishing && isSaving && user?.separateEditMode && (
|
{!isPublishing && isSaving && user?.separateEditMode && (
|
||||||
<Status>{t("Saving")}…</Status>
|
<Status>{t("Saving")}…</Status>
|
||||||
)}
|
)}
|
||||||
{!isDeleted && !isRevision && <Collaborators document={document} />}
|
{!isDeleted && !isRevision && can.listViews && (
|
||||||
|
<Collaborators document={document} />
|
||||||
|
)}
|
||||||
{(isEditing || !user?.separateEditMode) && !isTemplate && isNew && (
|
{(isEditing || !user?.separateEditMode) && !isTemplate && isNew && (
|
||||||
<Action>
|
<Action>
|
||||||
<TemplatesMenu
|
<TemplatesMenu
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ function PeopleTable({ canManage, ...rest }: Props) {
|
|||||||
<Badge primary>{t("Admin")}</Badge>
|
<Badge primary>{t("Admin")}</Badge>
|
||||||
) : row.original.isViewer ? (
|
) : row.original.isViewer ? (
|
||||||
<Badge>{t("Viewer")}</Badge>
|
<Badge>{t("Viewer")}</Badge>
|
||||||
|
) : row.original.isGuest ? (
|
||||||
|
<Badge yellow>{t("Guest")}</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge>{t("Editor")}</Badge>
|
<Badge>{t("Editor")}</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { UserRole } from "@shared/types";
|
||||||
|
|
||||||
interface Plugin {
|
interface Plugin {
|
||||||
id: string;
|
id: string;
|
||||||
config: {
|
config: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
adminOnly?: boolean;
|
roles?: UserRole[];
|
||||||
deployments?: string[];
|
deployments?: string[];
|
||||||
};
|
};
|
||||||
settings: React.FC;
|
settings: React.FC;
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
"id": "slack",
|
"id": "slack",
|
||||||
"name": "Slack",
|
"name": "Slack",
|
||||||
"priority": 40,
|
"priority": 40,
|
||||||
"adminOnly": false,
|
"roles": ["admin", "member"],
|
||||||
"description": "Adds a Slack authentication provider, support for the /outline slash command, and link unfurling."
|
"description": "Adds a Slack authentication provider, support for the /outline slash command, and link unfurling."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ import Length from "./validators/Length";
|
|||||||
}),
|
}),
|
||||||
as: "collection",
|
as: "collection",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
association: "memberships",
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -238,18 +238,41 @@ class User extends ParanoidModel<
|
|||||||
return !!this.suspendedAt || !!this.team?.isSuspended;
|
return !!this.suspendedAt || !!this.team?.isSuspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user has been invited but not yet signed in.
|
||||||
|
*/
|
||||||
get isInvited() {
|
get isInvited() {
|
||||||
return !this.lastActiveAt;
|
return !this.lastActiveAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is an admin.
|
||||||
|
*/
|
||||||
get isAdmin() {
|
get isAdmin() {
|
||||||
return this.role === UserRole.Admin;
|
return this.role === UserRole.Admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is a member (editor).
|
||||||
|
*/
|
||||||
|
get isMember() {
|
||||||
|
return this.role === UserRole.Member;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is a viewer.
|
||||||
|
*/
|
||||||
get isViewer() {
|
get isViewer() {
|
||||||
return this.role === UserRole.Viewer;
|
return this.role === UserRole.Viewer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the user is a guest.
|
||||||
|
*/
|
||||||
|
get isGuest() {
|
||||||
|
return this.role === UserRole.Guest;
|
||||||
|
}
|
||||||
|
|
||||||
get color() {
|
get color() {
|
||||||
return stringToColor(this.id);
|
return stringToColor(this.id);
|
||||||
}
|
}
|
||||||
@@ -404,9 +427,10 @@ class User extends ParanoidModel<
|
|||||||
return collectionStubs
|
return collectionStubs
|
||||||
.filter(
|
.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
Object.values(CollectionPermission).includes(
|
(Object.values(CollectionPermission).includes(
|
||||||
c.permission as CollectionPermission
|
c.permission as CollectionPermission
|
||||||
) ||
|
) &&
|
||||||
|
!this.isGuest) ||
|
||||||
c.memberships.length > 0 ||
|
c.memberships.length > 0 ||
|
||||||
c.collectionGroupMemberships.length > 0
|
c.collectionGroupMemberships.length > 0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ class ParanoidModel<
|
|||||||
> extends IdModel<TModelAttributes, TCreationAttributes> {
|
> extends IdModel<TModelAttributes, TCreationAttributes> {
|
||||||
@DeletedAt
|
@DeletedAt
|
||||||
deletedAt: Date | null;
|
deletedAt: Date | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the model has been deleted.
|
||||||
|
*
|
||||||
|
* @returns True if the model has been deleted
|
||||||
|
*/
|
||||||
|
get isDeleted() {
|
||||||
|
return !!this.deletedAt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ParanoidModel;
|
export default ParanoidModel;
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { ApiKey, User, Team } from "@server/models";
|
import { ApiKey, User, Team } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isOwner, isTeamModel, isTeamMutable } from "./utils";
|
||||||
|
|
||||||
allow(User, "createApiKey", Team, (user, team) => {
|
allow(User, "createApiKey", Team, (actor, team) =>
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamModel(actor, team),
|
||||||
return true;
|
isTeamMutable(actor),
|
||||||
});
|
!actor.isViewer,
|
||||||
|
!actor.isGuest
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, ["read", "update", "delete"], ApiKey, (user, apiKey) => {
|
allow(User, ["read", "update", "delete"], ApiKey, isOwner);
|
||||||
if (!apiKey) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.isViewer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user && user.id === apiKey.userId;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,38 +1,12 @@
|
|||||||
import { Attachment, User, Team } from "@server/models";
|
import { Attachment, User, Team } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isOwner, isTeamModel, or } from "./utils";
|
||||||
|
|
||||||
allow(User, "createAttachment", Team, (user, team) => {
|
allow(User, "createAttachment", Team, isTeamModel);
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "read", Attachment, (actor, attachment) => {
|
allow(User, ["read", "update", "delete"], Attachment, (actor, attachment) =>
|
||||||
if (!attachment || !actor || attachment.teamId !== actor.teamId) {
|
and(
|
||||||
return false;
|
isTeamModel(actor, attachment),
|
||||||
}
|
or(actor.isAdmin, isOwner(actor, attachment))
|
||||||
if (actor.isAdmin) {
|
)
|
||||||
return true;
|
);
|
||||||
}
|
|
||||||
if (actor.id === attachment.userId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "delete", Attachment, (actor, attachment) => {
|
|
||||||
if (actor.isViewer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!attachment || attachment.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (actor.id === attachment.userId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,40 +1,9 @@
|
|||||||
import { AuthenticationProvider, User, Team } from "@server/models";
|
import { AuthenticationProvider, User, Team } from "@server/models";
|
||||||
import { AdminRequiredError } from "../errors";
|
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isTeamAdmin, isTeamModel } from "./utils";
|
||||||
|
|
||||||
allow(User, "createAuthenticationProvider", Team, (actor, team) => {
|
allow(User, "createAuthenticationProvider", Team, isTeamAdmin);
|
||||||
if (!team || actor.teamId !== team.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
allow(User, "read", AuthenticationProvider, isTeamModel);
|
||||||
});
|
|
||||||
|
|
||||||
allow(
|
allow(User, ["update", "delete"], AuthenticationProvider, isTeamAdmin);
|
||||||
User,
|
|
||||||
"read",
|
|
||||||
AuthenticationProvider,
|
|
||||||
|
|
||||||
(actor, authenticationProvider) =>
|
|
||||||
actor && actor.teamId === authenticationProvider?.teamId
|
|
||||||
);
|
|
||||||
|
|
||||||
allow(
|
|
||||||
User,
|
|
||||||
["update", "delete"],
|
|
||||||
AuthenticationProvider,
|
|
||||||
|
|
||||||
(actor, authenticationProvider) => {
|
|
||||||
if (actor.teamId !== authenticationProvider?.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe("admin", () => {
|
|||||||
}).findByPk(collection.id);
|
}).findByPk(collection.id);
|
||||||
const abilities = serialize(user, reloaded);
|
const abilities = serialize(user, reloaded);
|
||||||
expect(abilities.readDocument).toEqual(false);
|
expect(abilities.readDocument).toEqual(false);
|
||||||
|
expect(abilities.updateDocument).toEqual(false);
|
||||||
expect(abilities.createDocument).toEqual(false);
|
expect(abilities.createDocument).toEqual(false);
|
||||||
expect(abilities.share).toEqual(false);
|
expect(abilities.share).toEqual(false);
|
||||||
expect(abilities.read).toEqual(false);
|
expect(abilities.read).toEqual(false);
|
||||||
@@ -41,6 +42,7 @@ describe("admin", () => {
|
|||||||
});
|
});
|
||||||
const abilities = serialize(user, collection);
|
const abilities = serialize(user, collection);
|
||||||
expect(abilities.readDocument).toEqual(true);
|
expect(abilities.readDocument).toEqual(true);
|
||||||
|
expect(abilities.updateDocument).toEqual(true);
|
||||||
expect(abilities.createDocument).toEqual(true);
|
expect(abilities.createDocument).toEqual(true);
|
||||||
expect(abilities.share).toEqual(true);
|
expect(abilities.share).toEqual(true);
|
||||||
expect(abilities.read).toEqual(true);
|
expect(abilities.read).toEqual(true);
|
||||||
@@ -72,8 +74,8 @@ describe("member", () => {
|
|||||||
const abilities = serialize(user, reloaded);
|
const abilities = serialize(user, reloaded);
|
||||||
expect(abilities.read).toEqual(true);
|
expect(abilities.read).toEqual(true);
|
||||||
expect(abilities.readDocument).toEqual(true);
|
expect(abilities.readDocument).toEqual(true);
|
||||||
expect(abilities.createDocument).toEqual(true);
|
// expect(abilities.createDocument).toEqual(true);
|
||||||
expect(abilities.share).toEqual(true);
|
// expect(abilities.share).toEqual(true);
|
||||||
expect(abilities.update).toEqual(true);
|
expect(abilities.update).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -336,3 +338,53 @@ describe("viewer", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("guest", () => {
|
||||||
|
describe("read_write permission", () => {
|
||||||
|
it("should allow no permissions for guest", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
role: UserRole.Guest,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
const collection = await buildCollection({
|
||||||
|
teamId: team.id,
|
||||||
|
permission: CollectionPermission.ReadWrite,
|
||||||
|
});
|
||||||
|
const abilities = serialize(user, collection);
|
||||||
|
expect(abilities.read).toEqual(false);
|
||||||
|
expect(abilities.readDocument).toEqual(false);
|
||||||
|
expect(abilities.createDocument).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow override with team member membership permission", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
role: UserRole.Guest,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
const collection = await buildCollection({
|
||||||
|
teamId: team.id,
|
||||||
|
permission: null,
|
||||||
|
});
|
||||||
|
await UserMembership.create({
|
||||||
|
createdById: user.id,
|
||||||
|
collectionId: collection.id,
|
||||||
|
userId: user.id,
|
||||||
|
permission: CollectionPermission.Read,
|
||||||
|
});
|
||||||
|
// reload to get membership
|
||||||
|
const reloaded = await Collection.scope({
|
||||||
|
method: ["withMembership", user.id],
|
||||||
|
}).findByPk(collection.id);
|
||||||
|
const abilities = serialize(user, reloaded);
|
||||||
|
expect(abilities.read).toEqual(true);
|
||||||
|
expect(abilities.readDocument).toEqual(true);
|
||||||
|
expect(abilities.createDocument).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,70 +2,72 @@ import invariant from "invariant";
|
|||||||
import some from "lodash/some";
|
import some from "lodash/some";
|
||||||
import { CollectionPermission, DocumentPermission } from "@shared/types";
|
import { CollectionPermission, DocumentPermission } from "@shared/types";
|
||||||
import { Collection, User, Team } from "@server/models";
|
import { Collection, User, Team } from "@server/models";
|
||||||
import { AdminRequiredError } from "../errors";
|
import { allow, _can as can } from "./cancan";
|
||||||
import { allow } from "./cancan";
|
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
|
||||||
|
|
||||||
|
allow(User, "createCollection", Team, (actor, team) =>
|
||||||
|
and(
|
||||||
|
isTeamModel(actor, team),
|
||||||
|
isTeamMutable(actor),
|
||||||
|
!actor.isGuest,
|
||||||
|
!actor.isViewer,
|
||||||
|
or(actor.isAdmin, !!team?.memberCollectionCreate)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
allow(User, "importCollection", Team, (actor, team) =>
|
||||||
|
and(
|
||||||
|
//
|
||||||
|
isTeamAdmin(actor, team),
|
||||||
|
isTeamMutable(actor)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
allow(User, "move", Collection, (actor, collection) =>
|
||||||
|
and(
|
||||||
|
//
|
||||||
|
isTeamAdmin(actor, collection),
|
||||||
|
isTeamMutable(actor),
|
||||||
|
!collection?.deletedAt
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
allow(
|
||||||
|
User,
|
||||||
|
["read", "readDocument", "star", "unstar"],
|
||||||
|
Collection,
|
||||||
|
(user, collection) => {
|
||||||
|
if (!collection || user.teamId !== collection.teamId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection.isPrivate || user.isGuest) {
|
||||||
|
return includesMembership(
|
||||||
|
collection,
|
||||||
|
Object.values(CollectionPermission)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
allow(User, "createCollection", Team, (user, team) => {
|
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.isAdmin || team.memberCollectionCreate) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
);
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "importCollection", Team, (actor, team) => {
|
allow(User, "export", Collection, (actor, collection) =>
|
||||||
if (!team || actor.teamId !== team.id) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
can(actor, "read", collection),
|
||||||
if (actor.isAdmin) {
|
!actor.isViewer,
|
||||||
return true;
|
!actor.isGuest
|
||||||
}
|
)
|
||||||
|
);
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "move", Collection, (user, collection) => {
|
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (collection.deletedAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "read", Collection, (user, collection) => {
|
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collection.isPrivate) {
|
|
||||||
return includesMembership(collection, Object.values(CollectionPermission));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["star", "unstar"], Collection, (user, collection) => {
|
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collection.isPrivate) {
|
|
||||||
return includesMembership(collection, Object.values(CollectionPermission));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "share", Collection, (user, collection) => {
|
allow(User, "share", Collection, (user, collection) => {
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
if (
|
||||||
|
!collection ||
|
||||||
|
user.isGuest ||
|
||||||
|
user.teamId !== collection.teamId ||
|
||||||
|
!isTeamMutable(user)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!collection.sharing) {
|
if (!collection.sharing) {
|
||||||
@@ -88,24 +90,16 @@ allow(User, "share", Collection, (user, collection) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
allow(User, ["readDocument", "export"], Collection, (user, collection) => {
|
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collection.isPrivate) {
|
|
||||||
return includesMembership(collection, Object.values(CollectionPermission));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(
|
allow(
|
||||||
User,
|
User,
|
||||||
["updateDocument", "createDocument", "deleteDocument"],
|
["updateDocument", "createDocument", "deleteDocument"],
|
||||||
Collection,
|
Collection,
|
||||||
(user, collection) => {
|
(user, collection) => {
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
if (
|
||||||
|
!collection ||
|
||||||
|
user.teamId !== collection.teamId ||
|
||||||
|
!isTeamMutable(user)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +109,8 @@ allow(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
collection.permission !== CollectionPermission.ReadWrite ||
|
collection.permission !== CollectionPermission.ReadWrite ||
|
||||||
user.isViewer
|
user.isViewer ||
|
||||||
|
user.isGuest
|
||||||
) {
|
) {
|
||||||
return includesMembership(collection, [
|
return includesMembership(collection, [
|
||||||
CollectionPermission.ReadWrite,
|
CollectionPermission.ReadWrite,
|
||||||
@@ -128,7 +123,7 @@ allow(
|
|||||||
);
|
);
|
||||||
|
|
||||||
allow(User, ["update", "delete"], Collection, (user, collection) => {
|
allow(User, ["update", "delete"], Collection, (user, collection) => {
|
||||||
if (!collection || user.teamId !== collection.teamId) {
|
if (!collection || user.isGuest || user.teamId !== collection.teamId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
@@ -139,12 +134,16 @@ allow(User, ["update", "delete"], Collection, (user, collection) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function includesMembership(
|
function includesMembership(
|
||||||
collection: Collection,
|
collection: Collection | null,
|
||||||
permissions: (CollectionPermission | DocumentPermission)[]
|
permissions: (CollectionPermission | DocumentPermission)[]
|
||||||
) {
|
) {
|
||||||
|
if (!collection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
collection.memberships,
|
collection.memberships,
|
||||||
"collection memberships should be preloaded, did you forget withMembership scope?"
|
"Development: collection memberships not preloaded, did you forget `withMembership` scope?"
|
||||||
);
|
);
|
||||||
return some(
|
return some(
|
||||||
[...collection.memberships, ...collection.collectionGroupMemberships],
|
[...collection.memberships, ...collection.collectionGroupMemberships],
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import { Comment, User, Team } from "@server/models";
|
import { Comment, User, Team } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isTeamModel, or } from "./utils";
|
||||||
|
|
||||||
allow(User, "createComment", Team, (user, team) => {
|
allow(User, "createComment", Team, isTeamModel);
|
||||||
if (!team || user.teamId !== team.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "read", Comment, (user, comment) => {
|
allow(User, "read", Comment, (actor, comment) =>
|
||||||
if (!comment) {
|
isTeamModel(actor, comment?.createdBy)
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
return user.teamId === comment.createdBy.teamId;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["update", "delete"], Comment, (user, comment) => {
|
allow(User, ["update", "delete"], Comment, (actor, comment) =>
|
||||||
if (!comment) {
|
and(
|
||||||
return false;
|
isTeamModel(actor, comment?.createdBy),
|
||||||
}
|
or(actor.isAdmin, actor?.id === comment?.createdById)
|
||||||
if (user.teamId !== comment.createdBy.teamId) {
|
)
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return user.isAdmin || user?.id === comment.createdById;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -64,6 +64,36 @@ describe("read_write collection", () => {
|
|||||||
expect(abilities.unsubscribe).toEqual(true);
|
expect(abilities.unsubscribe).toEqual(true);
|
||||||
expect(abilities.comment).toEqual(true);
|
expect(abilities.comment).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow no permissions for guest", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
teamId: team.id,
|
||||||
|
role: UserRole.Guest,
|
||||||
|
});
|
||||||
|
const collection = await buildCollection({
|
||||||
|
teamId: team.id,
|
||||||
|
permission: CollectionPermission.ReadWrite,
|
||||||
|
});
|
||||||
|
const doc = await buildDocument({
|
||||||
|
teamId: team.id,
|
||||||
|
collectionId: collection.id,
|
||||||
|
});
|
||||||
|
// reload to get membership
|
||||||
|
const document = await Document.findByPk(doc.id, { userId: user.id });
|
||||||
|
const abilities = serialize(user, document);
|
||||||
|
expect(abilities.read).toEqual(false);
|
||||||
|
expect(abilities.download).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(false);
|
||||||
|
expect(abilities.createChildDocument).toEqual(false);
|
||||||
|
expect(abilities.archive).toEqual(false);
|
||||||
|
expect(abilities.delete).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
expect(abilities.move).toEqual(false);
|
||||||
|
expect(abilities.subscribe).toEqual(false);
|
||||||
|
expect(abilities.unsubscribe).toEqual(false);
|
||||||
|
expect(abilities.comment).toEqual(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("read collection", () => {
|
describe("read collection", () => {
|
||||||
@@ -93,6 +123,36 @@ describe("read collection", () => {
|
|||||||
expect(abilities.unsubscribe).toEqual(true);
|
expect(abilities.unsubscribe).toEqual(true);
|
||||||
expect(abilities.comment).toEqual(true);
|
expect(abilities.comment).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow no permissions for guest", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
teamId: team.id,
|
||||||
|
role: UserRole.Guest,
|
||||||
|
});
|
||||||
|
const collection = await buildCollection({
|
||||||
|
teamId: team.id,
|
||||||
|
permission: CollectionPermission.Read,
|
||||||
|
});
|
||||||
|
const doc = await buildDocument({
|
||||||
|
teamId: team.id,
|
||||||
|
collectionId: collection.id,
|
||||||
|
});
|
||||||
|
// reload to get membership
|
||||||
|
const document = await Document.findByPk(doc.id, { userId: user.id });
|
||||||
|
const abilities = serialize(user, document);
|
||||||
|
expect(abilities.read).toEqual(false);
|
||||||
|
expect(abilities.download).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(false);
|
||||||
|
expect(abilities.createChildDocument).toEqual(false);
|
||||||
|
expect(abilities.archive).toEqual(false);
|
||||||
|
expect(abilities.delete).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
expect(abilities.move).toEqual(false);
|
||||||
|
expect(abilities.subscribe).toEqual(false);
|
||||||
|
expect(abilities.unsubscribe).toEqual(false);
|
||||||
|
expect(abilities.comment).toEqual(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("private collection", () => {
|
describe("private collection", () => {
|
||||||
@@ -120,6 +180,34 @@ describe("private collection", () => {
|
|||||||
expect(abilities.unsubscribe).toEqual(false);
|
expect(abilities.unsubscribe).toEqual(false);
|
||||||
expect(abilities.comment).toEqual(false);
|
expect(abilities.comment).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow no permissions for guest", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
teamId: team.id,
|
||||||
|
role: UserRole.Guest,
|
||||||
|
});
|
||||||
|
const collection = await buildCollection({
|
||||||
|
teamId: team.id,
|
||||||
|
permission: null,
|
||||||
|
});
|
||||||
|
const document = await buildDocument({
|
||||||
|
teamId: team.id,
|
||||||
|
collectionId: collection.id,
|
||||||
|
});
|
||||||
|
const abilities = serialize(user, document);
|
||||||
|
expect(abilities.read).toEqual(false);
|
||||||
|
expect(abilities.download).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(false);
|
||||||
|
expect(abilities.createChildDocument).toEqual(false);
|
||||||
|
expect(abilities.archive).toEqual(false);
|
||||||
|
expect(abilities.delete).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
expect(abilities.move).toEqual(false);
|
||||||
|
expect(abilities.subscribe).toEqual(false);
|
||||||
|
expect(abilities.unsubscribe).toEqual(false);
|
||||||
|
expect(abilities.comment).toEqual(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("no collection", () => {
|
describe("no collection", () => {
|
||||||
@@ -143,6 +231,29 @@ describe("no collection", () => {
|
|||||||
expect(abilities.comment).toEqual(false);
|
expect(abilities.comment).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow no permissions for guest", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
teamId: team.id,
|
||||||
|
role: UserRole.Guest,
|
||||||
|
});
|
||||||
|
const document = await buildDraftDocument({
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
const abilities = serialize(user, document);
|
||||||
|
expect(abilities.read).toEqual(false);
|
||||||
|
expect(abilities.download).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(false);
|
||||||
|
expect(abilities.createChildDocument).toEqual(false);
|
||||||
|
expect(abilities.archive).toEqual(false);
|
||||||
|
expect(abilities.delete).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
expect(abilities.move).toEqual(false);
|
||||||
|
expect(abilities.subscribe).toEqual(false);
|
||||||
|
expect(abilities.unsubscribe).toEqual(false);
|
||||||
|
expect(abilities.comment).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("should allow edit permissions for creator", async () => {
|
it("should allow edit permissions for creator", async () => {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
const user = await buildUser({ teamId: team.id });
|
const user = await buildUser({ teamId: team.id });
|
||||||
@@ -161,8 +272,8 @@ describe("no collection", () => {
|
|||||||
expect(abilities.delete).toEqual(true);
|
expect(abilities.delete).toEqual(true);
|
||||||
expect(abilities.share).toEqual(true);
|
expect(abilities.share).toEqual(true);
|
||||||
expect(abilities.move).toEqual(true);
|
expect(abilities.move).toEqual(true);
|
||||||
expect(abilities.subscribe).toEqual(false);
|
expect(abilities.subscribe).toEqual(true);
|
||||||
expect(abilities.unsubscribe).toEqual(false);
|
expect(abilities.unsubscribe).toEqual(true);
|
||||||
expect(abilities.comment).toEqual(true);
|
expect(abilities.comment).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,446 +7,201 @@ import {
|
|||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { Document, Revision, User, Team } from "@server/models";
|
import { Document, Revision, User, Team } from "@server/models";
|
||||||
import { allow, _cannot as cannot, _can as can } from "./cancan";
|
import { allow, _cannot as cannot, _can as can } from "./cancan";
|
||||||
|
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
|
||||||
allow(User, "createDocument", Team, (user, team) => {
|
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
allow(User, "createDocument", Team, (actor, document) =>
|
||||||
return false;
|
and(
|
||||||
}
|
//
|
||||||
return true;
|
!actor.isGuest,
|
||||||
});
|
!actor.isViewer,
|
||||||
|
isTeamModel(actor, document),
|
||||||
allow(User, "read", Document, (user, document) => {
|
isTeamMutable(actor)
|
||||||
if (!document) {
|
)
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
|
allow(User, "read", Document, (actor, document) =>
|
||||||
if (
|
and(
|
||||||
includesMembership(document, [
|
isTeamModel(actor, document),
|
||||||
DocumentPermission.Read,
|
or(
|
||||||
DocumentPermission.ReadWrite,
|
includesMembership(document, [
|
||||||
])
|
DocumentPermission.Read,
|
||||||
) {
|
DocumentPermission.ReadWrite,
|
||||||
return true;
|
]),
|
||||||
}
|
and(!!document?.isDraft, actor.id === document?.createdById),
|
||||||
|
can(actor, "readDocument", document?.collection)
|
||||||
// existence of collection option is not required here to account for share tokens
|
)
|
||||||
if (
|
)
|
||||||
document.collection &&
|
);
|
||||||
cannot(user, "readDocument", document.collection)
|
|
||||||
) {
|
allow(User, ["listRevisions", "listViews"], Document, (actor, document) =>
|
||||||
return false;
|
and(
|
||||||
}
|
//
|
||||||
|
can(actor, "read", document),
|
||||||
if (document.isDraft) {
|
!actor.isGuest
|
||||||
return user.id === document.createdById;
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
allow(User, "download", Document, (actor, document) =>
|
||||||
});
|
and(
|
||||||
|
can(actor, "read", document),
|
||||||
allow(User, "download", Document, (user, document) => {
|
or(
|
||||||
if (!document) {
|
and(!actor.isGuest, !actor.isViewer),
|
||||||
return false;
|
!!actor.team.getPreference(TeamPreference.ViewersCanExport)
|
||||||
}
|
)
|
||||||
|
)
|
||||||
if (
|
);
|
||||||
user.isViewer &&
|
|
||||||
!user.team.getPreference(TeamPreference.ViewersCanExport)
|
allow(User, "comment", Document, (actor, document) =>
|
||||||
) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
can(actor, "read", document),
|
||||||
|
isTeamMutable(actor),
|
||||||
if (
|
!!document?.isActive,
|
||||||
includesMembership(document, [
|
!document?.template
|
||||||
DocumentPermission.Read,
|
)
|
||||||
DocumentPermission.ReadWrite,
|
);
|
||||||
])
|
|
||||||
) {
|
allow(
|
||||||
return true;
|
User,
|
||||||
}
|
["star", "unstar", "subscribe", "unsubscribe"],
|
||||||
|
Document,
|
||||||
// existence of collection option is not required here to account for share tokens
|
(actor, document) =>
|
||||||
if (
|
and(
|
||||||
document.collection &&
|
//
|
||||||
cannot(user, "readDocument", document.collection)
|
can(actor, "read", document),
|
||||||
) {
|
!document?.template
|
||||||
return false;
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
if (document.isDraft) {
|
allow(User, "share", Document, (actor, document) =>
|
||||||
return user.id === document.createdById;
|
and(
|
||||||
}
|
can(actor, "read", document),
|
||||||
|
isTeamMutable(actor),
|
||||||
return user.teamId === document.teamId;
|
!!document?.isActive,
|
||||||
});
|
!document?.template,
|
||||||
|
!actor.isGuest,
|
||||||
allow(User, "comment", Document, (user, document) => {
|
or(!document?.collection, can(actor, "share", document?.collection))
|
||||||
if (!document || !document.isActive || document.template) {
|
)
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
|
allow(User, "update", Document, (actor, document) =>
|
||||||
if (
|
and(
|
||||||
includesMembership(document, [
|
can(actor, "read", document),
|
||||||
DocumentPermission.Read,
|
isTeamMutable(actor),
|
||||||
DocumentPermission.ReadWrite,
|
!!document?.isActive,
|
||||||
])
|
or(
|
||||||
) {
|
includesMembership(document, [DocumentPermission.ReadWrite]),
|
||||||
return true;
|
or(
|
||||||
}
|
can(actor, "updateDocument", document?.collection),
|
||||||
|
and(!!document?.isDraft && actor.id === document?.createdById)
|
||||||
if (document.collectionId) {
|
)
|
||||||
invariant(
|
)
|
||||||
document.collection,
|
)
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
);
|
||||||
);
|
|
||||||
if (can(user, "readDocument", document.collection)) {
|
allow(User, "publish", Document, (actor, document) =>
|
||||||
return true;
|
and(
|
||||||
}
|
//
|
||||||
}
|
can(actor, "update", document),
|
||||||
|
!!document?.isDraft
|
||||||
return user.id === document.createdById;
|
)
|
||||||
});
|
);
|
||||||
|
|
||||||
allow(User, ["star", "unstar"], Document, (user, document) => {
|
allow(User, ["move", "duplicate", "manageUsers"], Document, (actor, document) =>
|
||||||
if (!document || !document.isActive || document.template) {
|
and(
|
||||||
return false;
|
!actor.isGuest,
|
||||||
}
|
can(actor, "update", document),
|
||||||
|
or(
|
||||||
if (
|
can(actor, "updateDocument", document?.collection),
|
||||||
includesMembership(document, [
|
and(!!document?.isDraft && actor.id === document?.createdById)
|
||||||
DocumentPermission.Read,
|
)
|
||||||
DocumentPermission.ReadWrite,
|
)
|
||||||
])
|
);
|
||||||
) {
|
|
||||||
return true;
|
allow(User, "createChildDocument", Document, (actor, document) =>
|
||||||
}
|
and(
|
||||||
|
can(actor, "update", document),
|
||||||
if (document.collectionId) {
|
!document?.isDraft,
|
||||||
invariant(
|
!document?.template,
|
||||||
document.collection,
|
!actor.isGuest
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
)
|
||||||
);
|
);
|
||||||
if (cannot(user, "readDocument", document.collection)) {
|
|
||||||
return false;
|
allow(User, ["pin", "unpin"], Document, (actor, document) =>
|
||||||
}
|
and(
|
||||||
}
|
can(actor, "update", document),
|
||||||
|
can(actor, "update", document?.collection),
|
||||||
return user.teamId === document.teamId;
|
!document?.isDraft,
|
||||||
});
|
!document?.template,
|
||||||
|
!actor.isGuest
|
||||||
allow(User, "share", Document, (user, document) => {
|
)
|
||||||
if (
|
);
|
||||||
!document ||
|
|
||||||
document.archivedAt ||
|
allow(User, "pinToHome", Document, (actor, document) =>
|
||||||
document.deletedAt ||
|
and(
|
||||||
document.template
|
//
|
||||||
) {
|
isTeamAdmin(actor, document),
|
||||||
return false;
|
isTeamMutable(actor)
|
||||||
}
|
)
|
||||||
|
);
|
||||||
if (document.collectionId) {
|
|
||||||
invariant(
|
allow(User, "delete", Document, (actor, document) =>
|
||||||
document.collection,
|
and(
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
isTeamModel(actor, document),
|
||||||
);
|
isTeamMutable(actor),
|
||||||
if (cannot(user, "share", document.collection)) {
|
!actor.isGuest,
|
||||||
return false;
|
!document?.isDeleted,
|
||||||
}
|
or(can(actor, "update", document), !document?.collection)
|
||||||
}
|
)
|
||||||
|
);
|
||||||
if (document.isDraft) {
|
|
||||||
return user.id === document.createdById;
|
allow(User, ["restore", "permanentDelete"], Document, (actor, document) =>
|
||||||
}
|
and(
|
||||||
|
isTeamModel(actor, document),
|
||||||
return user.teamId === document.teamId;
|
!actor.isGuest,
|
||||||
});
|
!!document?.isDeleted,
|
||||||
|
or(
|
||||||
allow(User, "update", Document, (user, document) => {
|
includesMembership(document, [DocumentPermission.ReadWrite]),
|
||||||
if (!document || !document.isActive) {
|
or(
|
||||||
return false;
|
can(actor, "updateDocument", document?.collection),
|
||||||
}
|
and(!!document?.isDraft && actor.id === document?.createdById)
|
||||||
|
),
|
||||||
if (includesMembership(document, [DocumentPermission.ReadWrite])) {
|
!document?.collection
|
||||||
return true;
|
)
|
||||||
}
|
)
|
||||||
|
);
|
||||||
if (document.collectionId) {
|
|
||||||
invariant(
|
allow(User, "archive", Document, (actor, document) =>
|
||||||
document.collection,
|
and(
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
!actor.isGuest,
|
||||||
);
|
!document?.template,
|
||||||
if (cannot(user, "updateDocument", document.collection)) {
|
!document?.isDraft,
|
||||||
return false;
|
!!document?.isActive,
|
||||||
}
|
can(actor, "update", document),
|
||||||
}
|
can(actor, "updateDocument", document?.collection)
|
||||||
|
)
|
||||||
if (document.isDraft) {
|
);
|
||||||
return user.id === document.createdById;
|
|
||||||
}
|
allow(User, "unarchive", Document, (actor, document) =>
|
||||||
|
and(
|
||||||
return user.teamId === document.teamId;
|
!actor.isGuest,
|
||||||
});
|
!document?.template,
|
||||||
|
!document?.isDraft,
|
||||||
allow(User, "publish", Document, (user, document) => {
|
!document?.isDeleted,
|
||||||
if (!document || !document.isActive || !document.isDraft) {
|
!!document?.archivedAt,
|
||||||
return false;
|
and(
|
||||||
}
|
can(actor, "read", document),
|
||||||
|
or(
|
||||||
if (document.collectionId) {
|
includesMembership(document, [DocumentPermission.ReadWrite]),
|
||||||
invariant(
|
or(
|
||||||
document.collection,
|
can(actor, "updateDocument", document?.collection),
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
and(!!document?.isDraft && actor.id === document?.createdById)
|
||||||
);
|
)
|
||||||
if (can(user, "updateDocument", document.collection)) {
|
)
|
||||||
return true;
|
),
|
||||||
}
|
can(actor, "updateDocument", document?.collection)
|
||||||
}
|
)
|
||||||
|
);
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["manageUsers", "duplicate"], Document, (user, document) => {
|
|
||||||
if (!document || !document.isActive) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.collectionId) {
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (can(user, "updateDocument", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "updateInsights", Document, (user, document) => {
|
|
||||||
if (!document || !document.isActive) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.collectionId) {
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (can(user, "update", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "createChildDocument", Document, (user, document) => {
|
|
||||||
if (
|
|
||||||
!document ||
|
|
||||||
!document.isActive ||
|
|
||||||
document.isDraft ||
|
|
||||||
document.template
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (can(user, "updateDocument", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "move", Document, (user, document) => {
|
|
||||||
if (!document || !document.isActive) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (document.collection && can(user, "updateDocument", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "pin", Document, (user, document) => {
|
|
||||||
if (
|
|
||||||
!document ||
|
|
||||||
!document.isActive ||
|
|
||||||
document.isDraft ||
|
|
||||||
document.template
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (can(user, "update", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "unpin", Document, (user, document) => {
|
|
||||||
if (!document || document.isDraft || document.template) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (can(user, "update", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["subscribe", "unsubscribe"], Document, (user, document) => {
|
|
||||||
if (
|
|
||||||
!document ||
|
|
||||||
!document.isActive ||
|
|
||||||
document.isDraft ||
|
|
||||||
document.template
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
includesMembership(document, [
|
|
||||||
DocumentPermission.Read,
|
|
||||||
DocumentPermission.ReadWrite,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (can(user, "readDocument", document.collection)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.id === document.createdById;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "pinToHome", Document, (user, document) => {
|
|
||||||
if (
|
|
||||||
!document ||
|
|
||||||
!document.isActive ||
|
|
||||||
document.isDraft ||
|
|
||||||
document.template
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.teamId === document.teamId && user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "delete", Document, (user, document) => {
|
|
||||||
if (!document || document.deletedAt || user.isViewer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow deleting document without a collection
|
|
||||||
if (
|
|
||||||
document.collection &&
|
|
||||||
cannot(user, "deleteDocument", document.collection)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpublished drafts can always be deleted by their owner
|
|
||||||
if (document.isDraft) {
|
|
||||||
return user.id === document.createdById;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "permanentDelete", Document, (user, document) => {
|
|
||||||
if (!document || !document.deletedAt || user.isViewer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow deleting document without a collection
|
|
||||||
if (
|
|
||||||
document.collection &&
|
|
||||||
cannot(user, "updateDocument", document.collection)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpublished drafts can always be deleted by their owner
|
|
||||||
if (document.isDraft && user.id === document.createdById) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.teamId === document.teamId && user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "restore", Document, (user, document) => {
|
|
||||||
if (!document || !document.deletedAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
document.collection &&
|
|
||||||
cannot(user, "updateDocument", document.collection)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unpublished drafts can always be restored by their owner
|
|
||||||
if (document.isDraft && user.id === document.createdById) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "archive", Document, (user, document) => {
|
|
||||||
if (
|
|
||||||
!document ||
|
|
||||||
!document.isActive ||
|
|
||||||
document.isDraft ||
|
|
||||||
document.template
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (cannot(user, "updateDocument", document.collection)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user.teamId === document.teamId;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "unarchive", Document, (user, document) => {
|
|
||||||
if (!document || !document.archivedAt || document.deletedAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
invariant(
|
|
||||||
document.collection,
|
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
|
||||||
);
|
|
||||||
if (cannot(user, "updateDocument", document.collection)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.isDraft) {
|
|
||||||
return user.id === document.createdById;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(
|
allow(
|
||||||
Document,
|
Document,
|
||||||
@@ -456,7 +211,13 @@ allow(
|
|||||||
);
|
);
|
||||||
|
|
||||||
allow(User, "unpublish", Document, (user, document) => {
|
allow(User, "unpublish", Document, (user, document) => {
|
||||||
if (!document || !document.isActive || document.isDraft || user.isViewer) {
|
if (
|
||||||
|
!document ||
|
||||||
|
user.isGuest ||
|
||||||
|
user.isViewer ||
|
||||||
|
!document.isActive ||
|
||||||
|
document.isDraft
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
invariant(
|
invariant(
|
||||||
@@ -470,9 +231,13 @@ allow(User, "unpublish", Document, (user, document) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function includesMembership(
|
function includesMembership(
|
||||||
document: Document,
|
document: Document | null,
|
||||||
permissions: (DocumentPermission | CollectionPermission)[]
|
permissions: (DocumentPermission | CollectionPermission)[]
|
||||||
) {
|
) {
|
||||||
|
if (!document) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
document.memberships,
|
document.memberships,
|
||||||
"document memberships should be preloaded, did you forget withMembership scope?"
|
"document memberships should be preloaded, did you forget withMembership scope?"
|
||||||
|
|||||||
@@ -1,35 +1,25 @@
|
|||||||
import { FileOperationState, FileOperationType } from "@shared/types";
|
import { FileOperationState, FileOperationType } from "@shared/types";
|
||||||
import { User, Team, FileOperation } from "@server/models";
|
import { User, Team, FileOperation } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isTeamAdmin, isTeamMutable, or } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(
|
||||||
User,
|
User,
|
||||||
["createFileOperation", "createImport", "createExport"],
|
["createFileOperation", "createImport", "createExport"],
|
||||||
Team,
|
Team,
|
||||||
(user, team) => {
|
// Note: Not checking for isTeamMutable here because we want to allow exporting data in read-only.
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
isTeamAdmin
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user.isAdmin;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
allow(User, "read", FileOperation, (user, fileOperation) => {
|
allow(User, "read", FileOperation, isTeamAdmin);
|
||||||
if (!fileOperation || user.isViewer || user.teamId !== fileOperation.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "delete", FileOperation, (user, fileOperation) => {
|
allow(User, "delete", FileOperation, (actor, fileOperation) =>
|
||||||
if (!fileOperation || user.isViewer || user.teamId !== fileOperation.teamId) {
|
and(
|
||||||
return false;
|
isTeamAdmin(actor, fileOperation),
|
||||||
}
|
isTeamMutable(actor),
|
||||||
if (
|
or(
|
||||||
fileOperation.type === FileOperationType.Export &&
|
fileOperation?.type !== FileOperationType.Export,
|
||||||
fileOperation.state !== FileOperationState.Complete
|
fileOperation?.state === FileOperationState.Complete
|
||||||
) {
|
)
|
||||||
return false;
|
)
|
||||||
}
|
);
|
||||||
return user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
import { Group, User, Team } from "@server/models";
|
import { Group, User, Team } from "@server/models";
|
||||||
import { AdminRequiredError } from "../errors";
|
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isTeamAdmin, isTeamModel, isTeamMutable } from "./utils";
|
||||||
|
|
||||||
allow(User, "createGroup", Team, (actor, team) => {
|
allow(User, "createGroup", Team, (actor, team) =>
|
||||||
if (!team || actor.isViewer || actor.teamId !== team.id) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamAdmin(actor, team),
|
||||||
if (actor.isAdmin) {
|
isTeamMutable(actor)
|
||||||
return true;
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
throw AdminRequiredError();
|
allow(User, "listGroups", Team, (actor, team) =>
|
||||||
});
|
and(
|
||||||
|
//
|
||||||
|
isTeamModel(actor, team),
|
||||||
|
!actor.isGuest
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, "read", Group, (actor, group) => {
|
allow(User, "read", Group, (actor, team) =>
|
||||||
// for the time being, we're going to let everyone on the team see every group
|
and(
|
||||||
// we may need to make this more granular in the future
|
//
|
||||||
if (!group || actor.teamId !== group.teamId) {
|
isTeamModel(actor, team),
|
||||||
return false;
|
!actor.isGuest
|
||||||
}
|
)
|
||||||
return true;
|
);
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["update", "delete"], Group, (actor, group) => {
|
allow(User, ["update", "delete"], Group, (actor, team) =>
|
||||||
if (!group || actor.isViewer || actor.teamId !== group.teamId) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamAdmin(actor, team),
|
||||||
if (actor.isAdmin) {
|
isTeamMutable(actor)
|
||||||
return true;
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,42 +1,31 @@
|
|||||||
import { IntegrationType } from "@shared/types";
|
import { IntegrationType } from "@shared/types";
|
||||||
import { Integration, User, Team } from "@server/models";
|
import { Integration, User, Team } from "@server/models";
|
||||||
import { AdminRequiredError } from "../errors";
|
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import {
|
||||||
|
and,
|
||||||
|
isOwner,
|
||||||
|
isTeamAdmin,
|
||||||
|
isTeamModel,
|
||||||
|
isTeamMutable,
|
||||||
|
or,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
allow(User, "createIntegration", Team, (actor, team) => {
|
allow(User, "createIntegration", Team, (actor, team) =>
|
||||||
if (!team || actor.isViewer || actor.teamId !== team.id) {
|
and(isTeamAdmin(actor, team), isTeamMutable(actor))
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(
|
|
||||||
User,
|
|
||||||
"read",
|
|
||||||
Integration,
|
|
||||||
(user, integration) => user.teamId === integration?.teamId
|
|
||||||
);
|
);
|
||||||
|
|
||||||
allow(User, ["update", "delete"], Integration, (user, integration) => {
|
allow(User, "read", Integration, isTeamModel);
|
||||||
if (!integration || user.teamId !== integration.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
integration.userId === user.id &&
|
|
||||||
integration.type === IntegrationType.LinkedAccount
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (user.isViewer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
allow(User, ["update", "delete"], Integration, (actor, integration) =>
|
||||||
});
|
and(
|
||||||
|
isTeamModel(actor, integration),
|
||||||
|
isTeamMutable(actor),
|
||||||
|
!actor.isGuest,
|
||||||
|
!actor.isViewer,
|
||||||
|
or(
|
||||||
|
actor.isAdmin,
|
||||||
|
isOwner(actor, integration) &&
|
||||||
|
integration.type === IntegrationType.LinkedAccount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Notification, User } from "@server/models";
|
import { Notification, User } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isOwner } from "./utils";
|
||||||
|
|
||||||
allow(User, ["read", "update"], Notification, (user, notification) => {
|
allow(User, ["read", "update"], Notification, isOwner);
|
||||||
if (!notification) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user?.id === notification.userId;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { User, Pin } from "@server/models";
|
import { User, Pin } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isTeamAdmin } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(User, ["update", "delete"], Pin, isTeamAdmin);
|
||||||
User,
|
|
||||||
["update", "delete"],
|
|
||||||
Pin,
|
|
||||||
(user, pin) => user.teamId === pin?.teamId && user.isAdmin
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { SearchQuery, User } from "@server/models";
|
import { SearchQuery, User } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isOwner } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(User, ["read", "delete"], SearchQuery, isOwner);
|
||||||
User,
|
|
||||||
["read", "delete"],
|
|
||||||
SearchQuery,
|
|
||||||
(user, searchQuery) => user && user.id === searchQuery?.userId
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,41 +1,46 @@
|
|||||||
import { Share, User } from "@server/models";
|
import { Share, Team, User } from "@server/models";
|
||||||
import { AdminRequiredError } from "../errors";
|
import { allow, _can as can } from "./cancan";
|
||||||
import { allow, _cannot as cannot } from "./cancan";
|
import { and, isOwner, isTeamModel, isTeamMutable, or } from "./utils";
|
||||||
|
|
||||||
allow(User, "read", Share, (user, share) => user.teamId === share?.teamId);
|
allow(User, "createShare", Team, (actor, team) =>
|
||||||
|
and(
|
||||||
|
//
|
||||||
|
isTeamModel(actor, team),
|
||||||
|
isTeamMutable(actor),
|
||||||
|
!actor.isGuest
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, "update", Share, (user, share) => {
|
allow(User, "listShares", Team, (actor, team) =>
|
||||||
if (!share) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamModel(actor, team),
|
||||||
if (user.isViewer) {
|
!actor.isGuest
|
||||||
return false;
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
// only the user who can share the document publicly can update the share.
|
allow(User, "read", Share, (actor, share) =>
|
||||||
if (cannot(user, "share", share.document)) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamModel(actor, share),
|
||||||
|
!actor.isGuest
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return user.teamId === share.teamId;
|
allow(User, "update", Share, (actor, share) =>
|
||||||
});
|
and(
|
||||||
|
isTeamModel(actor, share),
|
||||||
|
!actor.isGuest,
|
||||||
|
!actor.isViewer,
|
||||||
|
can(actor, "share", share?.document)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, "revoke", Share, (user, share) => {
|
allow(User, "revoke", Share, (actor, share) =>
|
||||||
if (!share) {
|
and(
|
||||||
return false;
|
isTeamModel(actor, share),
|
||||||
}
|
!actor.isGuest,
|
||||||
if (user.isViewer) {
|
!actor.isViewer,
|
||||||
return false;
|
or(actor.isAdmin, isOwner(actor, share))
|
||||||
}
|
)
|
||||||
if (user.teamId !== share.teamId) {
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.id === share.userId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { User, Star } from "@server/models";
|
import { User, Star } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isOwner } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(User, ["read", "update", "delete"], Star, isOwner);
|
||||||
User,
|
|
||||||
["update", "delete"],
|
|
||||||
Star,
|
|
||||||
(user, star) => user.id === star?.userId
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
import { Subscription, User } from "@server/models";
|
import { Subscription, User } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isOwner } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(User, ["read", "update", "delete"], Subscription, isOwner);
|
||||||
User,
|
|
||||||
["read", "update", "delete"],
|
|
||||||
Subscription,
|
|
||||||
(user, subscription) => {
|
|
||||||
if (!subscription) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If `user` is an admin, early exit with allow.
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User should be able to read their subscriptions.
|
|
||||||
return user.id === subscription.userId;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
import env from "@server/env";
|
|
||||||
import { IncorrectEditionError } from "@server/errors";
|
|
||||||
import { Team, User } from "@server/models";
|
import { Team, User } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isCloudHosted, isTeamAdmin, isTeamModel } from "./utils";
|
||||||
|
|
||||||
allow(User, "read", Team, (user, team) => user.teamId === team?.id);
|
allow(User, "read", Team, isTeamModel);
|
||||||
|
|
||||||
allow(User, "share", Team, (user, team) => {
|
allow(User, "share", Team, (actor, team) =>
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
and(
|
||||||
return false;
|
isTeamModel(actor, team),
|
||||||
}
|
!actor.isGuest,
|
||||||
return team.sharing;
|
!actor.isViewer,
|
||||||
});
|
!!team?.sharing
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, "createTeam", Team, () => {
|
allow(User, "createTeam", Team, (actor) =>
|
||||||
if (!env.isCloudHosted) {
|
and(
|
||||||
throw IncorrectEditionError(
|
//
|
||||||
"Functionality is not available in this edition"
|
isCloudHosted(),
|
||||||
);
|
!actor.isGuest,
|
||||||
}
|
!actor.isViewer
|
||||||
return true;
|
)
|
||||||
});
|
);
|
||||||
|
|
||||||
allow(User, "update", Team, (user, team) => {
|
allow(User, "update", Team, isTeamAdmin);
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["delete", "audit"], Team, (user, team) => {
|
allow(User, ["delete", "audit"], Team, (actor, team) =>
|
||||||
if (!env.isCloudHosted) {
|
and(
|
||||||
throw IncorrectEditionError(
|
//
|
||||||
"Functionality is not available in this edition"
|
isCloudHosted(),
|
||||||
);
|
isTeamAdmin(actor, team)
|
||||||
}
|
)
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,114 +1,59 @@
|
|||||||
import { TeamPreference } from "@shared/types";
|
import { TeamPreference } from "@shared/types";
|
||||||
import { User, Team } from "@server/models";
|
import { User, Team } from "@server/models";
|
||||||
import { AdminRequiredError } from "../errors";
|
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(User, "read", User, isTeamModel);
|
||||||
User,
|
|
||||||
"read",
|
allow(User, "listUsers", Team, (actor, team) =>
|
||||||
User,
|
and(
|
||||||
(actor, user) => user && user.teamId === actor.teamId
|
//
|
||||||
|
isTeamModel(actor, team),
|
||||||
|
!actor.isGuest
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
allow(User, "inviteUser", Team, (actor, team) => {
|
allow(User, "inviteUser", Team, (actor, team) =>
|
||||||
if (!team || actor.teamId !== team.id || actor.isViewer) {
|
and(
|
||||||
return false;
|
isTeamModel(actor, team),
|
||||||
}
|
isTeamMutable(actor),
|
||||||
if (actor.isAdmin || team.getPreference(TeamPreference.MembersCanInvite)) {
|
!actor.isGuest,
|
||||||
return true;
|
!actor.isViewer,
|
||||||
}
|
actor.isAdmin || !!team?.getPreference(TeamPreference.MembersCanInvite)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
throw AdminRequiredError();
|
allow(User, ["update", "delete", "readDetails"], User, (actor, user) =>
|
||||||
});
|
or(
|
||||||
|
//
|
||||||
|
isTeamAdmin(actor, user),
|
||||||
|
actor.id === user?.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, "update", User, (actor, user) => {
|
allow(User, ["activate", "suspend"], User, isTeamAdmin);
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.id === actor.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actor.isAdmin) {
|
allow(User, "promote", User, (actor, user) =>
|
||||||
return true;
|
and(
|
||||||
}
|
//
|
||||||
|
isTeamAdmin(actor, user),
|
||||||
|
!user?.isAdmin,
|
||||||
|
!user?.isSuspended
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return false;
|
allow(User, "demote", User, (actor, user) =>
|
||||||
});
|
and(
|
||||||
|
//
|
||||||
|
isTeamAdmin(actor, user),
|
||||||
|
!user?.isSuspended
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
allow(User, "delete", User, (actor, user) => {
|
allow(User, "resendInvite", User, (actor, user) =>
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamAdmin(actor, user),
|
||||||
if (user.id === actor.id) {
|
!!user?.isInvited
|
||||||
return true;
|
)
|
||||||
}
|
);
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, ["activate", "suspend"], User, (actor, user) => {
|
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "readDetails", User, (actor, user) => {
|
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user === actor) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return actor.isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "promote", User, (actor, user) => {
|
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.isAdmin || user.isSuspended) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "resendInvite", User, (actor, user) => {
|
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!user.isInvited) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "demote", User, (actor, user) => {
|
|
||||||
if (!user || user.teamId !== actor.teamId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.isSuspended) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (actor.isAdmin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw AdminRequiredError();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { User, UserMembership } from "@server/models";
|
import { User, UserMembership } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { isOwner, or } from "./utils";
|
||||||
|
|
||||||
allow(
|
allow(User, ["update", "delete"], UserMembership, (actor, membership) =>
|
||||||
User,
|
or(
|
||||||
["update", "delete"],
|
//
|
||||||
UserMembership,
|
isOwner(actor, membership),
|
||||||
(user, membership) => user.id === membership?.userId || user.isAdmin
|
actor.isAdmin
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
91
server/policies/utils.ts
Normal file
91
server/policies/utils.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import env from "@server/env";
|
||||||
|
import { IncorrectEditionError } from "@server/errors";
|
||||||
|
import { User, Team } from "@server/models";
|
||||||
|
import Model from "@server/models/base/Model";
|
||||||
|
|
||||||
|
export function and(...args: boolean[]) {
|
||||||
|
return args.every(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function or(...args: boolean[]) {
|
||||||
|
return args.some(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the actor is present in the same team as the model.
|
||||||
|
*
|
||||||
|
* @param actor The actor to check
|
||||||
|
* @param model The model to check
|
||||||
|
* @returns True if the actor is in the same team as the model
|
||||||
|
*/
|
||||||
|
export function isTeamModel(
|
||||||
|
actor: User,
|
||||||
|
model: Model | null | undefined
|
||||||
|
): model is Model {
|
||||||
|
if (!model) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (model instanceof Team) {
|
||||||
|
return actor.teamId === model.id;
|
||||||
|
}
|
||||||
|
if ("teamId" in model) {
|
||||||
|
return actor.teamId === model.teamId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the actor is the owner of the model.
|
||||||
|
*
|
||||||
|
* @param actor The actor to check
|
||||||
|
* @param model The model to check
|
||||||
|
* @returns True if the actor is the owner of the model
|
||||||
|
*/
|
||||||
|
export function isOwner(
|
||||||
|
actor: User,
|
||||||
|
model: Model | null | undefined
|
||||||
|
): model is Model {
|
||||||
|
if (!model) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("userId" in model) {
|
||||||
|
return actor.id === model.userId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the actor is an admin of the team.
|
||||||
|
*
|
||||||
|
* @param actor The actor to check
|
||||||
|
* @param mode The model to check
|
||||||
|
* @returns True if the actor is an admin of the team the model belongs to
|
||||||
|
*/
|
||||||
|
export function isTeamAdmin(
|
||||||
|
actor: User,
|
||||||
|
model: Model | null | undefined
|
||||||
|
): model is Model {
|
||||||
|
return and(isTeamModel(actor, model), actor.isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the actors team is mutable, meaning the team models can be modified.
|
||||||
|
*
|
||||||
|
* @param actor The actor to check
|
||||||
|
* @returns True if the actor's team is mutable
|
||||||
|
*/
|
||||||
|
export function isTeamMutable(_actor: User, _model?: Model | null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this instance is running in the cloud-hosted environment.
|
||||||
|
*/
|
||||||
|
export function isCloudHosted() {
|
||||||
|
if (!env.isCloudHosted) {
|
||||||
|
throw IncorrectEditionError(
|
||||||
|
"Functionality is not available in this edition"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,35 +1,15 @@
|
|||||||
import { User, Team, WebhookSubscription } from "@server/models";
|
import { User, Team, WebhookSubscription } from "@server/models";
|
||||||
import { allow } from "./cancan";
|
import { allow } from "./cancan";
|
||||||
|
import { and, isTeamAdmin, isTeamMutable } from "./utils";
|
||||||
|
|
||||||
allow(User, "listWebhookSubscription", Team, (user, team) => {
|
allow(User, "createWebhookSubscription", Team, (actor, team) =>
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
and(
|
||||||
return false;
|
//
|
||||||
}
|
isTeamAdmin(actor, team),
|
||||||
|
isTeamMutable(actor)
|
||||||
return user.isAdmin;
|
)
|
||||||
});
|
|
||||||
|
|
||||||
allow(User, "createWebhookSubscription", Team, (user, team) => {
|
|
||||||
if (!team || user.isViewer || user.teamId !== team.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.isAdmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
allow(
|
|
||||||
User,
|
|
||||||
["read", "update", "delete"],
|
|
||||||
WebhookSubscription,
|
|
||||||
(user, webhook): boolean => {
|
|
||||||
if (!user || !webhook) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.isAdmin) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.teamId === webhook.teamId;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
allow(User, "listWebhookSubscription", Team, isTeamAdmin);
|
||||||
|
|
||||||
|
allow(User, ["read", "update", "delete"], WebhookSubscription, isTeamAdmin);
|
||||||
|
|||||||
@@ -2564,30 +2564,6 @@ describe("#documents.restore", () => {
|
|||||||
expect(body.data.archivedAt).toEqual(null);
|
expect(body.data.archivedAt).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not add restored templates to collection structure", async () => {
|
|
||||||
const user = await buildUser();
|
|
||||||
const collection = await buildCollection({
|
|
||||||
teamId: user.teamId,
|
|
||||||
});
|
|
||||||
const template = await buildDocument({
|
|
||||||
teamId: user.teamId,
|
|
||||||
collectionId: collection.id,
|
|
||||||
template: true,
|
|
||||||
});
|
|
||||||
await template.archive(user.id);
|
|
||||||
const res = await server.post("/api/documents.restore", {
|
|
||||||
body: {
|
|
||||||
token: user.getJwtToken(),
|
|
||||||
id: template.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const body = await res.json();
|
|
||||||
expect(res.status).toEqual(200);
|
|
||||||
expect(body.data.archivedAt).toEqual(null);
|
|
||||||
await collection.reload();
|
|
||||||
expect(collection.documentStructure).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should restore archived when previous parent is archived", async () => {
|
it("should restore archived when previous parent is archived", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
exports[`#groups.add_user should require admin 1`] = `
|
exports[`#groups.add_user should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "admin_required",
|
"error": "authorization_error",
|
||||||
"message": "An admin role is required to access this resource",
|
"message": "Authorization error",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"status": 403,
|
"status": 403,
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,8 @@ exports[`#groups.memberships should require authentication 1`] = `
|
|||||||
|
|
||||||
exports[`#groups.remove_user should require admin 1`] = `
|
exports[`#groups.remove_user should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "admin_required",
|
"error": "authorization_error",
|
||||||
"message": "An admin role is required to access this resource",
|
"message": "Authorization error",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"status": 403,
|
"status": 403,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ router.post(
|
|||||||
async (ctx: APIContext<T.GroupsListReq>) => {
|
async (ctx: APIContext<T.GroupsListReq>) => {
|
||||||
const { direction, sort, userId, name } = ctx.input.body;
|
const { direction, sort, userId, name } = ctx.input.body;
|
||||||
const { user } = ctx.state.auth;
|
const { user } = ctx.state.auth;
|
||||||
|
authorize(user, "listGroups", user.team);
|
||||||
|
|
||||||
let where: WhereOptions<Group> = {
|
let where: WhereOptions<Group> = {
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ router.post(
|
|||||||
const document = await Document.findByPk(revision.documentId, {
|
const document = await Document.findByPk(revision.documentId, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
authorize(user, "read", document);
|
authorize(user, "listRevisions", document);
|
||||||
after = revision;
|
after = revision;
|
||||||
before = await revision.before();
|
before = await revision.before();
|
||||||
} else if (documentId) {
|
} else if (documentId) {
|
||||||
const document = await Document.findByPk(documentId, {
|
const document = await Document.findByPk(documentId, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
authorize(user, "read", document);
|
authorize(user, "listRevisions", document);
|
||||||
after = Revision.buildFromDocument(document);
|
after = Revision.buildFromDocument(document);
|
||||||
after.id = RevisionHelper.latestId(document.id);
|
after.id = RevisionHelper.latestId(document.id);
|
||||||
after.user = document.updatedBy;
|
after.user = document.updatedBy;
|
||||||
@@ -75,7 +75,7 @@ router.post(
|
|||||||
const document = await Document.findByPk(revision.documentId, {
|
const document = await Document.findByPk(revision.documentId, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
authorize(user, "read", document);
|
authorize(user, "listRevisions", document);
|
||||||
|
|
||||||
let before;
|
let before;
|
||||||
if (compareToId) {
|
if (compareToId) {
|
||||||
@@ -126,7 +126,7 @@ router.post(
|
|||||||
const document = await Document.findByPk(documentId, {
|
const document = await Document.findByPk(documentId, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
authorize(user, "read", document);
|
authorize(user, "listRevisions", document);
|
||||||
|
|
||||||
const revisions = await Revision.findAll({
|
const revisions = await Revision.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ router.post(
|
|||||||
async (ctx: APIContext<T.SharesListReq>) => {
|
async (ctx: APIContext<T.SharesListReq>) => {
|
||||||
const { sort, direction } = ctx.input.body;
|
const { sort, direction } = ctx.input.body;
|
||||||
const { user } = ctx.state.auth;
|
const { user } = ctx.state.auth;
|
||||||
|
authorize(user, "listShares", user.team);
|
||||||
|
|
||||||
const where: WhereOptions<Share> = {
|
const where: WhereOptions<Share> = {
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -169,6 +171,8 @@ router.post(
|
|||||||
const { documentId, published, urlId, includeChildDocuments } =
|
const { documentId, published, urlId, includeChildDocuments } =
|
||||||
ctx.input.body;
|
ctx.input.body;
|
||||||
const { user } = ctx.state.auth;
|
const { user } = ctx.state.auth;
|
||||||
|
authorize(user, "createShare", user.team);
|
||||||
|
|
||||||
const document = await Document.findByPk(documentId, {
|
const document = await Document.findByPk(documentId, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
exports[`#users.activate should require admin 1`] = `
|
exports[`#users.activate should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "admin_required",
|
"error": "authorization_error",
|
||||||
"message": "An admin role is required to access this resource",
|
"message": "Authorization error",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"status": 403,
|
"status": 403,
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@ exports[`#users.demote should not allow demoting self 1`] = `
|
|||||||
|
|
||||||
exports[`#users.demote should require admin 1`] = `
|
exports[`#users.demote should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "admin_required",
|
"error": "authorization_error",
|
||||||
"message": "An admin role is required to access this resource",
|
"message": "Authorization error",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"status": 403,
|
"status": 403,
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,8 @@ exports[`#users.demote should require admin 1`] = `
|
|||||||
|
|
||||||
exports[`#users.promote should require admin 1`] = `
|
exports[`#users.promote should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "admin_required",
|
"error": "authorization_error",
|
||||||
"message": "An admin role is required to access this resource",
|
"message": "Authorization error",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"status": 403,
|
"status": 403,
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,8 @@ exports[`#users.suspend should not allow suspending the user themselves 1`] = `
|
|||||||
|
|
||||||
exports[`#users.suspend should require admin 1`] = `
|
exports[`#users.suspend should require admin 1`] = `
|
||||||
{
|
{
|
||||||
"error": "admin_required",
|
"error": "authorization_error",
|
||||||
"message": "An admin role is required to access this resource",
|
"message": "Authorization error",
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"status": 403,
|
"status": 403,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ describe("#users.list", () => {
|
|||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.data.length).toEqual(2);
|
expect(body.data.length).toEqual(2);
|
||||||
expect(body.data[0].email).toEqual(undefined);
|
expect(body.data[0].email).toEqual(undefined);
|
||||||
expect(body.data[1].email).toEqual(undefined);
|
expect(body.data[1].email).toEqual(user.email);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ router.post(
|
|||||||
const document = await Document.findByPk(documentId, {
|
const document = await Document.findByPk(documentId, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
authorize(user, "read", document);
|
authorize(user, "listViews", document);
|
||||||
|
|
||||||
if (!document.insightsEnabled) {
|
if (!document.insightsEnabled) {
|
||||||
throw ValidationError("Insights are not enabled for this document");
|
throw ValidationError("Insights are not enabled for this document");
|
||||||
|
|||||||
@@ -798,6 +798,7 @@
|
|||||||
"Where do I find the file?": "Where do I find the file?",
|
"Where do I find the file?": "Where do I find the file?",
|
||||||
"In Notion, click <em>Settings & Members</em> in the left sidebar and open Settings. Look for the Export section, and click <em>Export all workspace content</em>. Choose <em>HTML</em> as the format for the best data compatability.": "In Notion, click <em>Settings & Members</em> in the left sidebar and open Settings. Look for the Export section, and click <em>Export all workspace content</em>. Choose <em>HTML</em> as the format for the best data compatability.",
|
"In Notion, click <em>Settings & Members</em> in the left sidebar and open Settings. Look for the Export section, and click <em>Export all workspace content</em>. Choose <em>HTML</em> as the format for the best data compatability.": "In Notion, click <em>Settings & Members</em> in the left sidebar and open Settings. Look for the Export section, and click <em>Export all workspace content</em>. Choose <em>HTML</em> as the format for the best data compatability.",
|
||||||
"Last active": "Last active",
|
"Last active": "Last active",
|
||||||
|
"Guest": "Guest",
|
||||||
"Shared": "Shared",
|
"Shared": "Shared",
|
||||||
"by {{ name }}": "by {{ name }}",
|
"by {{ name }}": "by {{ name }}",
|
||||||
"Last accessed": "Last accessed",
|
"Last accessed": "Last accessed",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export enum UserRole {
|
|||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
Viewer = "viewer",
|
Viewer = "viewer",
|
||||||
|
Guest = "guest",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateFilter = "day" | "week" | "month" | "year";
|
export type DateFilter = "day" | "week" | "month" | "year";
|
||||||
|
|||||||
Reference in New Issue
Block a user