mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Implement checkbox for SchemaEdit permission in Access Rules UI.
Summary: - Introduces a fictitious "*SPECIAL:SchemaEdit" resource in UI only. - Hides "S" bit for the default rule section. - Shows a checkbox UI similar to other checkboxes, with an additional dismissable warning. Test Plan: Added a browser test Reviewers: paulfitz, georgegevoian Reviewed By: paulfitz, georgegevoian Differential Revision: https://phab.getgrist.com/D3765
This commit is contained in:
@@ -190,3 +190,25 @@ export function summarizePermissions(perms: MixedPermissionValue[]): MixedPermis
|
||||
const perm = perms[0];
|
||||
return perms.some(p => p !== perm) ? 'mixed' : perm;
|
||||
}
|
||||
|
||||
|
||||
function isEmpty(permissions: PartialPermissionSet): boolean {
|
||||
return Object.values(permissions).every(v => v === "");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Divide up a PartialPermissionSet into two: one containing only the 'schemaEdit' permission bit,
|
||||
* and the other containing everything else. Empty parts will be returned as undefined, except
|
||||
* when both are empty, in which case nonSchemaEdit will be returned as an empty permission set.
|
||||
*/
|
||||
export function splitSchemaEditPermissionSet(permissions: PartialPermissionSet):
|
||||
{schemaEdit?: PartialPermissionSet, nonSchemaEdit?: PartialPermissionSet} {
|
||||
|
||||
const schemaEdit = {...emptyPermissionSet(), schemaEdit: permissions.schemaEdit};
|
||||
const nonSchemaEdit: PartialPermissionSet = {...permissions, schemaEdit: ""};
|
||||
return {
|
||||
schemaEdit: !isEmpty(schemaEdit) ? schemaEdit : undefined,
|
||||
nonSchemaEdit: !isEmpty(nonSchemaEdit) || isEmpty(schemaEdit) ? nonSchemaEdit : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {parsePermissions} from 'app/common/ACLPermissions';
|
||||
import {parsePermissions, permissionSetToText, splitSchemaEditPermissionSet} from 'app/common/ACLPermissions';
|
||||
import {AclRuleProblem} from 'app/common/ActiveDocAPI';
|
||||
import {ILogger} from 'app/common/BaseAPI';
|
||||
import {DocData} from 'app/common/DocData';
|
||||
import {AclMatchFunc, ParsedAclFormula, RulePart, RuleSet, UserAttributeRule} from 'app/common/GranularAccessClause';
|
||||
import {getSetMapValue} from 'app/common/gutil';
|
||||
import {getSetMapValue, isNonNullish} from 'app/common/gutil';
|
||||
import {MetaRowRecord} from 'app/common/TableData';
|
||||
import {decodeObject} from 'app/plugin/objtypes';
|
||||
import sortBy = require('lodash/sortBy');
|
||||
@@ -34,7 +34,28 @@ const DEFAULT_RULE_SET: RuleSet = {
|
||||
}],
|
||||
};
|
||||
|
||||
// Check if the given resource is the special "SchemaEdit" resource, which only exists as a
|
||||
// frontend representation.
|
||||
export function isSchemaEditResource(resource: {tableId: string, colIds: string}): boolean {
|
||||
return resource.tableId === SPECIAL_RULES_TABLE_ID && resource.colIds === 'SchemaEdit';
|
||||
}
|
||||
|
||||
const SPECIAL_RULE_SETS: Record<string, RuleSet> = {
|
||||
SchemaEdit: {
|
||||
tableId: SPECIAL_RULES_TABLE_ID,
|
||||
colIds: ['SchemaEdit'],
|
||||
body: [{
|
||||
aclFormula: "user.Access in [EDITOR, OWNER]",
|
||||
matchFunc: (input) => ['editors', 'owners'].includes(String(input.user.Access)),
|
||||
permissions: parsePermissions('+S'),
|
||||
permissionsText: '+S',
|
||||
}, {
|
||||
aclFormula: "",
|
||||
matchFunc: defaultMatchFunc,
|
||||
permissions: parsePermissions('-S'),
|
||||
permissionsText: '-S',
|
||||
}],
|
||||
},
|
||||
AccessRules: {
|
||||
tableId: SPECIAL_RULES_TABLE_ID,
|
||||
colIds: ['AccessRules'],
|
||||
@@ -191,6 +212,21 @@ export class ACLRuleCollection {
|
||||
} else {
|
||||
specialRuleSets.set(specialType, {...ruleSet, body: [...ruleSet.body, ...specialDefault.body]});
|
||||
}
|
||||
} else if (options.pullOutSchemaEdit && ruleSet.tableId === '*' && ruleSet.colIds === '*') {
|
||||
// If pullOutSchemaEdit is requested, we move out rules with SchemaEdit permissions from
|
||||
// the default resource into the ficticious "*SPECIAL:SchemaEdit" resource. This is used
|
||||
// in the frontend only, to present those rules in a separate section.
|
||||
const schemaParts = ruleSet.body.map(part => splitSchemaEditRulePart(part).schemaEdit).filter(isNonNullish);
|
||||
|
||||
if (schemaParts.length > 0) {
|
||||
const specialType = 'SchemaEdit';
|
||||
const specialDefault = specialRuleSets.get(specialType)!;
|
||||
specialRuleSets.set(specialType, {
|
||||
tableId: SPECIAL_RULES_TABLE_ID,
|
||||
colIds: ['SchemaEdit'],
|
||||
body: [...schemaParts, ...specialDefault.body]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,9 +239,15 @@ export class ACLRuleCollection {
|
||||
for (const ruleSet of ruleSets) {
|
||||
if (ruleSet.tableId === '*') {
|
||||
if (ruleSet.colIds === '*') {
|
||||
// If pullOutSchemaEdit is requested, skip the SchemaEdit rules for the default resource;
|
||||
// those got pulled out earlier into the fictitious "*SPECIAL:SchemaEdit" resource.
|
||||
const body = options.pullOutSchemaEdit ?
|
||||
ruleSet.body.map(part => splitSchemaEditRulePart(part).nonSchemaEdit).filter(isNonNullish) :
|
||||
ruleSet.body;
|
||||
|
||||
defaultRuleSet = {
|
||||
...ruleSet,
|
||||
body: [...ruleSet.body, ...DEFAULT_RULE_SET.body],
|
||||
body: [...body, ...DEFAULT_RULE_SET.body],
|
||||
};
|
||||
} else {
|
||||
// tableId of '*' cannot list particular columns.
|
||||
@@ -341,6 +383,11 @@ export interface ReadAclOptions {
|
||||
// 1. They would show in the UI
|
||||
// 2. They would be saved back after editing, causing them to accumulate
|
||||
includeHelperCols?: boolean;
|
||||
|
||||
// If true, rules with 'schemaEdit' permission are moved out of the '*:*' resource into a
|
||||
// fictitious '*SPECIAL:SchemaEdit' resource. This is used only on the client, to present
|
||||
// schemaEdit as a separate checkbox. Such rules are saved back to the '*:*' resource.
|
||||
pullOutSchemaEdit?: boolean;
|
||||
}
|
||||
|
||||
export interface ReadAclResults {
|
||||
@@ -474,3 +521,37 @@ function readAclRules(docData: DocData, {log, compile, includeHelperCols}: ReadA
|
||||
}
|
||||
return {ruleSets, userAttributes};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* In the UI, we present SchemaEdit rules in a separate section, even though in reality they live
|
||||
* as schemaEdit permission bits among the rules for the default resource. This function splits a
|
||||
* RulePart into two: one containing the schemaEdit permission bit, and the other containing the
|
||||
* other bits. If either part is empty, it will be returned as undefined, but if both are empty,
|
||||
* nonSchemaEdit will be included as a rule with empty permission bits.
|
||||
*
|
||||
* It's possible for both parts to be non-empty (for rules created before the updated UI), in
|
||||
* which case the schemaEdit one will have a fake origRecord, to cause it to be saved as a new
|
||||
* record when saving.
|
||||
*/
|
||||
function splitSchemaEditRulePart(rulePart: RulePart): {schemaEdit?: RulePart, nonSchemaEdit?: RulePart} {
|
||||
const p = splitSchemaEditPermissionSet(rulePart.permissions);
|
||||
let schemaEdit: RulePart|undefined;
|
||||
let nonSchemaEdit: RulePart|undefined;
|
||||
if (p.schemaEdit) {
|
||||
schemaEdit = {...rulePart,
|
||||
permissions: p.schemaEdit,
|
||||
permissionsText: permissionSetToText(p.schemaEdit),
|
||||
};
|
||||
}
|
||||
if (p.nonSchemaEdit) {
|
||||
nonSchemaEdit = {...rulePart,
|
||||
permissions: p.nonSchemaEdit,
|
||||
permissionsText: permissionSetToText(p.nonSchemaEdit),
|
||||
};
|
||||
}
|
||||
if (schemaEdit && nonSchemaEdit) {
|
||||
schemaEdit.origRecord = {id: -1} as MetaRowRecord<'_grist_ACLRules'>;
|
||||
}
|
||||
return {schemaEdit, nonSchemaEdit};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user