mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Trim unapplicable permissions bits for column rules, both at parse time, and in UI
Summary: - UI now trims column rules before saving. - When rules are loaded, bits that aren't applicable to a resource get ignored. This should fix the incorrect behavior in existing docs without a migration. Test Plan: - Added test of UI, that it now sends trimmed rules - Added a unitteset of new trimPermissions() function - Add test of fixed interpretation of existing rules: now only permission bits applicable to a resource get respected. I.e. create/delete/schemaEdit are ignored in column rules, and schemaEdit is also ignored in table rules. - Note that DuplicateTest was affected: updated on the assumption that schemaEdit still can't actually apply at a table level. Reviewers: georgegevoian, paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D4205
This commit is contained in:
parent
0532ed6547
commit
ca8ac806db
@ -6,7 +6,7 @@ import {aclFormulaEditor} from 'app/client/aclui/ACLFormulaEditor';
|
|||||||
import {aclMemoEditor} from 'app/client/aclui/ACLMemoEditor';
|
import {aclMemoEditor} from 'app/client/aclui/ACLMemoEditor';
|
||||||
import {aclSelect} from 'app/client/aclui/ACLSelect';
|
import {aclSelect} from 'app/client/aclui/ACLSelect';
|
||||||
import {ACLUsersPopup} from 'app/client/aclui/ACLUsers';
|
import {ACLUsersPopup} from 'app/client/aclui/ACLUsers';
|
||||||
import {PermissionKey, permissionsWidget} from 'app/client/aclui/PermissionsWidget';
|
import {permissionsWidget} from 'app/client/aclui/PermissionsWidget';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {reportError, UserError} from 'app/client/models/errors';
|
import {reportError, UserError} from 'app/client/models/errors';
|
||||||
@ -20,13 +20,17 @@ import {textInput} from 'app/client/ui2018/editableLabel';
|
|||||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||||
import {menu, menuItemAsync} from 'app/client/ui2018/menus';
|
import {menu, menuItemAsync} from 'app/client/ui2018/menus';
|
||||||
import {
|
import {
|
||||||
|
AVAILABLE_BITS_COLUMNS,
|
||||||
|
AVAILABLE_BITS_TABLES,
|
||||||
emptyPermissionSet,
|
emptyPermissionSet,
|
||||||
MixedPermissionValue,
|
MixedPermissionValue,
|
||||||
parsePermissions,
|
parsePermissions,
|
||||||
PartialPermissionSet,
|
PartialPermissionSet,
|
||||||
|
PermissionKey,
|
||||||
permissionSetToText,
|
permissionSetToText,
|
||||||
summarizePermissions,
|
summarizePermissions,
|
||||||
summarizePermissionSet
|
summarizePermissionSet,
|
||||||
|
trimPermissions
|
||||||
} from 'app/common/ACLPermissions';
|
} from 'app/common/ACLPermissions';
|
||||||
import {ACLRuleCollection, isSchemaEditResource, SPECIAL_RULES_TABLE_ID} from 'app/common/ACLRuleCollection';
|
import {ACLRuleCollection, isSchemaEditResource, SPECIAL_RULES_TABLE_ID} from 'app/common/ACLRuleCollection';
|
||||||
import {AclRuleProblem, AclTableDescription, getTableTitle} from 'app/common/ActiveDocAPI';
|
import {AclRuleProblem, AclTableDescription, getTableTitle} from 'app/common/ActiveDocAPI';
|
||||||
@ -990,12 +994,19 @@ abstract class ObsRuleSet extends Disposable {
|
|||||||
// Should not happen.
|
// Should not happen.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include only the permissions for the bits that this RuleSet supports. E.g. this matters
|
||||||
|
// for seed rules, which may include create/delete bits which shouldn't apply to columns.
|
||||||
|
const origPermissions = parsePermissions(permissionsText);
|
||||||
|
const trimmedPermissions = trimPermissions(origPermissions, this.getAvailableBits());
|
||||||
|
const trimmedPermissionsText = permissionSetToText(trimmedPermissions);
|
||||||
|
|
||||||
this.addRulePart(
|
this.addRulePart(
|
||||||
this.getFirst() || null,
|
this.getFirst() || null,
|
||||||
{
|
{
|
||||||
aclFormula,
|
aclFormula,
|
||||||
permissionsText,
|
permissionsText: trimmedPermissionsText,
|
||||||
permissions: parsePermissions(permissionsText),
|
permissions: trimmedPermissions,
|
||||||
memo,
|
memo,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
@ -1048,7 +1059,7 @@ abstract class ObsRuleSet extends Disposable {
|
|||||||
* Which permission bits to allow the user to set.
|
* Which permission bits to allow the user to set.
|
||||||
*/
|
*/
|
||||||
public getAvailableBits(): PermissionKey[] {
|
public getAvailableBits(): PermissionKey[] {
|
||||||
return ['read', 'update', 'create', 'delete'];
|
return AVAILABLE_BITS_TABLES;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1117,8 +1128,7 @@ class ColumnObsRuleSet extends ObsRuleSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getAvailableBits(): PermissionKey[] {
|
public getAvailableBits(): PermissionKey[] {
|
||||||
// Create/Delete bits can't be set on a column-specific rule.
|
return AVAILABLE_BITS_COLUMNS;
|
||||||
return ['read', 'update'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasColumns() {
|
public hasColumns() {
|
||||||
|
@ -6,15 +6,12 @@ import {colors, testId, theme} from 'app/client/ui2018/cssVars';
|
|||||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||||
import {menu, menuIcon, menuItem} from 'app/client/ui2018/menus';
|
import {menu, menuIcon, menuItem} from 'app/client/ui2018/menus';
|
||||||
import {PartialPermissionSet, PartialPermissionValue} from 'app/common/ACLPermissions';
|
import {PartialPermissionSet, PartialPermissionValue} from 'app/common/ACLPermissions';
|
||||||
import {ALL_PERMISSION_PROPS, emptyPermissionSet} from 'app/common/ACLPermissions';
|
import {ALL_PERMISSION_PROPS, emptyPermissionSet, PermissionKey} from 'app/common/ACLPermissions';
|
||||||
import {capitalize} from 'app/common/gutil';
|
import {capitalize} from 'app/common/gutil';
|
||||||
import {dom, DomElementArg, Observable, styled} from 'grainjs';
|
import {dom, DomElementArg, Observable, styled} from 'grainjs';
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
|
||||||
// One of the strings 'read', 'update', etc.
|
|
||||||
export type PermissionKey = keyof PartialPermissionSet;
|
|
||||||
|
|
||||||
// Canonical order of permission bits when rendered in a permissionsWidget.
|
// Canonical order of permission bits when rendered in a permissionsWidget.
|
||||||
const PERMISSION_BIT_ORDER = 'RUCDS';
|
const PERMISSION_BIT_ORDER = 'RUCDS';
|
||||||
|
|
||||||
|
@ -41,7 +41,10 @@ export type PartialPermissionSet = PermissionSet<PartialPermissionValue>;
|
|||||||
export type MixedPermissionSet = PermissionSet<MixedPermissionValue>;
|
export type MixedPermissionSet = PermissionSet<MixedPermissionValue>;
|
||||||
export type TablePermissionSet = PermissionSet<TablePermissionValue>;
|
export type TablePermissionSet = PermissionSet<TablePermissionValue>;
|
||||||
|
|
||||||
const PERMISSION_BITS: {[letter: string]: keyof PermissionSet} = {
|
// One of the strings 'read', 'update', etc.
|
||||||
|
export type PermissionKey = keyof PermissionSet;
|
||||||
|
|
||||||
|
const PERMISSION_BITS: {[letter: string]: PermissionKey} = {
|
||||||
R: 'read',
|
R: 'read',
|
||||||
C: 'create',
|
C: 'create',
|
||||||
U: 'update',
|
U: 'update',
|
||||||
@ -60,6 +63,9 @@ const ALIASES: {[key: string]: string} = {
|
|||||||
};
|
};
|
||||||
const REVERSE_ALIASES = fromPairs(Object.entries(ALIASES).map(([alias, value]) => [value, alias]));
|
const REVERSE_ALIASES = fromPairs(Object.entries(ALIASES).map(([alias, value]) => [value, alias]));
|
||||||
|
|
||||||
|
export const AVAILABLE_BITS_TABLES: PermissionKey[] = ['read', 'update', 'create', 'delete'];
|
||||||
|
export const AVAILABLE_BITS_COLUMNS: PermissionKey[] = ['read', 'update'];
|
||||||
|
|
||||||
// Comes in useful for initializing unset PermissionSets.
|
// Comes in useful for initializing unset PermissionSets.
|
||||||
export function emptyPermissionSet(): PartialPermissionSet {
|
export function emptyPermissionSet(): PartialPermissionSet {
|
||||||
return {read: "", create: "", update: "", delete: "", schemaEdit: ""};
|
return {read: "", create: "", update: "", delete: "", schemaEdit: ""};
|
||||||
@ -141,6 +147,20 @@ export function mergePartialPermissions(a: PartialPermissionSet, b: PartialPermi
|
|||||||
return mergePermissions([a, b], ([_a, _b]) => combinePartialPermission(_a, _b));
|
return mergePermissions([a, b], ([_a, _b]) => combinePartialPermission(_a, _b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns permissions trimmed to include only the available bits, and empty for any other bits.
|
||||||
|
*/
|
||||||
|
export function trimPermissions(
|
||||||
|
permissions: PartialPermissionSet, availableBits: PermissionKey[]
|
||||||
|
): PartialPermissionSet {
|
||||||
|
const trimmed = emptyPermissionSet();
|
||||||
|
for (const bit of availableBits) {
|
||||||
|
trimmed[bit] = permissions[bit];
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge a list of PermissionSets by combining individual bits.
|
* Merge a list of PermissionSets by combining individual bits.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {parsePermissions, permissionSetToText, splitSchemaEditPermissionSet} from 'app/common/ACLPermissions';
|
import {parsePermissions, permissionSetToText, splitSchemaEditPermissionSet} from 'app/common/ACLPermissions';
|
||||||
|
import {AVAILABLE_BITS_COLUMNS, AVAILABLE_BITS_TABLES, trimPermissions} from 'app/common/ACLPermissions';
|
||||||
import {ACLShareRules, TableWithOverlay} from 'app/common/ACLShareRules';
|
import {ACLShareRules, TableWithOverlay} from 'app/common/ACLShareRules';
|
||||||
import {AclRuleProblem} from 'app/common/ActiveDocAPI';
|
import {AclRuleProblem} from 'app/common/ActiveDocAPI';
|
||||||
import {DocData} from 'app/common/DocData';
|
import {DocData} from 'app/common/DocData';
|
||||||
@ -537,13 +538,18 @@ function readAclRules(docData: DocData, {log, compile, enrichRulesForImplementat
|
|||||||
if (hasShares && rule.id >= 0) {
|
if (hasShares && rule.id >= 0) {
|
||||||
aclFormulaParsed = shareRules.transformNonShareRules({rule, aclFormulaParsed});
|
aclFormulaParsed = shareRules.transformNonShareRules({rule, aclFormulaParsed});
|
||||||
}
|
}
|
||||||
|
let permissions = parsePermissions(String(rule.permissionsText));
|
||||||
|
if (tableId !== '*' && tableId !== SPECIAL_RULES_TABLE_ID) {
|
||||||
|
const availableBits = (colIds === '*') ? AVAILABLE_BITS_TABLES : AVAILABLE_BITS_COLUMNS;
|
||||||
|
permissions = trimPermissions(permissions, availableBits);
|
||||||
|
}
|
||||||
body.push({
|
body.push({
|
||||||
origRecord: rule,
|
origRecord: rule,
|
||||||
aclFormula: String(rule.aclFormula),
|
aclFormula: String(rule.aclFormula),
|
||||||
matchFunc: rule.aclFormula ? compile?.(aclFormulaParsed) : defaultMatchFunc,
|
matchFunc: rule.aclFormula ? compile?.(aclFormulaParsed) : defaultMatchFunc,
|
||||||
memo: rule.memo,
|
memo: rule.memo,
|
||||||
permissions: parsePermissions(String(rule.permissionsText)),
|
permissions,
|
||||||
permissionsText: String(rule.permissionsText),
|
permissionsText: permissionSetToText(permissions)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,3 +2,6 @@ const chai = require('chai');
|
|||||||
const chaiAsPromised = require('chai-as-promised');
|
const chaiAsPromised = require('chai-as-promised');
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
// By default this is false, which affects asserts like isRejected and isFulfilled.
|
||||||
|
chai.config.includeStack = true;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {emptyPermissionSet, PartialPermissionSet,
|
import {emptyPermissionSet, PartialPermissionSet, PermissionKey,
|
||||||
summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
||||||
import {makePartialPermissions, parsePermissions, permissionSetToText} from 'app/common/ACLPermissions';
|
import {makePartialPermissions, parsePermissions, permissionSetToText} from 'app/common/ACLPermissions';
|
||||||
import {mergePartialPermissions, mergePermissions} from 'app/common/ACLPermissions';
|
import {mergePartialPermissions, mergePermissions, trimPermissions} from 'app/common/ACLPermissions';
|
||||||
import {assert} from 'chai';
|
import {assert} from 'chai';
|
||||||
|
|
||||||
describe("ACLPermissions", function() {
|
describe("ACLPermissions", function() {
|
||||||
@ -112,6 +112,17 @@ describe("ACLPermissions", function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support trimPermissions', function() {
|
||||||
|
const trim = (permissionsText: string, availableBits: PermissionKey[]) =>
|
||||||
|
permissionSetToText(trimPermissions(parsePermissions(permissionsText), availableBits));
|
||||||
|
assert.deepEqual(trim("+CRUD", ["read", "update"]), "+RU");
|
||||||
|
assert.deepEqual(trim("all", ["read", "update"]), "+RU");
|
||||||
|
assert.deepEqual(trim("-C+R-U+D-S", ["update", "read"]), "+R-U");
|
||||||
|
assert.deepEqual(trim("none", ["read", "update", "create", "delete", "schemaEdit"]), "none");
|
||||||
|
assert.deepEqual(trim("none", ["read", "update", "create", "delete"]), "-CRUD");
|
||||||
|
assert.deepEqual(trim("none", ["read"]), "-R");
|
||||||
|
});
|
||||||
|
|
||||||
it ('should allow summarization of permission sets', function() {
|
it ('should allow summarization of permission sets', function() {
|
||||||
assert.deepEqual(summarizePermissionSet(parsePermissions("+U-D")), 'mixed');
|
assert.deepEqual(summarizePermissionSet(parsePermissions("+U-D")), 'mixed');
|
||||||
assert.deepEqual(summarizePermissionSet(parsePermissions("+U+D")), 'allow');
|
assert.deepEqual(summarizePermissionSet(parsePermissions("+U+D")), 'allow');
|
||||||
|
Loading…
Reference in New Issue
Block a user