diff options
Diffstat (limited to 'src/fields/util.ts')
-rw-r--r-- | src/fields/util.ts | 191 |
1 files changed, 175 insertions, 16 deletions
diff --git a/src/fields/util.ts b/src/fields/util.ts index 54e7eca28..4c71572db 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,14 +1,16 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action, trace } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; -import { ScriptCast } from "./Types"; +import { ScriptCast, StrCast } from "./Types"; +import { returnZero } from "../Utils"; + function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -33,7 +35,6 @@ export namespace Plugins { } const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { - //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value); if (SerializationHelper.IsSerializing()) { target[prop] = value; return true; @@ -66,18 +67,25 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number delete curValue[Parent]; delete curValue[OnUpdate]; } + + const effectiveAcl = GetEffectiveAcl(target); + const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); - const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default); + const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly); + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode; + if (writeToDoc) { if (value === undefined) { + target.__fieldKeys && (delete target.__fieldKeys[prop]); delete target.__fields[prop]; } else { + target.__fieldKeys && (target.__fieldKeys[prop] = true); target.__fields[prop] = value; } - if (typeof value === "object" && !(value instanceof ObjectField)) debugger; + //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; + if (writeToServer) { if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } }); else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } }); @@ -88,8 +96,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue }); + return true; } - return true; + return false; }); let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl; @@ -101,12 +110,163 @@ export function makeReadOnly() { export function makeEditable() { _setter = _setterImpl; } +var _overrideAcl = false; +export function OVERRIDE_ACL(val: boolean) { + _overrideAcl = val; +} + +// playground mode allows the user to add/delete documents or make layout changes without them saving to the server +// let playgroundMode = false; + +// export function togglePlaygroundMode() { +// playgroundMode = !playgroundMode; +// } + +// the list of groups that the current user is a member of +let currentUserGroups: string[] = []; + +// called from GroupManager once the groups have been fetched from the server +export function setGroups(groups: string[]) { + currentUserGroups = groups; +} + +/** + * These are the various levels of access a user can have to a document. + * + * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. + * + * Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document. + * + * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything. + * + * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. + * + * None: the document is not shared with that user. + */ +export enum SharingPermissions { + Admin = "Admin", + Edit = "Can Edit", + Add = "Can Add", + View = "Can View", + None = "Not Shared" +} + +/** + * Calculates the effective access right to a document for the current user. + */ +export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { + if (!target) return AclPrivate; + if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; + + if (target[AclSym] && Object.keys(target[AclSym]).length) { + + // if the current user is the author of the document / the current user is a member of the admin group + // but not if the doc in question is an alias - the current user will be the author of their alias rather than the original author + if ((Doc.CurrentUserEmail === (target.__fields?.author || target.author) && !(target.aliasOf || target.__fields?.aliasOf)) || currentUserGroups.includes("admin")) return AclAdmin; + + // if the ACL is being overriden or the property being modified is one of the playground fields (which can be freely modified) + if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; + + let effectiveAcl = AclPrivate; + const HierarchyMapping = new Map<symbol, number>([ + [AclPrivate, 0], + [AclReadonly, 1], + [AclAddonly, 2], + [AclEdit, 3], + [AclAdmin, 4] + ]); + + for (const [key, value] of Object.entries(target[AclSym])) { + // there are issues with storing fields with . in the name, so they are replaced with _ during creation + // as a result we need to restore them again during this comparison. + if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) { + if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { + effectiveAcl = value as symbol; + if (effectiveAcl === AclAdmin) break; + } + } + } + // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) + return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl; + } + return AclAdmin; +} +/** + * Recursively distributes the access right for a user across the children of a document and its annotations. + * @param key the key storing the access right (e.g. ACL-groupname) + * @param acl the access right being stored (e.g. "Can Edit") + * @param target the document on which this access right is being set + * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the ACLs from the collection) + * inheritingFromCollection is not currently being used but could be used if ACL assignment defaults change + */ +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { + + const HierarchyMapping = new Map<string, number>([ + ["Not Shared", 0], + ["Can View", 1], + ["Can Add", 2], + ["Can Edit", 3], + ["Admin", 4] + ]); + + let changed = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) + const dataDoc = target[DataSym]; + + // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive + if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) { + target[key] = acl; + changed = true; + + // maps over the aliases of the document + if (target.aliases) { + DocListCast(target.aliases).map(alias => { + distributeAcls(key, acl, alias, inheritingFromCollection); + }); + } + + } + + if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { + dataDoc[key] = acl; + changed = true; + + // maps over the children of the document + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { + if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d, inheritingFromCollection); + } + const data = d[DataSym]; + if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, data, inheritingFromCollection); + } + }); + + // maps over the annotations of the document + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { + if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d, inheritingFromCollection); + } + const data = d[DataSym]; + if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, data, inheritingFromCollection); + } + }); + } + + changed && fetchProto(target); // updates target[AclSym] when changes to acls have been made +} const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", - "LODdisable", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; + "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - if (target[AclSym]) return true; + const effectiveAcl = GetEffectiveAcl(target, in_prop); + if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; + + // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't + if (typeof prop === "string" && prop.startsWith("ACL") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; + // if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; + if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); @@ -125,8 +285,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === AclSym) return target[AclSym]; - if (target[AclSym] === AclPrivate) return undefined; + + if (in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; + if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; + if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; if (prop === LayoutSym) { return target.__LAYOUT__; } @@ -151,9 +313,6 @@ export function getter(target: any, in_prop: string | symbol | number, receiver: function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { receiver = receiver || target[SelfProxy]; - if (target === undefined) { - console.log(""); - } let field = target.__fields[prop]; for (const plugin of getterPlugins) { const res = plugin(receiver, prop, field); @@ -166,7 +325,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP } if (field === undefined && !ignoreProto && prop !== "proto") { const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters - if (proto instanceof Doc && proto[AclSym] !== AclPrivate) { + if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { return getFieldImpl(proto[Self], prop, receiver, ignoreProto); } return undefined; |