mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add support for special ACL rules, for viewing rules and downloading documents.
Summary: - Use special ACLResources of the form "*SPECIAL:<RuleType>" to represent special document-wide rules. - Include default rules that give Read access to these resources to Owners only. - Add UI with a checkbox to give access to everyone instead. - Allow expanding the UI for advanced configuration. - These rules don't actually have any behavior yet. Test Plan: WIP Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2764
This commit is contained in:
parent
d8df2404c2
commit
e14488bcc8
@ -11,21 +11,24 @@ import {reportError, UserError} from 'app/client/models/errors';
|
||||
import {TableData} from 'app/client/models/TableData';
|
||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {textInput} from 'app/client/ui2018/editableLabel';
|
||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||
import {IOptionFull, menu, menuItemAsync} from 'app/client/ui2018/menus';
|
||||
import {emptyPermissionSet, MixedPermissionValue} from 'app/common/ACLPermissions';
|
||||
import {PartialPermissionSet, permissionSetToText, summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
||||
import {ACLRuleCollection} from 'app/common/ACLRuleCollection';
|
||||
import { ApiError } from 'app/common/ApiError';
|
||||
import {emptyPermissionSet, MixedPermissionValue, PartialPermissionSet} from 'app/common/ACLPermissions';
|
||||
import {parsePermissions, permissionSetToText} from 'app/common/ACLPermissions';
|
||||
import {summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
||||
import {ACLRuleCollection, SPECIAL_RULES_TABLE_ID} from 'app/common/ACLRuleCollection';
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import {BulkColValues, RowRecord, UserAction} from 'app/common/DocActions';
|
||||
import {FormulaProperties, getFormulaProperties, RulePart, RuleSet, UserAttributeRule} from 'app/common/GranularAccessClause';
|
||||
import {FormulaProperties, RulePart, RuleSet, UserAttributeRule} from 'app/common/GranularAccessClause';
|
||||
import {getFormulaProperties} from 'app/common/GranularAccessClause';
|
||||
import {isHiddenCol} from 'app/common/gristTypes';
|
||||
import {isObject} from 'app/common/gutil';
|
||||
import {SchemaTypes} from 'app/common/schema';
|
||||
import {BaseObservable, Computed, Disposable, MutableObsArray, obsArray, Observable} from 'grainjs';
|
||||
import {dom, DomElementArg, styled} from 'grainjs';
|
||||
import {dom, DomElementArg, IDisposableOwner, styled} from 'grainjs';
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
// tslint:disable:max-classes-per-file no-console
|
||||
@ -67,6 +70,9 @@ export class AccessRules extends Disposable {
|
||||
// The default rule set for the document (for "*:*").
|
||||
private _docDefaultRuleSet = Observable.create<DefaultObsRuleSet|null>(this, null);
|
||||
|
||||
// Special document-level rules, for resources of the form ("*SPECIAL:<RuleType>").
|
||||
private _specialRules = Observable.create<SpecialRules|null>(this, null);
|
||||
|
||||
// Array of all UserAttribute rules.
|
||||
private _userAttrRules = this.autoDispose(obsArray<ObsUserAttributeRule>());
|
||||
|
||||
@ -90,6 +96,7 @@ export class AccessRules extends Disposable {
|
||||
this._ruleStatus = Computed.create(this, (use) => {
|
||||
const defRuleSet = use(this._docDefaultRuleSet);
|
||||
const tableRules = use(this._tableRules);
|
||||
const specialRules = use(this._specialRules);
|
||||
const userAttr = use(this._userAttrRules);
|
||||
return Math.max(
|
||||
defRuleSet ? use(defRuleSet.ruleStatus) : RuleStatus.Unchanged,
|
||||
@ -99,6 +106,7 @@ export class AccessRules extends Disposable {
|
||||
getChangedStatus(userAttr.length < this._ruleCollection.getUserAttributeRules().size),
|
||||
...tableRules.map(t => use(t.ruleStatus)),
|
||||
...userAttr.map(u => use(u.ruleStatus)),
|
||||
specialRules ? use(specialRules.ruleStatus) : RuleStatus.Unchanged,
|
||||
);
|
||||
});
|
||||
|
||||
@ -163,9 +171,16 @@ export class AccessRules extends Disposable {
|
||||
]);
|
||||
|
||||
this._tableRules.set(
|
||||
rules.getAllTableIds().map(tableId => TableRules.create(this._tableRules,
|
||||
rules.getAllTableIds()
|
||||
.filter(tableId => (tableId !== SPECIAL_RULES_TABLE_ID))
|
||||
.map(tableId => TableRules.create(this._tableRules,
|
||||
tableId, this, rules.getAllColumnRuleSets(tableId), rules.getTableDefaultRuleSet(tableId)))
|
||||
);
|
||||
|
||||
SpecialRules.create(this._specialRules, SPECIAL_RULES_TABLE_ID, this,
|
||||
rules.getAllColumnRuleSets(SPECIAL_RULES_TABLE_ID),
|
||||
rules.getTableDefaultRuleSet(SPECIAL_RULES_TABLE_ID));
|
||||
|
||||
DefaultObsRuleSet.create(this._docDefaultRuleSet, this, null, undefined, rules.getDocDefaultRuleSet());
|
||||
this._userAttrRules.set(
|
||||
Array.from(rules.getUserAttributeRules().values(), userAttr =>
|
||||
@ -188,7 +203,9 @@ export class AccessRules extends Disposable {
|
||||
|
||||
// Add/remove resources to have just the ones we need.
|
||||
const newResources: RowRecord[] = flatten(
|
||||
[{tableId: '*', colIds: '*'}], ...this._tableRules.get().map(t => t.getResources()))
|
||||
[{tableId: '*', colIds: '*'}],
|
||||
this._specialRules.get()?.getResources() || [],
|
||||
...this._tableRules.get().map(t => t.getResources()))
|
||||
.map(r => ({id: -1, ...r}));
|
||||
|
||||
// Prepare userActions and a mapping of serializedResource to rowIds.
|
||||
@ -346,7 +363,8 @@ export class AccessRules extends Disposable {
|
||||
dom.maybe(this._docDefaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
|
||||
),
|
||||
testId('rule-table'),
|
||||
)
|
||||
),
|
||||
dom.maybe(this._specialRules, tableRules => tableRules.buildDom()),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -357,6 +375,7 @@ export class AccessRules extends Disposable {
|
||||
public getRules(): RuleRec[] {
|
||||
return flatten(
|
||||
...this._tableRules.get().map(t => t.getRules()),
|
||||
this._specialRules.get()?.getRules() || [],
|
||||
this._docDefaultRuleSet.get()?.getRules('*') || []
|
||||
);
|
||||
}
|
||||
@ -379,7 +398,7 @@ export class AccessRules extends Disposable {
|
||||
// Check if the given tableId, and optionally a list of colIds, are present in this document.
|
||||
// Returns '' if valid, or an error string if not. Exempt colIds will not trigger an error.
|
||||
public checkTableColumns(tableId: string, colIds?: string[], exemptColIds?: string[]): string {
|
||||
if (!tableId) { return ''; }
|
||||
if (!tableId || tableId === SPECIAL_RULES_TABLE_ID) { return ''; }
|
||||
const tableColIds = this._aclResources[tableId];
|
||||
if (!tableColIds) { return `Invalid table: ${tableId}`; }
|
||||
if (colIds) {
|
||||
@ -415,7 +434,7 @@ class TableRules extends Disposable {
|
||||
public ruleStatus: Computed<RuleStatus>;
|
||||
|
||||
// The column-specific rule sets.
|
||||
private _columnRuleSets = this.autoDispose(obsArray<ColumnObsRuleSet>());
|
||||
protected _columnRuleSets = this.autoDispose(obsArray<ColumnObsRuleSet>());
|
||||
|
||||
// Whether there are any column-specific rule sets.
|
||||
private _haveColumnRules = Computed.create(this, this._columnRuleSets, (use, cols) => cols.length > 0);
|
||||
@ -427,7 +446,7 @@ class TableRules extends Disposable {
|
||||
private _colRuleSets?: RuleSet[], private _defRuleSet?: RuleSet) {
|
||||
super();
|
||||
this._columnRuleSets.set(this._colRuleSets?.map(rs =>
|
||||
ColumnObsRuleSet.create(this._columnRuleSets, this._accessRules, this, rs,
|
||||
this._createColumnObsRuleSet(this._columnRuleSets, this._accessRules, this, rs,
|
||||
rs.colIds === '*' ? [] : rs.colIds)) || []);
|
||||
|
||||
if (!this._colRuleSets) {
|
||||
@ -479,14 +498,24 @@ class TableRules extends Disposable {
|
||||
)
|
||||
),
|
||||
),
|
||||
dom.forEach(this._columnRuleSets, ruleSet => ruleSet.buildRuleSetDom()),
|
||||
dom.maybe(this._defaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
|
||||
this.buildColumnRuleSets(),
|
||||
),
|
||||
dom.forEach(this._columnRuleSets, c => cssConditionError(dom.text(c.formulaError))),
|
||||
this.buildErrors(),
|
||||
testId('rule-table'),
|
||||
);
|
||||
}
|
||||
|
||||
public buildColumnRuleSets() {
|
||||
return [
|
||||
dom.forEach(this._columnRuleSets, ruleSet => ruleSet.buildRuleSetDom()),
|
||||
dom.maybe(this._defaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
|
||||
];
|
||||
}
|
||||
|
||||
public buildErrors() {
|
||||
return dom.forEach(this._columnRuleSets, c => cssConditionError(dom.text(c.formulaError)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resources (tableId:colIds entities), for saving, checking along the way that they
|
||||
* are valid.
|
||||
@ -557,6 +586,13 @@ class TableRules extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected _createColumnObsRuleSet(
|
||||
owner: IDisposableOwner, accessRules: AccessRules, tableRules: TableRules,
|
||||
ruleSet: RuleSet|undefined, initialColIds: string[],
|
||||
): ColumnObsRuleSet {
|
||||
return ColumnObsRuleSet.create(owner, accessRules, tableRules, ruleSet, initialColIds);
|
||||
}
|
||||
|
||||
private _addColumnRuleSet() {
|
||||
this._columnRuleSets.push(ColumnObsRuleSet.create(this._columnRuleSets, this._accessRules, this, undefined, []));
|
||||
}
|
||||
@ -568,6 +604,30 @@ class TableRules extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialRules extends TableRules {
|
||||
public buildDom() {
|
||||
return cssSection(
|
||||
cssSectionHeading('Special Rules', testId('rule-table-header')),
|
||||
this.buildColumnRuleSets(),
|
||||
this.buildErrors(),
|
||||
testId('rule-table'),
|
||||
);
|
||||
}
|
||||
|
||||
public getResources(): ResourceRec[] {
|
||||
return this._columnRuleSets.get()
|
||||
.filter(rs => !rs.hasOnlyBuiltInRules())
|
||||
.map(rs => ({tableId: this.tableId, colIds: rs.getColIds()}));
|
||||
}
|
||||
|
||||
protected _createColumnObsRuleSet(
|
||||
owner: IDisposableOwner, accessRules: AccessRules, tableRules: TableRules,
|
||||
ruleSet: RuleSet|undefined, initialColIds: string[],
|
||||
): ColumnObsRuleSet {
|
||||
return SpecialObsRuleSet.create(owner, accessRules, tableRules, ruleSet, initialColIds);
|
||||
}
|
||||
}
|
||||
|
||||
// Represents one RuleSet, for a combination of columns in one table, or the default RuleSet for
|
||||
// all remaining columns in a table.
|
||||
abstract class ObsRuleSet extends Disposable {
|
||||
@ -576,7 +636,7 @@ abstract class ObsRuleSet extends Disposable {
|
||||
|
||||
// List of individual rule parts for this entity. The default permissions may be included as the
|
||||
// last rule part, with an empty aclFormula.
|
||||
private _body = this.autoDispose(obsArray<ObsRulePart>());
|
||||
protected readonly _body = this.autoDispose(obsArray<ObsRulePart>());
|
||||
|
||||
// ruleSet is omitted for a new ObsRuleSet added by the user.
|
||||
constructor(public accessRules: AccessRules, protected _tableRules: TableRules|null, private _ruleSet?: RuleSet) {
|
||||
@ -656,7 +716,7 @@ abstract class ObsRuleSet extends Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* When an empty-conditition RulePart is the only part of a RuleSet, we can say it applies to
|
||||
* When an empty-condition RulePart is the only part of a RuleSet, we can say it applies to
|
||||
* "Everyone".
|
||||
*/
|
||||
public isSoleCondition(use: UseCB, part: ObsRulePart): boolean {
|
||||
@ -665,7 +725,7 @@ abstract class ObsRuleSet extends Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* When an empty-conditition RulePart is last in a RuleSet, we say it applies to "Everyone Else".
|
||||
* When an empty-condition RulePart is last in a RuleSet, we say it applies to "Everyone Else".
|
||||
*/
|
||||
public isLastCondition(use: UseCB, part: ObsRulePart): boolean {
|
||||
const body = use(this._body);
|
||||
@ -698,6 +758,10 @@ abstract class ObsRuleSet extends Disposable {
|
||||
public hasColumns() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public hasOnlyBuiltInRules() {
|
||||
return this._body.get().every(rule => rule.isBuiltIn());
|
||||
}
|
||||
}
|
||||
|
||||
class ColumnObsRuleSet extends ObsRuleSet {
|
||||
@ -724,7 +788,7 @@ class ColumnObsRuleSet extends ObsRuleSet {
|
||||
});
|
||||
}
|
||||
|
||||
public buildResourceDom() {
|
||||
public buildResourceDom(): DomElementArg {
|
||||
return aclColumnList(this._colIds, this.getValidColIds());
|
||||
}
|
||||
|
||||
@ -761,6 +825,90 @@ class DefaultObsRuleSet extends ObsRuleSet {
|
||||
}
|
||||
}
|
||||
|
||||
function getSpecialRuleDescription(type: string): string {
|
||||
switch (type) {
|
||||
case 'AccessRules':
|
||||
return 'Allow everyone to view Access Rules';
|
||||
case 'FullCopies':
|
||||
return 'Allow everyone to download and copy the full document even if they have incomplete access to it';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
function getSpecialRuleName(type: string): string {
|
||||
switch (type) {
|
||||
case 'AccessRules': return 'Permission to view Access Rules';
|
||||
case 'FullCopies': return 'Permission to download and copy the document in full';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialObsRuleSet extends ColumnObsRuleSet {
|
||||
public buildRuleSetDom() {
|
||||
const isNonStandard: Observable<boolean> = Computed.create(null, this._body, (use, body) =>
|
||||
!body.every(rule => rule.isBuiltIn() || rule.matches(use, 'True', '+R')));
|
||||
|
||||
const allowEveryone: Observable<boolean> = Computed.create(null, this._body,
|
||||
(use, body) => !use(isNonStandard) && !body.every(rule => rule.isBuiltIn()))
|
||||
.onWrite(val => this._allowEveryone(val));
|
||||
|
||||
const isExpanded = Observable.create<boolean>(null, isNonStandard.get());
|
||||
|
||||
return dom('div',
|
||||
dom.autoDispose(isExpanded),
|
||||
dom.autoDispose(allowEveryone),
|
||||
cssRuleDescription(
|
||||
cssIconButton(icon('Expand'),
|
||||
dom.style('transform', (use) => use(isExpanded) ? 'rotate(90deg)' : ''),
|
||||
dom.on('click', () => isExpanded.set(!isExpanded.get())),
|
||||
testId('rule-special-expand'),
|
||||
),
|
||||
squareCheckbox(allowEveryone,
|
||||
dom.prop('disabled', isNonStandard),
|
||||
testId('rule-special-checkbox'),
|
||||
),
|
||||
getSpecialRuleDescription(this.getColIds()),
|
||||
),
|
||||
dom.maybe(isExpanded, () =>
|
||||
cssTableRounded(
|
||||
{style: 'margin-left: 56px'},
|
||||
cssTableHeaderRow(
|
||||
cssCellIcon(),
|
||||
cssCell4(cssColHeaderCell(getSpecialRuleName(this.getColIds()))),
|
||||
cssCell1(cssColHeaderCell('Permissions')),
|
||||
cssCellIcon(),
|
||||
),
|
||||
cssTableRow(
|
||||
cssRuleBody.cls(''),
|
||||
dom.forEach(this._body, part => part.buildRulePartDom(true)),
|
||||
),
|
||||
testId('rule-set'),
|
||||
)
|
||||
),
|
||||
testId('rule-special'),
|
||||
testId(`rule-special-${this.getColIds()}`), // Make accessible in tests as, e.g. rule-special-FullCopies
|
||||
);
|
||||
}
|
||||
|
||||
public getAvailableBits(): PermissionKey[] {
|
||||
return ['read'];
|
||||
}
|
||||
|
||||
private _allowEveryone(value: boolean) {
|
||||
const builtInRules = this._body.get().filter(r => r.isBuiltIn());
|
||||
if (value === true) {
|
||||
const rulePart: RulePart = {
|
||||
aclFormula: 'True',
|
||||
permissionsText: '+R',
|
||||
permissions: parsePermissions('+R'),
|
||||
};
|
||||
this._body.set([ObsRulePart.create(this._body, this, rulePart, true), ...builtInRules]);
|
||||
} else if (value === false) {
|
||||
this._body.set(builtInRules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ObsUserAttributeRule extends Disposable {
|
||||
public ruleStatus: Computed<RuleStatus>;
|
||||
|
||||
@ -906,9 +1054,13 @@ class ObsRulePart extends Disposable {
|
||||
// Error message if any validation failed.
|
||||
private _error: Computed<string>;
|
||||
|
||||
// rulePart is omitted for a new ObsRulePart added by the user.
|
||||
constructor(private _ruleSet: ObsRuleSet, private _rulePart?: RulePart) {
|
||||
// rulePart is omitted for a new ObsRulePart added by the user. If given, isNew may be set to
|
||||
// treat the rule as new and only use the rulePart for its initialization.
|
||||
constructor(private _ruleSet: ObsRuleSet, private _rulePart?: RulePart, isNew = false) {
|
||||
super();
|
||||
if (_rulePart && isNew) {
|
||||
this._rulePart = undefined;
|
||||
}
|
||||
this._error = Computed.create(this, (use) => {
|
||||
return use(this._formulaError) ||
|
||||
( !this._ruleSet.isLastCondition(use, this) &&
|
||||
@ -939,6 +1091,11 @@ class ObsRulePart extends Disposable {
|
||||
};
|
||||
}
|
||||
|
||||
public matches(use: UseCB, aclFormula: string, permissionsText: string): boolean {
|
||||
return (use(this._aclFormula) === aclFormula &&
|
||||
permissionSetToText(use(this._permissions)) === permissionsText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if RulePart may only add permissions, only remove permissions, or may do either.
|
||||
* A rule that neither adds nor removes permissions is treated as mixed for simplicity,
|
||||
@ -967,7 +1124,7 @@ class ObsRulePart extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
public buildRulePartDom() {
|
||||
public buildRulePartDom(wide: boolean = false) {
|
||||
return cssColumnGroup(
|
||||
cssCellIcon(
|
||||
(this._isNonFirstBuiltIn() ?
|
||||
@ -979,6 +1136,7 @@ class ObsRulePart extends Disposable {
|
||||
),
|
||||
),
|
||||
cssCell2(
|
||||
wide ? cssCell4.cls('') : null,
|
||||
aclFormulaEditor({
|
||||
initialValue: this._aclFormula.get(),
|
||||
readOnly: this.isBuiltIn(),
|
||||
@ -1293,6 +1451,13 @@ const cssRuleBody = styled('div', `
|
||||
margin: 4px 0;
|
||||
`);
|
||||
|
||||
const cssRuleDescription = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16px 0 8px 0;
|
||||
gap: 8px;
|
||||
`);
|
||||
|
||||
const cssCellContent = styled('div', `
|
||||
margin: 4px 8px;
|
||||
`);
|
||||
|
@ -8,6 +8,8 @@ import sortBy = require('lodash/sortBy');
|
||||
|
||||
const defaultMatchFunc: AclMatchFunc = () => true;
|
||||
|
||||
export const SPECIAL_RULES_TABLE_ID = '*SPECIAL';
|
||||
|
||||
// This is the hard-coded default RuleSet that's added to any user-created default rule.
|
||||
const DEFAULT_RULE_SET: RuleSet = {
|
||||
tableId: '*',
|
||||
@ -30,6 +32,39 @@ const DEFAULT_RULE_SET: RuleSet = {
|
||||
}],
|
||||
};
|
||||
|
||||
const SPECIAL_RULE_SETS: Record<string, RuleSet> = {
|
||||
AccessRules: {
|
||||
tableId: SPECIAL_RULES_TABLE_ID,
|
||||
colIds: ['AccessRules'],
|
||||
body: [{
|
||||
aclFormula: "user.Access in [OWNER]",
|
||||
matchFunc: (input) => ['owners'].includes(String(input.user.Access)),
|
||||
permissions: parsePermissions('+R'),
|
||||
permissionsText: '+R',
|
||||
}, {
|
||||
aclFormula: "",
|
||||
matchFunc: defaultMatchFunc,
|
||||
permissions: parsePermissions('none'),
|
||||
permissionsText: 'none',
|
||||
}],
|
||||
},
|
||||
FullCopies: {
|
||||
tableId: SPECIAL_RULES_TABLE_ID,
|
||||
colIds: ['FullCopies'],
|
||||
body: [{
|
||||
aclFormula: "user.Access in [OWNER]",
|
||||
matchFunc: (input) => ['owners'].includes(String(input.user.Access)),
|
||||
permissions: parsePermissions('+R'),
|
||||
permissionsText: '+R',
|
||||
}, {
|
||||
aclFormula: "",
|
||||
matchFunc: defaultMatchFunc,
|
||||
permissions: parsePermissions('none'),
|
||||
permissionsText: 'none',
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
// If the user-created rules become dysfunctional, we can swap in this emergency set.
|
||||
// It grants full access to owners, and no access to anyone else.
|
||||
const EMERGENCY_RULE_SET: RuleSet = {
|
||||
@ -59,6 +94,7 @@ export class ACLRuleCollection {
|
||||
private _haveRules = false;
|
||||
|
||||
// Map of tableId to list of column RuleSets (those with colIds other than '*')
|
||||
// Includes also SPECIAL_RULES_TABLE_ID.
|
||||
private _columnRuleSets = new Map<string, RuleSet[]>();
|
||||
|
||||
// Maps 'tableId:colId' to one of the RuleSets in the list _columnRuleSets.get(tableId).
|
||||
@ -130,6 +166,24 @@ export class ACLRuleCollection {
|
||||
const tableIds = new Set<string>();
|
||||
let defaultRuleSet: RuleSet = DEFAULT_RULE_SET;
|
||||
|
||||
// Collect special rules, combining them with corresponding defaults.
|
||||
const specialRuleSets = new Map<string, RuleSet>(Object.entries(SPECIAL_RULE_SETS));
|
||||
for (const ruleSet of ruleSets) {
|
||||
if (ruleSet.tableId === SPECIAL_RULES_TABLE_ID) {
|
||||
const specialType = String(ruleSet.colIds);
|
||||
const specialDefault = specialRuleSets.get(specialType);
|
||||
if (!specialDefault) {
|
||||
throw new Error(`Invalid rule for ${ruleSet.tableId}:${ruleSet.colIds}`);
|
||||
}
|
||||
specialRuleSets.set(specialType, {...ruleSet, body: [...ruleSet.body, ...specialDefault.body]});
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the special rule sets into colRuleSets.
|
||||
for (const ruleSet of specialRuleSets.values()) {
|
||||
getSetMapValue(colRuleSets, SPECIAL_RULES_TABLE_ID, () => []).push(ruleSet);
|
||||
}
|
||||
|
||||
this._haveRules = (ruleSets.length > 0);
|
||||
for (const ruleSet of ruleSets) {
|
||||
if (ruleSet.tableId === '*') {
|
||||
@ -142,6 +196,8 @@ export class ACLRuleCollection {
|
||||
// tableId of '*' cannot list particular columns.
|
||||
throw new Error(`Invalid rule for tableId ${ruleSet.tableId}, colIds ${ruleSet.colIds}`);
|
||||
}
|
||||
} else if (ruleSet.tableId === SPECIAL_RULES_TABLE_ID) {
|
||||
// Skip, since we handled these separately earlier.
|
||||
} else if (ruleSet.colIds === '*') {
|
||||
tableIds.add(ruleSet.tableId);
|
||||
if (tableRuleSets.has(ruleSet.tableId)) {
|
||||
|
@ -21,7 +21,8 @@ import { compileAclFormula } from 'app/server/lib/ACLFormula';
|
||||
import { DocClients } from 'app/server/lib/DocClients';
|
||||
import { getDocSessionAccess, getDocSessionUser, OptDocSession } from 'app/server/lib/DocSession';
|
||||
import * as log from 'app/server/lib/log';
|
||||
import { IPermissionInfo, PermissionInfo, PermissionSetWithContext, TablePermissionSetWithContext } from 'app/server/lib/PermissionInfo';
|
||||
import { IPermissionInfo, PermissionInfo, PermissionSetWithContext } from 'app/server/lib/PermissionInfo';
|
||||
import { TablePermissionSetWithContext } from 'app/server/lib/PermissionInfo';
|
||||
import { integerParam } from 'app/server/lib/requestUtils';
|
||||
import { getRelatedRows, getRowIdsFromDocAction } from 'app/server/lib/RowAccess';
|
||||
import cloneDeep = require('lodash/cloneDeep');
|
||||
@ -135,7 +136,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
||||
// affected rows for the relevant table before and after each DocAction. It
|
||||
// may contain some unaffected rows as well. Other metadata is included if
|
||||
// needed.
|
||||
private _steps: Promise<Array<ActionStep>>|null = null;
|
||||
private _steps: Promise<ActionStep[]>|null = null;
|
||||
// Access control is done sequentially, bundle by bundle. This is the current bundle.
|
||||
private _activeBundle: {
|
||||
docSession: OptDocSession,
|
||||
@ -173,7 +174,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
||||
* Update granular access from DocData.
|
||||
*/
|
||||
public async update() {
|
||||
this._ruler.update(this._docData);
|
||||
await this._ruler.update(this._docData);
|
||||
|
||||
// Also clear the per-docSession cache of user attributes.
|
||||
this._userAttributesMap = new WeakMap();
|
||||
|
Loading…
Reference in New Issue
Block a user