mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
5a4db980af
- Tighten Document.findByIds so isPrivate filtering fails closed when the attribute is not loaded, and include isPrivate in the projection used by Relationship.findSourceDocumentIdsForUser so backlinks from restricted docs are no longer leaked to collection-only members. - Add !isPrivate gate to the unpublish policy so collection writers without direct membership cannot unpublish restricted documents. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
329 lines
8.0 KiB
TypeScript
329 lines
8.0 KiB
TypeScript
import invariant from "invariant";
|
|
import { DocumentPermission, TeamPreference } from "@shared/types";
|
|
import { Document, Revision, User, Team } from "@server/models";
|
|
import { allow, can } from "./cancan";
|
|
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
|
|
|
|
allow(User, "createDocument", Team, (actor, document) =>
|
|
and(
|
|
//
|
|
!actor.isGuest,
|
|
!actor.isViewer,
|
|
isTeamModel(actor, document),
|
|
isTeamMutable(actor)
|
|
)
|
|
);
|
|
|
|
allow(User, "read", Document, (actor, document) =>
|
|
and(
|
|
isTeamModel(actor, document),
|
|
or(
|
|
includesMembership(document, [
|
|
DocumentPermission.Read,
|
|
DocumentPermission.ReadWrite,
|
|
DocumentPermission.Admin,
|
|
]),
|
|
and(!!document?.isDraft, actor.id === document?.createdById),
|
|
isTeamAdmin(actor, document),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "readDocument", document?.collection)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, ["listRevisions", "listViews"], Document, (actor, document) =>
|
|
or(
|
|
and(!actor.isGuest, can(actor, "read", document)),
|
|
and(actor.isGuest, can(actor, "update", document))
|
|
)
|
|
);
|
|
|
|
allow(User, "download", Document, (actor, document) =>
|
|
and(
|
|
can(actor, "read", document),
|
|
or(
|
|
and(!actor.isGuest, !actor.isViewer),
|
|
!!actor.team.getPreference(TeamPreference.ViewersCanExport)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "comment", Document, (actor, document) =>
|
|
and(
|
|
!!document?.isActive,
|
|
isTeamMutable(actor),
|
|
// TODO: We'll introduce a separate permission for commenting
|
|
or(
|
|
and(!actor.isGuest, can(actor, "read", document)),
|
|
and(actor.isGuest, can(actor, "update", document))
|
|
),
|
|
or(!document?.collection, document?.collection?.commenting !== false)
|
|
)
|
|
);
|
|
|
|
allow(
|
|
User,
|
|
["star", "unstar", "subscribe", "unsubscribe"],
|
|
Document,
|
|
(actor, document) =>
|
|
and(
|
|
//
|
|
can(actor, "read", document)
|
|
)
|
|
);
|
|
|
|
allow(User, "share", Document, (actor, document) =>
|
|
and(
|
|
!!document?.isActive,
|
|
isTeamMutable(actor),
|
|
can(actor, "read", document),
|
|
or(!document?.collection, can(actor, "share", document?.collection))
|
|
)
|
|
);
|
|
|
|
allow(User, "update", Document, (actor, document) =>
|
|
and(
|
|
!!document?.isActive,
|
|
isTeamMutable(actor),
|
|
can(actor, "read", document),
|
|
or(
|
|
includesMembership(document, [
|
|
DocumentPermission.ReadWrite,
|
|
DocumentPermission.Admin,
|
|
]),
|
|
and(isTeamAdmin(actor, document), can(actor, "read", document)),
|
|
and(!!document?.isDraft && actor.id === document?.createdById),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "publish", Document, (actor, document) =>
|
|
and(
|
|
//
|
|
!!document?.isDraft,
|
|
can(actor, "update", document)
|
|
)
|
|
);
|
|
|
|
allow(User, "manageUsers", Document, (actor, document) =>
|
|
and(
|
|
can(actor, "update", document),
|
|
or(
|
|
includesMembership(document, [DocumentPermission.Admin]),
|
|
and(isTeamAdmin(actor, document), can(actor, "read", document)),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
),
|
|
!!document?.isDraft && actor.id === document?.createdById
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "duplicate", Document, (actor, document) =>
|
|
and(
|
|
can(actor, "update", document),
|
|
or(
|
|
includesMembership(document, [DocumentPermission.Admin]),
|
|
and(isTeamAdmin(actor, document), can(actor, "read", document)),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
),
|
|
!!document?.isDraft && actor.id === document?.createdById
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "move", Document, (actor, document) =>
|
|
and(
|
|
can(actor, "update", document),
|
|
or(
|
|
includesMembership(document, [
|
|
DocumentPermission.ReadWrite,
|
|
DocumentPermission.Admin,
|
|
]),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
),
|
|
and(!!document?.isDraft && actor.id === document?.createdById),
|
|
and(!!document?.isDraft && !document?.collection)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "createChildDocument", Document, (actor, document) =>
|
|
and(
|
|
//
|
|
!document?.isDraft,
|
|
can(actor, "update", document)
|
|
)
|
|
);
|
|
|
|
allow(User, ["updateInsights", "pin", "unpin"], Document, (actor, document) =>
|
|
and(
|
|
!document?.isDraft,
|
|
!actor.isGuest,
|
|
can(actor, "update", document),
|
|
can(actor, "update", document?.collection)
|
|
)
|
|
);
|
|
|
|
allow(User, "pinToHome", Document, (actor, document) =>
|
|
and(
|
|
//
|
|
!document?.isDraft,
|
|
!!document?.isActive,
|
|
isTeamAdmin(actor, document),
|
|
isTeamMutable(actor)
|
|
)
|
|
);
|
|
|
|
allow(User, "delete", Document, (actor, document) =>
|
|
and(
|
|
!document?.isDeleted,
|
|
isTeamModel(actor, document),
|
|
isTeamMutable(actor),
|
|
or(
|
|
can(actor, "unarchive", document),
|
|
can(actor, "update", document),
|
|
and(!document?.collection, actor.id === document?.createdById)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "restore", Document, (actor, document) =>
|
|
and(
|
|
!actor.isGuest,
|
|
!!document?.isDeleted,
|
|
isTeamModel(actor, document),
|
|
or(
|
|
includesMembership(document, [
|
|
DocumentPermission.ReadWrite,
|
|
DocumentPermission.Admin,
|
|
]),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
),
|
|
and(!!document?.isDraft && actor.id === document?.createdById)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "permanentDelete", Document, (actor, document) =>
|
|
and(
|
|
!actor.isGuest,
|
|
!!document?.isDeleted,
|
|
isTeamModel(actor, document),
|
|
isTeamAdmin(actor, document)
|
|
)
|
|
);
|
|
|
|
allow(User, "archive", Document, (actor, document) =>
|
|
and(
|
|
!document?.isDraft,
|
|
!!document?.isActive,
|
|
can(actor, "update", document),
|
|
or(
|
|
includesMembership(document, [DocumentPermission.Admin]),
|
|
and(isTeamAdmin(actor, document), can(actor, "read", document)),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(User, "unarchive", Document, (actor, document) =>
|
|
and(
|
|
!document?.isDraft,
|
|
!document?.isDeleted,
|
|
!!document?.archivedAt,
|
|
can(actor, "read", document),
|
|
or(
|
|
includesMembership(document, [
|
|
DocumentPermission.ReadWrite,
|
|
DocumentPermission.Admin,
|
|
]),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(actor, "updateDocument", document?.collection)
|
|
),
|
|
and(!!document?.isDraft && actor.id === document?.createdById)
|
|
)
|
|
)
|
|
);
|
|
|
|
allow(
|
|
Document,
|
|
"restore",
|
|
Revision,
|
|
(document, revision) => document.id === revision?.documentId
|
|
);
|
|
|
|
allow(User, "unpublish", Document, (user, document) =>
|
|
and(
|
|
!!document,
|
|
!user.isGuest,
|
|
!user.isViewer,
|
|
!!document?.isActive,
|
|
!document?.isDraft,
|
|
user.teamId === document?.teamId,
|
|
or(
|
|
includesMembership(document, [
|
|
DocumentPermission.ReadWrite,
|
|
DocumentPermission.Admin,
|
|
]),
|
|
and(isTeamAdmin(user, document), can(user, "read", document)),
|
|
and(
|
|
!document?.isPrivate,
|
|
can(user, "updateDocument", document?.collection)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
|
|
function includesMembership(
|
|
document: Document | null,
|
|
permissions: DocumentPermission[]
|
|
) {
|
|
if (!document) {
|
|
return false;
|
|
}
|
|
|
|
invariant(
|
|
document.memberships,
|
|
"Development: document memberships should be preloaded, did you forget withMembership scope?"
|
|
);
|
|
invariant(
|
|
document.groupMemberships,
|
|
"Development: document groupMemberships should be preloaded, did you forget withMembership scope?"
|
|
);
|
|
|
|
const permissionSet = new Set(permissions);
|
|
const membershipIds: string[] = [];
|
|
|
|
for (const membership of document.memberships) {
|
|
if (permissionSet.has(membership.permission as DocumentPermission)) {
|
|
membershipIds.push(membership.id);
|
|
}
|
|
}
|
|
|
|
for (const membership of document.groupMemberships) {
|
|
if (permissionSet.has(membership.permission as DocumentPermission)) {
|
|
membershipIds.push(membership.id);
|
|
}
|
|
}
|
|
|
|
return membershipIds.length > 0 ? membershipIds : false;
|
|
}
|