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 {TableData} from 'app/client/models/TableData';
|
||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||||
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||||
|
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {textInput} from 'app/client/ui2018/editableLabel';
|
import {textInput} from 'app/client/ui2018/editableLabel';
|
||||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||||
import {IOptionFull, menu, menuItemAsync} from 'app/client/ui2018/menus';
|
import {IOptionFull, menu, menuItemAsync} from 'app/client/ui2018/menus';
|
||||||
import {emptyPermissionSet, MixedPermissionValue} from 'app/common/ACLPermissions';
|
import {emptyPermissionSet, MixedPermissionValue, PartialPermissionSet} from 'app/common/ACLPermissions';
|
||||||
import {PartialPermissionSet, permissionSetToText, summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
import {parsePermissions, permissionSetToText} from 'app/common/ACLPermissions';
|
||||||
import {ACLRuleCollection} from 'app/common/ACLRuleCollection';
|
import {summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
||||||
|
import {ACLRuleCollection, SPECIAL_RULES_TABLE_ID} from 'app/common/ACLRuleCollection';
|
||||||
import {ApiError} from 'app/common/ApiError';
|
import {ApiError} from 'app/common/ApiError';
|
||||||
import {BulkColValues, RowRecord, UserAction} from 'app/common/DocActions';
|
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 {isHiddenCol} from 'app/common/gristTypes';
|
||||||
import {isObject} from 'app/common/gutil';
|
import {isObject} from 'app/common/gutil';
|
||||||
import {SchemaTypes} from 'app/common/schema';
|
import {SchemaTypes} from 'app/common/schema';
|
||||||
import {BaseObservable, Computed, Disposable, MutableObsArray, obsArray, Observable} from 'grainjs';
|
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');
|
import isEqual = require('lodash/isEqual');
|
||||||
|
|
||||||
// tslint:disable:max-classes-per-file no-console
|
// 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 "*:*").
|
// The default rule set for the document (for "*:*").
|
||||||
private _docDefaultRuleSet = Observable.create<DefaultObsRuleSet|null>(this, null);
|
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.
|
// Array of all UserAttribute rules.
|
||||||
private _userAttrRules = this.autoDispose(obsArray<ObsUserAttributeRule>());
|
private _userAttrRules = this.autoDispose(obsArray<ObsUserAttributeRule>());
|
||||||
|
|
||||||
@ -90,6 +96,7 @@ export class AccessRules extends Disposable {
|
|||||||
this._ruleStatus = Computed.create(this, (use) => {
|
this._ruleStatus = Computed.create(this, (use) => {
|
||||||
const defRuleSet = use(this._docDefaultRuleSet);
|
const defRuleSet = use(this._docDefaultRuleSet);
|
||||||
const tableRules = use(this._tableRules);
|
const tableRules = use(this._tableRules);
|
||||||
|
const specialRules = use(this._specialRules);
|
||||||
const userAttr = use(this._userAttrRules);
|
const userAttr = use(this._userAttrRules);
|
||||||
return Math.max(
|
return Math.max(
|
||||||
defRuleSet ? use(defRuleSet.ruleStatus) : RuleStatus.Unchanged,
|
defRuleSet ? use(defRuleSet.ruleStatus) : RuleStatus.Unchanged,
|
||||||
@ -99,6 +106,7 @@ export class AccessRules extends Disposable {
|
|||||||
getChangedStatus(userAttr.length < this._ruleCollection.getUserAttributeRules().size),
|
getChangedStatus(userAttr.length < this._ruleCollection.getUserAttributeRules().size),
|
||||||
...tableRules.map(t => use(t.ruleStatus)),
|
...tableRules.map(t => use(t.ruleStatus)),
|
||||||
...userAttr.map(u => use(u.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(
|
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)))
|
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());
|
DefaultObsRuleSet.create(this._docDefaultRuleSet, this, null, undefined, rules.getDocDefaultRuleSet());
|
||||||
this._userAttrRules.set(
|
this._userAttrRules.set(
|
||||||
Array.from(rules.getUserAttributeRules().values(), userAttr =>
|
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.
|
// Add/remove resources to have just the ones we need.
|
||||||
const newResources: RowRecord[] = flatten(
|
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}));
|
.map(r => ({id: -1, ...r}));
|
||||||
|
|
||||||
// Prepare userActions and a mapping of serializedResource to rowIds.
|
// 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()),
|
dom.maybe(this._docDefaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
|
||||||
),
|
),
|
||||||
testId('rule-table'),
|
testId('rule-table'),
|
||||||
)
|
),
|
||||||
|
dom.maybe(this._specialRules, tableRules => tableRules.buildDom()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -357,6 +375,7 @@ export class AccessRules extends Disposable {
|
|||||||
public getRules(): RuleRec[] {
|
public getRules(): RuleRec[] {
|
||||||
return flatten(
|
return flatten(
|
||||||
...this._tableRules.get().map(t => t.getRules()),
|
...this._tableRules.get().map(t => t.getRules()),
|
||||||
|
this._specialRules.get()?.getRules() || [],
|
||||||
this._docDefaultRuleSet.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.
|
// 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.
|
// 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 {
|
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];
|
const tableColIds = this._aclResources[tableId];
|
||||||
if (!tableColIds) { return `Invalid table: ${tableId}`; }
|
if (!tableColIds) { return `Invalid table: ${tableId}`; }
|
||||||
if (colIds) {
|
if (colIds) {
|
||||||
@ -415,7 +434,7 @@ class TableRules extends Disposable {
|
|||||||
public ruleStatus: Computed<RuleStatus>;
|
public ruleStatus: Computed<RuleStatus>;
|
||||||
|
|
||||||
// The column-specific rule sets.
|
// 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.
|
// Whether there are any column-specific rule sets.
|
||||||
private _haveColumnRules = Computed.create(this, this._columnRuleSets, (use, cols) => cols.length > 0);
|
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) {
|
private _colRuleSets?: RuleSet[], private _defRuleSet?: RuleSet) {
|
||||||
super();
|
super();
|
||||||
this._columnRuleSets.set(this._colRuleSets?.map(rs =>
|
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)) || []);
|
rs.colIds === '*' ? [] : rs.colIds)) || []);
|
||||||
|
|
||||||
if (!this._colRuleSets) {
|
if (!this._colRuleSets) {
|
||||||
@ -479,14 +498,24 @@ class TableRules extends Disposable {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dom.forEach(this._columnRuleSets, ruleSet => ruleSet.buildRuleSetDom()),
|
this.buildColumnRuleSets(),
|
||||||
dom.maybe(this._defaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
|
|
||||||
),
|
),
|
||||||
dom.forEach(this._columnRuleSets, c => cssConditionError(dom.text(c.formulaError))),
|
this.buildErrors(),
|
||||||
testId('rule-table'),
|
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
|
* Return the resources (tableId:colIds entities), for saving, checking along the way that they
|
||||||
* are valid.
|
* 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() {
|
private _addColumnRuleSet() {
|
||||||
this._columnRuleSets.push(ColumnObsRuleSet.create(this._columnRuleSets, this._accessRules, this, undefined, []));
|
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
|
// Represents one RuleSet, for a combination of columns in one table, or the default RuleSet for
|
||||||
// all remaining columns in a table.
|
// all remaining columns in a table.
|
||||||
abstract class ObsRuleSet extends Disposable {
|
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
|
// List of individual rule parts for this entity. The default permissions may be included as the
|
||||||
// last rule part, with an empty aclFormula.
|
// 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.
|
// ruleSet is omitted for a new ObsRuleSet added by the user.
|
||||||
constructor(public accessRules: AccessRules, protected _tableRules: TableRules|null, private _ruleSet?: RuleSet) {
|
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".
|
* "Everyone".
|
||||||
*/
|
*/
|
||||||
public isSoleCondition(use: UseCB, part: ObsRulePart): boolean {
|
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 {
|
public isLastCondition(use: UseCB, part: ObsRulePart): boolean {
|
||||||
const body = use(this._body);
|
const body = use(this._body);
|
||||||
@ -698,6 +758,10 @@ abstract class ObsRuleSet extends Disposable {
|
|||||||
public hasColumns() {
|
public hasColumns() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasOnlyBuiltInRules() {
|
||||||
|
return this._body.get().every(rule => rule.isBuiltIn());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ColumnObsRuleSet extends ObsRuleSet {
|
class ColumnObsRuleSet extends ObsRuleSet {
|
||||||
@ -724,7 +788,7 @@ class ColumnObsRuleSet extends ObsRuleSet {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildResourceDom() {
|
public buildResourceDom(): DomElementArg {
|
||||||
return aclColumnList(this._colIds, this.getValidColIds());
|
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 {
|
class ObsUserAttributeRule extends Disposable {
|
||||||
public ruleStatus: Computed<RuleStatus>;
|
public ruleStatus: Computed<RuleStatus>;
|
||||||
|
|
||||||
@ -906,9 +1054,13 @@ class ObsRulePart extends Disposable {
|
|||||||
// Error message if any validation failed.
|
// Error message if any validation failed.
|
||||||
private _error: Computed<string>;
|
private _error: Computed<string>;
|
||||||
|
|
||||||
// rulePart is omitted for a new ObsRulePart added by the user.
|
// rulePart is omitted for a new ObsRulePart added by the user. If given, isNew may be set to
|
||||||
constructor(private _ruleSet: ObsRuleSet, private _rulePart?: RulePart) {
|
// treat the rule as new and only use the rulePart for its initialization.
|
||||||
|
constructor(private _ruleSet: ObsRuleSet, private _rulePart?: RulePart, isNew = false) {
|
||||||
super();
|
super();
|
||||||
|
if (_rulePart && isNew) {
|
||||||
|
this._rulePart = undefined;
|
||||||
|
}
|
||||||
this._error = Computed.create(this, (use) => {
|
this._error = Computed.create(this, (use) => {
|
||||||
return use(this._formulaError) ||
|
return use(this._formulaError) ||
|
||||||
( !this._ruleSet.isLastCondition(use, this) &&
|
( !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.
|
* 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,
|
* 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(
|
return cssColumnGroup(
|
||||||
cssCellIcon(
|
cssCellIcon(
|
||||||
(this._isNonFirstBuiltIn() ?
|
(this._isNonFirstBuiltIn() ?
|
||||||
@ -979,6 +1136,7 @@ class ObsRulePart extends Disposable {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
cssCell2(
|
cssCell2(
|
||||||
|
wide ? cssCell4.cls('') : null,
|
||||||
aclFormulaEditor({
|
aclFormulaEditor({
|
||||||
initialValue: this._aclFormula.get(),
|
initialValue: this._aclFormula.get(),
|
||||||
readOnly: this.isBuiltIn(),
|
readOnly: this.isBuiltIn(),
|
||||||
@ -1293,6 +1451,13 @@ const cssRuleBody = styled('div', `
|
|||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssRuleDescription = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
gap: 8px;
|
||||||
|
`);
|
||||||
|
|
||||||
const cssCellContent = styled('div', `
|
const cssCellContent = styled('div', `
|
||||||
margin: 4px 8px;
|
margin: 4px 8px;
|
||||||
`);
|
`);
|
||||||
|
@ -8,6 +8,8 @@ import sortBy = require('lodash/sortBy');
|
|||||||
|
|
||||||
const defaultMatchFunc: AclMatchFunc = () => true;
|
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.
|
// This is the hard-coded default RuleSet that's added to any user-created default rule.
|
||||||
const DEFAULT_RULE_SET: RuleSet = {
|
const DEFAULT_RULE_SET: RuleSet = {
|
||||||
tableId: '*',
|
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.
|
// 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.
|
// It grants full access to owners, and no access to anyone else.
|
||||||
const EMERGENCY_RULE_SET: RuleSet = {
|
const EMERGENCY_RULE_SET: RuleSet = {
|
||||||
@ -59,6 +94,7 @@ export class ACLRuleCollection {
|
|||||||
private _haveRules = false;
|
private _haveRules = false;
|
||||||
|
|
||||||
// Map of tableId to list of column RuleSets (those with colIds other than '*')
|
// 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[]>();
|
private _columnRuleSets = new Map<string, RuleSet[]>();
|
||||||
|
|
||||||
// Maps 'tableId:colId' to one of the RuleSets in the list _columnRuleSets.get(tableId).
|
// 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>();
|
const tableIds = new Set<string>();
|
||||||
let defaultRuleSet: RuleSet = DEFAULT_RULE_SET;
|
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);
|
this._haveRules = (ruleSets.length > 0);
|
||||||
for (const ruleSet of ruleSets) {
|
for (const ruleSet of ruleSets) {
|
||||||
if (ruleSet.tableId === '*') {
|
if (ruleSet.tableId === '*') {
|
||||||
@ -142,6 +196,8 @@ export class ACLRuleCollection {
|
|||||||
// tableId of '*' cannot list particular columns.
|
// tableId of '*' cannot list particular columns.
|
||||||
throw new Error(`Invalid rule for tableId ${ruleSet.tableId}, colIds ${ruleSet.colIds}`);
|
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 === '*') {
|
} else if (ruleSet.colIds === '*') {
|
||||||
tableIds.add(ruleSet.tableId);
|
tableIds.add(ruleSet.tableId);
|
||||||
if (tableRuleSets.has(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 { DocClients } from 'app/server/lib/DocClients';
|
||||||
import { getDocSessionAccess, getDocSessionUser, OptDocSession } from 'app/server/lib/DocSession';
|
import { getDocSessionAccess, getDocSessionUser, OptDocSession } from 'app/server/lib/DocSession';
|
||||||
import * as log from 'app/server/lib/log';
|
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 { integerParam } from 'app/server/lib/requestUtils';
|
||||||
import { getRelatedRows, getRowIdsFromDocAction } from 'app/server/lib/RowAccess';
|
import { getRelatedRows, getRowIdsFromDocAction } from 'app/server/lib/RowAccess';
|
||||||
import cloneDeep = require('lodash/cloneDeep');
|
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
|
// affected rows for the relevant table before and after each DocAction. It
|
||||||
// may contain some unaffected rows as well. Other metadata is included if
|
// may contain some unaffected rows as well. Other metadata is included if
|
||||||
// needed.
|
// 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.
|
// Access control is done sequentially, bundle by bundle. This is the current bundle.
|
||||||
private _activeBundle: {
|
private _activeBundle: {
|
||||||
docSession: OptDocSession,
|
docSession: OptDocSession,
|
||||||
@ -173,7 +174,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
|||||||
* Update granular access from DocData.
|
* Update granular access from DocData.
|
||||||
*/
|
*/
|
||||||
public async update() {
|
public async update() {
|
||||||
this._ruler.update(this._docData);
|
await this._ruler.update(this._docData);
|
||||||
|
|
||||||
// Also clear the per-docSession cache of user attributes.
|
// Also clear the per-docSession cache of user attributes.
|
||||||
this._userAttributesMap = new WeakMap();
|
this._userAttributesMap = new WeakMap();
|
||||||
|
Loading…
Reference in New Issue
Block a user