mirror of
https://github.com/outline/outline.git
synced 2025-12-21 10:39:41 -06:00
Vendorize cancan with performance improvements (#7448)
* Vendorize CanCan with performance improvements * docs
This commit is contained in:
@@ -95,7 +95,6 @@
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"body-scroll-lock": "^4.0.0-beta.0",
|
||||
"bull": "^4.12.2",
|
||||
"cancan": "3.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"command-score": "^0.1.2",
|
||||
|
||||
@@ -1,13 +1,189 @@
|
||||
import CanCan from "cancan";
|
||||
import isObject from "lodash/isPlainObject";
|
||||
import { Model } from "sequelize-typescript";
|
||||
import { AuthorizationError } from "@server/errors";
|
||||
|
||||
type Constructor = new (...args: any) => any;
|
||||
|
||||
type Condition<T extends Constructor, P extends Constructor> = (
|
||||
performer: InstanceType<P>,
|
||||
target: InstanceType<T> | null,
|
||||
options?: any
|
||||
) => boolean;
|
||||
|
||||
type Ability = {
|
||||
model: Constructor;
|
||||
action: string;
|
||||
target: Constructor | Model | string;
|
||||
condition?: Condition<Constructor, Constructor>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class that provides a simple way to define and check authorization abilities.
|
||||
* This is originally adapted from https://www.npmjs.com/package/cancan
|
||||
*/
|
||||
export class CanCan {
|
||||
public abilities: Ability[] = [];
|
||||
|
||||
/**
|
||||
* Define an authorized ability for a model, action, and target.
|
||||
*
|
||||
* @param model The model that the ability is for.
|
||||
* @param actions The action or actions that are allowed.
|
||||
* @param targets The target or targets that the ability applies to.
|
||||
* @param condition The condition that must be met for the ability to apply
|
||||
*/
|
||||
public allow = <T extends Constructor, P extends Constructor>(
|
||||
model: P,
|
||||
actions: string | ReadonlyArray<string>,
|
||||
targets: T | ReadonlyArray<T> | string | ReadonlyArray<string>,
|
||||
condition?: Condition<T, P> | object
|
||||
) => {
|
||||
if (
|
||||
typeof condition !== "undefined" &&
|
||||
typeof condition !== "function" &&
|
||||
!isObject(condition)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`Expected condition to be object or function, got ${typeof condition}`
|
||||
);
|
||||
}
|
||||
|
||||
if (condition && isObject(condition)) {
|
||||
condition = this.getConditionFn(condition);
|
||||
}
|
||||
|
||||
(this.toArray(actions) as string[]).forEach((action) => {
|
||||
(this.toArray(targets) as T[]).forEach((target) => {
|
||||
this.abilities.push({ model, action, target, condition } as Ability);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a performer can perform an action on a target.
|
||||
*
|
||||
* @param performer The performer that is trying to perform the action.
|
||||
* @param action The action that the performer is trying to perform.
|
||||
* @param target The target that the action is upon.
|
||||
* @param options Additional options to pass to the condition function.
|
||||
* @returns Whether the performer can perform the action on the target.
|
||||
*/
|
||||
public can = (
|
||||
performer: Model,
|
||||
action: string,
|
||||
target: Model | null | undefined,
|
||||
options = {}
|
||||
) => {
|
||||
const matchingAbilities = this.abilities.filter(
|
||||
(ability) =>
|
||||
performer instanceof ability.model &&
|
||||
(ability.target === "all" ||
|
||||
target === ability.target ||
|
||||
target instanceof (ability.target as any)) &&
|
||||
(ability.action === "manage" || action === ability.action)
|
||||
);
|
||||
|
||||
// Check conditions only for matching abilities
|
||||
return matchingAbilities.some(
|
||||
(ability) =>
|
||||
!ability.condition ||
|
||||
ability.condition(performer, target, options || {})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a performer cannot perform an action on a target, which is the opposite of `can`.
|
||||
*
|
||||
* @param performer The performer that is trying to perform the action.
|
||||
* @param action The action that the performer is trying to perform.
|
||||
* @param target The target that the action is upon.
|
||||
* @param options Additional options to pass to the condition function.
|
||||
* @returns Whether the performer cannot perform the action on the target.
|
||||
*/
|
||||
public cannot = (
|
||||
performer: Model,
|
||||
action: string,
|
||||
target: Model | null | undefined,
|
||||
options = {}
|
||||
) => !this.can(performer, action, target, options);
|
||||
|
||||
/**
|
||||
* Guard if a performer can perform an action on a target, throwing an error if they cannot.
|
||||
*
|
||||
* @param performer The performer that is trying to perform the action.
|
||||
* @param action The action that the performer is trying to perform.
|
||||
* @param target The target that the action is upon.
|
||||
* @param options Additional options to pass to the condition function.
|
||||
* @throws AuthorizationError If the performer cannot perform the action on the target.
|
||||
*/
|
||||
public authorize = (
|
||||
performer: Model,
|
||||
action: string,
|
||||
target: Model | null | undefined,
|
||||
options = {}
|
||||
): asserts target => {
|
||||
if (this.cannot(performer, action, target, options)) {
|
||||
throw AuthorizationError("Authorization error");
|
||||
}
|
||||
};
|
||||
|
||||
// Private methods
|
||||
|
||||
private get = (obj: object, key: string) =>
|
||||
"get" in obj && typeof obj.get === "function" ? obj.get(key) : obj[key];
|
||||
|
||||
private isPartiallyEqual = (target: object, obj: object) =>
|
||||
Object.keys(obj).every((key) => this.get(target, key) === obj[key]);
|
||||
|
||||
private getConditionFn =
|
||||
(condition: object) => (performer: Model, target: Model) =>
|
||||
this.isPartiallyEqual(target, condition);
|
||||
|
||||
private toArray = (value: unknown): unknown[] => {
|
||||
if (value === null || value === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return [value];
|
||||
}
|
||||
if (typeof value[Symbol.iterator] === "function") {
|
||||
// @ts-expect-error - TS doesn't know that value is iterable
|
||||
return [...value];
|
||||
}
|
||||
|
||||
return [value];
|
||||
};
|
||||
}
|
||||
|
||||
const cancan = new CanCan();
|
||||
|
||||
export const _can = cancan.can;
|
||||
export const { allow, can, cannot, abilities } = cancan;
|
||||
|
||||
export const _authorize = cancan.authorize;
|
||||
// This is exported separately as a workaround for the following issue:
|
||||
// https://github.com/microsoft/TypeScript/issues/36931
|
||||
export const authorize: typeof cancan.authorize = cancan.authorize;
|
||||
|
||||
export const _cannot = cancan.cannot;
|
||||
// The MIT License (MIT)
|
||||
|
||||
export const _abilities = cancan.abilities;
|
||||
// Copyright (c) Vadim Demedes <vdemedes@gmail.com> (github.com/vadimdemedes)
|
||||
|
||||
export const allow = cancan.allow;
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
@@ -2,7 +2,7 @@ import invariant from "invariant";
|
||||
import some from "lodash/some";
|
||||
import { CollectionPermission, DocumentPermission } from "@shared/types";
|
||||
import { Collection, User, Team } from "@server/models";
|
||||
import { allow, _can as can } from "./cancan";
|
||||
import { allow, can } from "./cancan";
|
||||
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
|
||||
|
||||
allow(User, "createCollection", Team, (actor, team) =>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TeamPreference,
|
||||
} from "@shared/types";
|
||||
import { Document, Revision, User, Team } from "@server/models";
|
||||
import { allow, _cannot as cannot, _can as can } from "./cancan";
|
||||
import { allow, cannot, can } from "./cancan";
|
||||
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
|
||||
|
||||
allow(User, "createDocument", Team, (actor, document) =>
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import type {
|
||||
ApiKey,
|
||||
Attachment,
|
||||
AuthenticationProvider,
|
||||
Collection,
|
||||
Comment,
|
||||
Document,
|
||||
FileOperation,
|
||||
Integration,
|
||||
Pin,
|
||||
SearchQuery,
|
||||
Share,
|
||||
Star,
|
||||
Subscription,
|
||||
User,
|
||||
Team,
|
||||
Group,
|
||||
WebhookSubscription,
|
||||
Notification,
|
||||
UserMembership,
|
||||
} from "@server/models";
|
||||
import { _abilities, _can, _cannot, _authorize } from "./cancan";
|
||||
import type { Model } from "sequelize-typescript";
|
||||
import type { User } from "@server/models";
|
||||
import { abilities, can } from "./cancan";
|
||||
|
||||
// export everything from cancan
|
||||
export * from "./cancan";
|
||||
|
||||
// Import all policies
|
||||
import "./apiKey";
|
||||
import "./attachment";
|
||||
import "./authenticationProvider";
|
||||
@@ -42,48 +28,18 @@ import "./userMembership";
|
||||
|
||||
type Policy = Record<string, boolean>;
|
||||
|
||||
// this should not be needed but is a workaround for this TypeScript issue:
|
||||
// https://github.com/microsoft/TypeScript/issues/36931
|
||||
export const authorize: typeof _authorize = _authorize;
|
||||
|
||||
export const can = _can;
|
||||
|
||||
export const cannot = _cannot;
|
||||
|
||||
export const abilities = _abilities;
|
||||
|
||||
/*
|
||||
* Given a user and a model – output an object which describes the actions the
|
||||
* user may take against the model. This serialized policy is used for testing
|
||||
* and sent in API responses to allow clients to adjust which UI is displayed.
|
||||
*/
|
||||
export function serialize(
|
||||
model: User,
|
||||
target:
|
||||
| ApiKey
|
||||
| Attachment
|
||||
| AuthenticationProvider
|
||||
| Collection
|
||||
| Comment
|
||||
| Document
|
||||
| FileOperation
|
||||
| Integration
|
||||
| Pin
|
||||
| SearchQuery
|
||||
| Share
|
||||
| Star
|
||||
| Subscription
|
||||
| User
|
||||
| Team
|
||||
| Group
|
||||
| WebhookSubscription
|
||||
| Notification
|
||||
| UserMembership
|
||||
| null
|
||||
): Policy {
|
||||
export function serialize(model: User, target: Model | null): Policy {
|
||||
const output = {};
|
||||
abilities.forEach((ability) => {
|
||||
if (model instanceof ability.model && target instanceof ability.target) {
|
||||
if (
|
||||
model instanceof ability.model &&
|
||||
target instanceof (ability.target as any)
|
||||
) {
|
||||
let response = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Share, Team, User } from "@server/models";
|
||||
import { allow, _can as can } from "./cancan";
|
||||
import { allow, can } from "./cancan";
|
||||
import { and, isOwner, isTeamModel, isTeamMutable, or } from "./utils";
|
||||
|
||||
allow(User, "createShare", Team, (actor, team) =>
|
||||
|
||||
59
server/typings/cancan.d.ts
vendored
59
server/typings/cancan.d.ts
vendored
@@ -1,59 +0,0 @@
|
||||
declare module "cancan" {
|
||||
import { Model } from "sequelize-typescript";
|
||||
|
||||
namespace CanCan {
|
||||
interface Option {
|
||||
instanceOf?: ((instance: any, model: any) => boolean) | undefined;
|
||||
createError?: (() => any) | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class CanCan {
|
||||
constructor(options?: CanCan.Option);
|
||||
|
||||
allow<
|
||||
T extends new (...args: any) => any,
|
||||
U extends new (...args: any) => any
|
||||
>(
|
||||
model: U,
|
||||
actions: string | ReadonlyArray<string>,
|
||||
targets: T | ReadonlyArray<T> | string | ReadonlyArray<string>,
|
||||
condition?:
|
||||
| object
|
||||
| ((
|
||||
performer: InstanceType<U>,
|
||||
target: InstanceType<T> | null,
|
||||
options?: any
|
||||
) => boolean)
|
||||
): void;
|
||||
|
||||
can(
|
||||
performer: Model,
|
||||
action: string,
|
||||
target: Model | null | undefined,
|
||||
options?: any
|
||||
): boolean;
|
||||
|
||||
cannot(
|
||||
performer: Model,
|
||||
action: string,
|
||||
target: Model | null | undefined,
|
||||
options?: any
|
||||
): boolean;
|
||||
|
||||
authorize(
|
||||
performer: Model,
|
||||
action: string,
|
||||
target: Model | null | undefined,
|
||||
options?: any
|
||||
): asserts target;
|
||||
|
||||
abilities: {
|
||||
model: any;
|
||||
action: string;
|
||||
target: any;
|
||||
}[];
|
||||
}
|
||||
|
||||
export = CanCan;
|
||||
}
|
||||
24
yarn.lock
24
yarn.lock
@@ -5602,11 +5602,6 @@ arraybuffer.prototype.slice@^1.0.3:
|
||||
is-array-buffer "^3.0.4"
|
||||
is-shared-array-buffer "^1.0.2"
|
||||
|
||||
arrify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="
|
||||
|
||||
asap@^2.0.0:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
@@ -5642,11 +5637,6 @@ attr-accept@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
|
||||
integrity "sha1-ZGYTgJZgEQdJ6S8sEIM7cJaNkps= sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
|
||||
|
||||
auto-bind@^1.1.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-1.2.1.tgz#807f7910b0210db9eefe133f3492c28e89698b96"
|
||||
integrity "sha1-gH95ELAhDbnu/hM/NJLCjolpi5Y= sha512-/W9yj1yKmBLwpexwAujeD9YHwYmRuWFGV8HWE7smQab797VeHa4/cnE2NFeDhA+E+5e/OGBI8763EhLjfZ/MXA=="
|
||||
|
||||
autotrack@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/autotrack/-/autotrack-2.4.1.tgz#ccbf010e3d95ef23c8dd6db4e8df025135c82ee6"
|
||||
@@ -6233,15 +6223,6 @@ camelize@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
|
||||
integrity "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= sha512-W2lPwkBkMZwFlPCXhIlYgxu+7gC/NUlCtdK652DAJ1JdgV0sTrvuPFshNPrFa1TY2JOkLhgdeEBplB4ezEa+xg=="
|
||||
|
||||
cancan@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cancan/-/cancan-3.1.0.tgz#4d148e73795324f689a9b1002e61839c17ea821e"
|
||||
integrity "sha1-TRSOc3lTJPaJqbEALmGDnBfqgh4= sha512-Glz6HEEOfQ5Cv5yWx2Zu4zPtDBJzNcIAE/pSzE3XTncA2ZvfwA5w8wLvJ455Ud4qKEGpHay4Z0KduGNWCoKPXA=="
|
||||
dependencies:
|
||||
arrify "^1.0.1"
|
||||
auto-bind "^1.1.0"
|
||||
is-plain-obj "^1.1.0"
|
||||
|
||||
caniuse-lite@^1.0.30001629:
|
||||
version "1.0.30001639"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz#972b3a6adeacdd8f46af5fc7f771e9639f6c1521"
|
||||
@@ -9721,11 +9702,6 @@ is-path-inside@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||
integrity "sha1-0jE2LlOgf/Kw4Op/7QSRYf/RYoM= sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
|
||||
|
||||
is-plain-obj@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
|
||||
integrity "sha1-caUMhCnfync8kqOQpKA7OfzVHT4= sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="
|
||||
|
||||
is-plain-obj@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.0.0.tgz#06c0999fd7574edf5a906ba5644ad0feb3a84d22"
|
||||
|
||||
Reference in New Issue
Block a user