mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
0130409447
Summary: Shares and documents would both produce a rule set for the same column if the document rule set was for multiple columns. In this case, it was causing one of the rules to be overwritten by the other (specifically, the rule granting access to form references was not being applied in shares). The symptom was `null` values in place of the referenced table's values. We address this by splitting any rule sets for multiple columns that are also affected by shares, so that they can be overridden by shares without causing a conflicting rule set to be created (i.e. 2 column rule sets containing the same column). Test Plan: Server tests. Reviewers: dsagal, paulfitz Reviewed By: dsagal, paulfitz Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D4208
444 lines
15 KiB
TypeScript
444 lines
15 KiB
TypeScript
import {ACLRulesReader} from 'app/common/ACLRulesReader';
|
|
import {DocData} from 'app/common/DocData';
|
|
import {MetaRowRecord} from 'app/common/TableData';
|
|
import {CellValue} from 'app/plugin/GristData';
|
|
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
|
import {makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
|
import {assert} from 'chai';
|
|
import * as sinon from 'sinon';
|
|
import {createDocTools} from 'test/server/docTools';
|
|
|
|
describe('ACLRulesReader', function() {
|
|
this.timeout(10000);
|
|
|
|
const docTools = createDocTools({persistAcrossCases: true});
|
|
const fakeSession = makeExceptionalDocSession('system');
|
|
|
|
let activeDoc: ActiveDoc;
|
|
let docData: DocData;
|
|
|
|
before(async function () {
|
|
activeDoc = await docTools.createDoc('ACLRulesReader');
|
|
docData = activeDoc.docData!;
|
|
});
|
|
|
|
describe('without shares', function() {
|
|
it('entries', async function() {
|
|
// Check output of reading the resources and rules of an empty document.
|
|
for (const options of [undefined, {addShareRules: true}]) {
|
|
assertResourcesAndRules(new ACLRulesReader(docData, options), [
|
|
DEFAULT_UNUSED_RESOURCE_AND_RULE,
|
|
]);
|
|
}
|
|
|
|
// Add some table and default rules and re-check output.
|
|
await activeDoc.applyUserActions(fakeSession, [
|
|
['AddTable', 'Private', [{id: 'A'}]],
|
|
['AddTable', 'PartialPrivate', [{id: 'A'}]],
|
|
['AddRecord', 'PartialPrivate', null, { A: 0 }],
|
|
['AddRecord', 'PartialPrivate', null, { A: 1 }],
|
|
['AddRecord', '_grist_ACLResources', -1, {tableId: 'Private', colIds: '*'}],
|
|
['AddRecord', '_grist_ACLResources', -2, {tableId: '*', colIds: '*'}],
|
|
['AddRecord', '_grist_ACLResources', -3, {tableId: 'PartialPrivate', colIds: '*'}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -1,
|
|
aclFormula: 'user.Access == "owners"',
|
|
permissionsText: 'all',
|
|
memo: 'owner check',
|
|
}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -1, aclFormula: '', permissionsText: 'none',
|
|
}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -2, aclFormula: 'user.Access != "owners"', permissionsText: '-S',
|
|
}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -3, aclFormula: 'user.Access != "owners" and rec.A > 0', permissionsText: 'none',
|
|
}],
|
|
['AddTable', 'Public', [{id: 'A'}]],
|
|
]);
|
|
for (const options of [undefined, {addShareRules: true}]) {
|
|
assertResourcesAndRules(new ACLRulesReader(docData, options), [
|
|
{
|
|
resource: {id: 2, tableId: 'Private', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.Access == "owners"',
|
|
permissionsText: 'all',
|
|
},
|
|
{
|
|
aclFormula: '',
|
|
permissionsText: 'none',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 3, tableId: '*', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.Access != "owners"',
|
|
permissionsText: '-S',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 4, tableId: 'PartialPrivate', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.Access != "owners" and rec.A > 0',
|
|
permissionsText: 'none',
|
|
},
|
|
],
|
|
},
|
|
DEFAULT_UNUSED_RESOURCE_AND_RULE,
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('getResourceById', async function() {
|
|
for (const options of [undefined, {addShareRules: true}]) {
|
|
// Check output of valid resource ids.
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, options).getResourceById(1),
|
|
{id: 1, tableId: '', colIds: ''}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, options).getResourceById(2),
|
|
{id: 2, tableId: 'Private', colIds: '*'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, options).getResourceById(3),
|
|
{id: 3, tableId: '*', colIds: '*'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, options).getResourceById(4),
|
|
{id: 4, tableId: 'PartialPrivate', colIds: '*'}
|
|
);
|
|
|
|
// Check output of non-existent resource ids.
|
|
assert.isUndefined(new ACLRulesReader(docData, options).getResourceById(5));
|
|
assert.isUndefined(new ACLRulesReader(docData, options).getResourceById(0));
|
|
assert.isUndefined(new ACLRulesReader(docData, options).getResourceById(-1));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('with shares', function() {
|
|
before(async function() {
|
|
sinon.stub(ActiveDoc.prototype as any, '_getHomeDbManagerOrFail').returns({
|
|
syncShares: () => Promise.resolve(),
|
|
});
|
|
activeDoc = await docTools.loadFixtureDoc('FilmsWithImages.grist');
|
|
docData = activeDoc.docData!;
|
|
await activeDoc.applyUserActions(fakeSession, [
|
|
['AddRecord', '_grist_Shares', null, {
|
|
linkId: 'x',
|
|
options: '{"publish": true}'
|
|
}],
|
|
]);
|
|
});
|
|
|
|
after(function() {
|
|
sinon.restore();
|
|
});
|
|
|
|
it('entries', async function() {
|
|
// Check output of reading the resources and rules of an empty document.
|
|
assertResourcesAndRules(new ACLRulesReader(docData), [
|
|
DEFAULT_UNUSED_RESOURCE_AND_RULE,
|
|
]);
|
|
|
|
// Check output of reading the resources and rules of an empty document, with share rules.
|
|
assertResourcesAndRules(new ACLRulesReader(docData, {addShareRules: true}), [
|
|
{
|
|
resource: {id: -1, tableId: 'Films', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-CRUDS',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: -2, tableId: 'Friends', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-CRUDS',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: -3, tableId: 'Performances', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-CRUDS',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: -4, tableId: '*', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-S',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 1, tableId: '', colIds: ''},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (True)',
|
|
permissionsText: '',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
// Add some default, table, and column rules.
|
|
await activeDoc.applyUserActions(fakeSession, [
|
|
['UpdateRecord', '_grist_Views_section', 7,
|
|
{shareOptions: '{"publish": true, "form": true}'}],
|
|
['UpdateRecord', '_grist_Pages', 2, {shareRef: 1}],
|
|
['AddRecord', '_grist_ACLResources', -1, {tableId: 'Films', colIds: 'Title,Poster,PosterDup'}],
|
|
['AddRecord', '_grist_ACLResources', -2, {tableId: 'Films', colIds: '*'}],
|
|
['AddRecord', '_grist_ACLResources', -3, {tableId: '*', colIds: '*'}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -1, aclFormula: 'user.access != OWNER', permissionsText: '-R',
|
|
}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -2, aclFormula: 'True', permissionsText: 'all',
|
|
}],
|
|
['AddRecord', '_grist_ACLRules', null, {
|
|
resource: -3, aclFormula: 'True', permissionsText: 'all',
|
|
}],
|
|
]);
|
|
|
|
// Re-check output without share rules.
|
|
assertResourcesAndRules(new ACLRulesReader(docData), [
|
|
{
|
|
resource: {id: 2, tableId: 'Films', colIds: 'Title,Poster,PosterDup'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.access != OWNER',
|
|
permissionsText: '-R',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 3, tableId: 'Films', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'True',
|
|
permissionsText: 'all',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 4, tableId: '*', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'True',
|
|
permissionsText: 'all',
|
|
},
|
|
],
|
|
},
|
|
DEFAULT_UNUSED_RESOURCE_AND_RULE,
|
|
]);
|
|
|
|
// Re-check output with share rules.
|
|
assertResourcesAndRules(new ACLRulesReader(docData, {addShareRules: true}), [
|
|
{
|
|
resource: {id: -1, tableId: 'Friends', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef == 1',
|
|
permissionsText: '+C',
|
|
},
|
|
{
|
|
aclFormula: 'user.ShareRef == 1 and rec.id == 0',
|
|
permissionsText: '+R',
|
|
},
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-CRUDS',
|
|
},
|
|
],
|
|
},
|
|
// Resource -2, -3, and -4, were split from resource 2.
|
|
{
|
|
resource: {id: -2, tableId: 'Films', colIds: 'Title'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef == 1',
|
|
permissionsText: '+R',
|
|
},
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (user.access != OWNER)',
|
|
permissionsText: '-R',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 3, tableId: 'Films', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-CRUDS',
|
|
},
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (True)',
|
|
permissionsText: 'all',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: -5, tableId: 'Performances', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-CRUDS',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 4, tableId: '*', colIds: '*'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is not None',
|
|
permissionsText: '-S',
|
|
},
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (True)',
|
|
permissionsText: 'all',
|
|
},
|
|
],
|
|
},
|
|
// Resource -3 and -4 were split from resource 2.
|
|
{
|
|
resource: {id: -3, tableId: 'Films', colIds: 'Poster'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (user.access != OWNER)',
|
|
permissionsText: '-R',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: -4, tableId: 'Films', colIds: 'PosterDup'},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (user.access != OWNER)',
|
|
permissionsText: '-R',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
resource: {id: 1, tableId: '', colIds: ''},
|
|
rules: [
|
|
{
|
|
aclFormula: 'user.ShareRef is None and (True)',
|
|
permissionsText: '',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('getResourceById', async function() {
|
|
// Check output of valid resource ids.
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData).getResourceById(1),
|
|
{id: 1, tableId: '', colIds: ''}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData).getResourceById(2),
|
|
{id: 2, tableId: 'Films', colIds: 'Title,Poster,PosterDup'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData).getResourceById(3),
|
|
{id: 3, tableId: 'Films', colIds: '*'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData).getResourceById(4),
|
|
{id: 4, tableId: '*', colIds: '*'}
|
|
);
|
|
|
|
// Check output of non-existent resource ids.
|
|
assert.isUndefined(new ACLRulesReader(docData).getResourceById(5));
|
|
assert.isUndefined(new ACLRulesReader(docData).getResourceById(0));
|
|
assert.isUndefined(new ACLRulesReader(docData).getResourceById(-1));
|
|
|
|
// Check output of valid resource ids (with share rules).
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(1),
|
|
{id: 1, tableId: '', colIds: ''}
|
|
);
|
|
assert.isUndefined(new ACLRulesReader(docData, {addShareRules: true}).getResourceById(2));
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(3),
|
|
{id: 3, tableId: 'Films', colIds: '*'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(4),
|
|
{id: 4, tableId: '*', colIds: '*'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(-1),
|
|
{id: -1, tableId: 'Friends', colIds: '*'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(-2),
|
|
{id: -2, tableId: 'Films', colIds: 'Title'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(-3),
|
|
{id: -3, tableId: 'Films', colIds: 'Poster'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(-4),
|
|
{id: -4, tableId: 'Films', colIds: 'PosterDup'}
|
|
);
|
|
assert.deepEqual(
|
|
new ACLRulesReader(docData, {addShareRules: true}).getResourceById(-5),
|
|
{id: -5, tableId: 'Performances', colIds: '*'}
|
|
);
|
|
|
|
// Check output of non-existent resource ids (with share rules).
|
|
assert.isUndefined(new ACLRulesReader(docData, {addShareRules: true}).getResourceById(5));
|
|
assert.isUndefined(new ACLRulesReader(docData, {addShareRules: true}).getResourceById(0));
|
|
assert.isUndefined(new ACLRulesReader(docData, {addShareRules: true}).getResourceById(-6));
|
|
});
|
|
});
|
|
});
|
|
|
|
interface ACLResourceAndRules {
|
|
resource: MetaRowRecord<'_grist_ACLResources'>|undefined;
|
|
rules: {aclFormula: CellValue, permissionsText: CellValue}[];
|
|
}
|
|
|
|
function assertResourcesAndRules(
|
|
aclRulesReader: ACLRulesReader,
|
|
expected: ACLResourceAndRules[]
|
|
) {
|
|
const actual: ACLResourceAndRules[] = [...aclRulesReader.entries()].map(([resourceId, rules]) => {
|
|
return {
|
|
resource: aclRulesReader.getResourceById(resourceId),
|
|
rules: rules.map(({aclFormula, permissionsText}) => ({aclFormula, permissionsText})),
|
|
};
|
|
});
|
|
assert.deepEqual(actual, expected);
|
|
}
|
|
|
|
/**
|
|
* An unused resource and rule that's automatically included in every Grist document.
|
|
*
|
|
* See comment in `UserActions.InitNewDoc` (from `useractions.py`) for context.
|
|
*/
|
|
const DEFAULT_UNUSED_RESOURCE_AND_RULE: ACLResourceAndRules = {
|
|
resource: {id: 1, tableId: '', colIds: ''},
|
|
rules: [{aclFormula: '', permissionsText: ''}],
|
|
};
|