2020-11-17 21:49:32 +00:00
|
|
|
import { emptyPermissionSet, parsePermissions, PartialPermissionSet } from 'app/common/ACLPermissions';
|
|
|
|
import { ILogger } from 'app/common/BaseAPI';
|
|
|
|
import { CellValue, RowRecord } from 'app/common/DocActions';
|
|
|
|
import { DocData } from 'app/common/DocData';
|
|
|
|
import { getSetMapValue } from 'app/common/gutil';
|
|
|
|
import sortBy = require('lodash/sortBy');
|
2020-10-19 14:25:21 +00:00
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
export interface RuleSet {
|
|
|
|
tableId: '*' | string;
|
|
|
|
colIds: '*' | string[];
|
|
|
|
body: RulePart[];
|
|
|
|
defaultPermissions: PartialPermissionSet;
|
2020-10-12 13:50:07 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
export interface RulePart {
|
|
|
|
aclFormula: string;
|
|
|
|
permissions: PartialPermissionSet;
|
|
|
|
permissionsText: string; // The text version of PermissionSet, as stored.
|
|
|
|
|
|
|
|
// Compiled version of aclFormula.
|
|
|
|
matchFunc?: AclMatchFunc;
|
2020-10-12 13:50:07 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
// Light wrapper around characteristics or records.
|
|
|
|
export interface InfoView {
|
|
|
|
get(key: string): CellValue;
|
|
|
|
toJSON(): {[key: string]: any};
|
2020-10-19 14:25:21 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
// Represents user info, which may include properties which are themselves RowRecords.
|
|
|
|
export type UserInfo = Record<string, CellValue|InfoView>;
|
|
|
|
|
2020-11-03 23:44:09 +00:00
|
|
|
/**
|
2020-11-17 21:49:32 +00:00
|
|
|
* Input into the AclMatchFunc. Compiled formulas evaluate AclMatchInput to produce a boolean.
|
2020-11-03 23:44:09 +00:00
|
|
|
*/
|
2020-11-17 21:49:32 +00:00
|
|
|
export interface AclMatchInput {
|
|
|
|
user: UserInfo;
|
|
|
|
rec?: InfoView;
|
|
|
|
newRec?: InfoView;
|
2020-11-03 23:44:09 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 14:25:21 +00:00
|
|
|
/**
|
2020-11-17 21:49:32 +00:00
|
|
|
* The actual boolean function that can evaluate a request. The result of compiling ParsedAclFormula.
|
2020-10-19 14:25:21 +00:00
|
|
|
*/
|
2020-11-17 21:49:32 +00:00
|
|
|
export type AclMatchFunc = (input: AclMatchInput) => boolean;
|
2020-10-19 14:25:21 +00:00
|
|
|
|
2020-11-03 23:44:09 +00:00
|
|
|
/**
|
2020-11-17 21:49:32 +00:00
|
|
|
* Representation of a parsed ACL formula.
|
2020-11-03 23:44:09 +00:00
|
|
|
*/
|
2020-11-17 21:49:32 +00:00
|
|
|
export type ParsedAclFormula = [string, ...Array<ParsedAclFormula|CellValue>];
|
2020-10-19 14:25:21 +00:00
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
export interface 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'.
|
2020-10-19 14:25:21 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
export interface ReadAclOptions {
|
|
|
|
log: ILogger; // For logging warnings during rule processing.
|
|
|
|
compile?: (parsed: ParsedAclFormula) => AclMatchFunc;
|
2020-10-19 14:25:21 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
export interface ReadAclResults {
|
|
|
|
ruleSets: RuleSet[];
|
|
|
|
userAttributes: UserAttributeRule[];
|
2020-10-19 14:25:21 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
/**
|
|
|
|
* 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')!;
|
2020-10-19 14:25:21 +00:00
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
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(',');
|
2020-10-19 14:25:21 +00:00
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
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);
|
2020-10-19 14:25:21 +00:00
|
|
|
}
|
2020-11-17 21:49:32 +00:00
|
|
|
return {ruleSets, userAttributes};
|
2020-10-12 13:50:07 +00:00
|
|
|
}
|