mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Implement much of the general AccessRules UI.
Summary: - Factored out ACLRuleCollection into its own file, and use for building UI. - Moved AccessRules out of UserManager to a page linked from left panel. - Changed default RulePart to be the last part of a rule for simpler code. - Implemented much of the UI for adding/deleting rules. - For now, editing the ACLFormula and Permissions is done using text inputs. - Implemented saving rules by syncing a bundle of them. - Fixed DocData to clean up action bundle in case of an early error. Test Plan: WIP planning to add some new browser tests for the UI Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2678
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
import { emptyPermissionSet, parsePermissions, PartialPermissionSet } from 'app/common/ACLPermissions';
|
||||
import { ILogger } from 'app/common/BaseAPI';
|
||||
import { PartialPermissionSet } from 'app/common/ACLPermissions';
|
||||
import { CellValue, RowRecord } from 'app/common/DocActions';
|
||||
import { DocData } from 'app/common/DocData';
|
||||
import { getSetMapValue } from 'app/common/gutil';
|
||||
import sortBy = require('lodash/sortBy');
|
||||
|
||||
export interface RuleSet {
|
||||
tableId: '*' | string;
|
||||
colIds: '*' | string[];
|
||||
// The default permissions for this resource, if set, are represented by a RulePart with
|
||||
// aclFormula of "", which must be the last element of body.
|
||||
body: RulePart[];
|
||||
defaultPermissions: PartialPermissionSet;
|
||||
}
|
||||
|
||||
export interface RulePart {
|
||||
origRecord?: RowRecord; // Original record used to create this RulePart.
|
||||
aclFormula: string;
|
||||
permissions: PartialPermissionSet;
|
||||
permissionsText: string; // The text version of PermissionSet, as stored.
|
||||
@@ -50,90 +48,9 @@ export type AclMatchFunc = (input: AclMatchInput) => boolean;
|
||||
export type ParsedAclFormula = [string, ...Array<ParsedAclFormula|CellValue>];
|
||||
|
||||
export interface UserAttributeRule {
|
||||
origRecord?: RowRecord; // Original record used to create this UserAttributeRule.
|
||||
name: string; // Should be unique among UserAttributeRules.
|
||||
tableId: string; // Table in which to look up an existing attribute.
|
||||
lookupColId: string; // Column in tableId in which to do the lookup.
|
||||
charId: string; // Attribute to look up, possibly a path. E.g. 'Email' or 'office.city'.
|
||||
}
|
||||
|
||||
export interface ReadAclOptions {
|
||||
log: ILogger; // For logging warnings during rule processing.
|
||||
compile?: (parsed: ParsedAclFormula) => AclMatchFunc;
|
||||
}
|
||||
|
||||
export interface ReadAclResults {
|
||||
ruleSets: RuleSet[];
|
||||
userAttributes: UserAttributeRule[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all ACL rules in the document from DocData into a list of RuleSets and of
|
||||
* UserAttributeRules. This is used by both client-side code and server-side.
|
||||
*/
|
||||
export function readAclRules(docData: DocData, {log, compile}: ReadAclOptions): ReadAclResults {
|
||||
const resourcesTable = docData.getTable('_grist_ACLResources')!;
|
||||
const rulesTable = docData.getTable('_grist_ACLRules')!;
|
||||
|
||||
const ruleSets: RuleSet[] = [];
|
||||
const userAttributes: UserAttributeRule[] = [];
|
||||
|
||||
// Group rules by resource first, ordering by rulePos. Each group will become a RuleSet.
|
||||
const rulesByResource = new Map<number, RowRecord[]>();
|
||||
for (const ruleRecord of sortBy(rulesTable.getRecords(), 'rulePos')) {
|
||||
getSetMapValue(rulesByResource, ruleRecord.resource, () => []).push(ruleRecord);
|
||||
}
|
||||
|
||||
for (const [resourceId, rules] of rulesByResource.entries()) {
|
||||
const resourceRec = resourcesTable.getRecord(resourceId as number);
|
||||
if (!resourceRec) {
|
||||
log.error(`ACLRule ${rules[0].id} ignored; refers to an invalid ACLResource ${resourceId}`);
|
||||
continue;
|
||||
}
|
||||
if (!resourceRec.tableId || !resourceRec.colIds) {
|
||||
// This should only be the case for the old-style default rule/resource, which we
|
||||
// intentionally ignore and skip.
|
||||
continue;
|
||||
}
|
||||
const tableId = resourceRec.tableId as string;
|
||||
const colIds = resourceRec.colIds === '*' ? '*' : (resourceRec.colIds as string).split(',');
|
||||
|
||||
let defaultPermissions: PartialPermissionSet|undefined;
|
||||
const body: RulePart[] = [];
|
||||
for (const rule of rules) {
|
||||
if (rule.userAttributes) {
|
||||
if (tableId !== '*' || colIds !== '*') {
|
||||
log.warn(`ACLRule ${rule.id} ignored; user attributes must be on the default resource`);
|
||||
continue;
|
||||
}
|
||||
const parsed = JSON.parse(String(rule.userAttributes));
|
||||
// TODO: could perhaps use ts-interface-checker here.
|
||||
if (!(parsed && typeof parsed === 'object' &&
|
||||
[parsed.name, parsed.tableId, parsed.lookupColId, parsed.charId]
|
||||
.every(p => p && typeof p === 'string'))) {
|
||||
throw new Error(`Invalid user attribute rule: ${parsed}`);
|
||||
}
|
||||
userAttributes.push(parsed as UserAttributeRule);
|
||||
} else if (rule.aclFormula === '') {
|
||||
defaultPermissions = parsePermissions(String(rule.permissionsText));
|
||||
} else if (defaultPermissions) {
|
||||
log.warn(`ACLRule ${rule.id} ignored because listed after default rule`);
|
||||
} else if (!rule.aclFormulaParsed) {
|
||||
log.warn(`ACLRule ${rule.id} ignored because missing its parsed formula`);
|
||||
} else {
|
||||
body.push({
|
||||
aclFormula: String(rule.aclFormula),
|
||||
matchFunc: compile?.(JSON.parse(String(rule.aclFormulaParsed))),
|
||||
permissions: parsePermissions(String(rule.permissionsText)),
|
||||
permissionsText: String(rule.permissionsText),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!defaultPermissions) {
|
||||
// Empty permissions allow falling through to the doc-default resource.
|
||||
defaultPermissions = emptyPermissionSet();
|
||||
}
|
||||
const ruleSet: RuleSet = {tableId, colIds, body, defaultPermissions};
|
||||
ruleSets.push(ruleSet);
|
||||
}
|
||||
return {ruleSets, userAttributes};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user