2020-07-27 18:57:36 +00:00
|
|
|
"""
|
|
|
|
Test of ACL rules.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import acl
|
|
|
|
import actions
|
|
|
|
import logger
|
|
|
|
import schema
|
|
|
|
import testutil
|
|
|
|
import test_engine
|
|
|
|
import useractions
|
|
|
|
|
|
|
|
log = logger.Logger(__name__, logger.INFO)
|
|
|
|
|
|
|
|
|
|
|
|
class TestACL(test_engine.EngineTestCase):
|
|
|
|
maxDiff = None # Allow self.assertEqual to display big diffs
|
|
|
|
|
|
|
|
starting_table_data = [
|
|
|
|
["id", "city", "state", "amount" ],
|
|
|
|
[ 21, "New York", "NY" , 1. ],
|
|
|
|
[ 22, "Albany", "NY" , 2. ],
|
|
|
|
[ 23, "Seattle", "WA" , 3. ],
|
|
|
|
[ 24, "Chicago", "IL" , 4. ],
|
|
|
|
[ 25, "Bedford", "MA" , 5. ],
|
|
|
|
[ 26, "New York", "NY" , 6. ],
|
|
|
|
[ 27, "Buffalo", "NY" , 7. ],
|
|
|
|
[ 28, "Bedford", "NY" , 8. ],
|
|
|
|
[ 29, "Boston", "MA" , 9. ],
|
|
|
|
[ 30, "Yonkers", "NY" , 10. ],
|
|
|
|
[ 31, "New York", "NY" , 11. ],
|
|
|
|
]
|
|
|
|
|
|
|
|
sample = testutil.parse_test_sample({
|
|
|
|
"SCHEMA": [
|
|
|
|
[1, "Address", [
|
|
|
|
[11, "city", "Text", False, "", "City", ""],
|
|
|
|
[12, "state", "Text", False, "", "State", "WidgetOptions1"],
|
|
|
|
[13, "amount", "Numeric", False, "", "Amount", "WidgetOptions2"],
|
|
|
|
]]
|
|
|
|
],
|
|
|
|
"DATA": {
|
|
|
|
"Address": starting_table_data,
|
|
|
|
"_grist_ACLRules": [
|
|
|
|
["id", "resource", "permissions", "principals", "aclFormula", "aclColumn"],
|
|
|
|
],
|
|
|
|
"_grist_ACLResources": [
|
|
|
|
["id", "tableId", "colIds"],
|
|
|
|
],
|
|
|
|
"_grist_ACLPrincipals": [
|
|
|
|
["id", "type", "userEmail", "userName", "groupName", "instanceId"],
|
|
|
|
],
|
|
|
|
"_grist_ACLMemberships": [
|
|
|
|
["id", "parent", "child"],
|
|
|
|
]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
def _apply_ua(self, *useraction_reprs):
|
|
|
|
"""Returns an ActionBundle."""
|
|
|
|
user_actions = [useractions.from_repr(ua) for ua in useraction_reprs]
|
|
|
|
return self.engine.acl_split(self.engine.apply_user_actions(user_actions))
|
|
|
|
|
|
|
|
def test_trivial_action_bundle(self):
|
|
|
|
# In this test case, we just check that an ActionGroup is packaged unchanged into an
|
|
|
|
# ActionBundle when there are no ACL rules at all.
|
|
|
|
self.load_sample(self.sample)
|
|
|
|
|
|
|
|
# Verify the starting table; there should be no views yet.
|
|
|
|
self.assertTableData("Address", self.starting_table_data)
|
|
|
|
|
|
|
|
# Check that the raw action group created by an action is as expected.
|
|
|
|
out_action = self.update_record("Address", 22, amount=20.)
|
|
|
|
self.assertPartialOutActions(out_action, {
|
|
|
|
'stored': [['UpdateRecord', 'Address', 22, {'amount': 20.}]],
|
|
|
|
'undo': [['UpdateRecord', 'Address', 22, {'amount': 2.}]],
|
|
|
|
'calc': [],
|
|
|
|
'retValues': [None],
|
|
|
|
})
|
|
|
|
|
|
|
|
# In this case, we have no rules, and the action is packaged unchanged into an ActionBundle.
|
|
|
|
out_bundle = self.engine.acl_split(out_action)
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
'envelopes': [{"recipients": []}],
|
|
|
|
'stored': [(0, ['UpdateRecord', 'Address', 22, {'amount': 20.}])],
|
|
|
|
'undo': [(0, ['UpdateRecord', 'Address', 22, {'amount': 2.}])],
|
|
|
|
'calc': [],
|
|
|
|
'retValues': [None],
|
|
|
|
'rules': [],
|
|
|
|
})
|
|
|
|
|
|
|
|
# Another similar action.
|
|
|
|
out_bundle = self._apply_ua(
|
|
|
|
['UpdateRecord', 'Address', 21, {'amount': 10., 'city': 'NYC'}])
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
'envelopes': [{"recipients": []}],
|
|
|
|
'stored': [(0, ['UpdateRecord', 'Address', 21, {'amount': 10., 'city': 'NYC'}])],
|
|
|
|
'undo': [(0, ['UpdateRecord', 'Address', 21, {'amount': 1., 'city': 'New York'}])],
|
|
|
|
'calc': [],
|
|
|
|
'retValues': [None],
|
|
|
|
'rules': [],
|
|
|
|
})
|
|
|
|
|
|
|
|
def test_bundle_default_rules(self):
|
|
|
|
# Check that a newly-created document (which should have default rules) produces the same
|
|
|
|
# bundle as the trivial document without rules.
|
|
|
|
self._apply_ua(['InitNewDoc', 'UTC'])
|
|
|
|
|
|
|
|
# Create a schema for a table, and fill with some data.
|
|
|
|
self.apply_user_action(["AddTable", "Address", [
|
|
|
|
{"id": "city", "type": "Text"},
|
|
|
|
{"id": "state", "type": "Text"},
|
|
|
|
{"id": "amount", "type": "Numeric"},
|
|
|
|
]])
|
|
|
|
self.add_records("Address", self.starting_table_data[0], self.starting_table_data[1:])
|
|
|
|
self.assertTableData("Address", cols="subset", data=self.starting_table_data)
|
|
|
|
|
|
|
|
# Check that an action creates the same bundle as in the trivial case.
|
|
|
|
out_bundle = self._apply_ua(
|
|
|
|
['UpdateRecord', 'Address', 21, {'amount': 10., 'city': 'NYC'}])
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
'envelopes': [{"recipients": []}],
|
|
|
|
'stored': [(0, ['UpdateRecord', 'Address', 21, {'amount': 10., 'city': 'NYC'}])],
|
|
|
|
'undo': [(0, ['UpdateRecord', 'Address', 21, {'amount': 1., 'city': 'New York'}])],
|
|
|
|
'calc': [],
|
|
|
|
'retValues': [None],
|
|
|
|
'rules': [1],
|
|
|
|
})
|
|
|
|
|
|
|
|
# Once we add principals to Owners group, they should show up in the recipient list.
|
|
|
|
self.add_records('_grist_ACLPrincipals', ['id', 'type', 'userName', 'instanceId'], [
|
|
|
|
[20, 'user', 'foo@grist', ''],
|
|
|
|
[21, 'instance', '', '12345'],
|
|
|
|
[22, 'instance', '', '0abcd'],
|
|
|
|
])
|
|
|
|
self.add_records('_grist_ACLMemberships', ['parent', 'child'], [
|
|
|
|
[1, 20], # group 'Owners' contains user 'foo@grist'
|
|
|
|
[20, 21], # user 'foo@grist', contains instance '12345' and '67890'
|
|
|
|
[20, 22],
|
|
|
|
])
|
|
|
|
|
|
|
|
# Similar action to before, which is bundled as a single envelope, but includes recipients.
|
|
|
|
out_bundle = self._apply_ua(
|
|
|
|
['UpdateRecord', 'Address', 21, {'amount': 11., 'city': 'NYC2'}])
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
'envelopes': [{"recipients": ['0abcd', '12345']}],
|
|
|
|
'stored': [(0, ['UpdateRecord', 'Address', 21, {'amount': 11., 'city': 'NYC2'}])],
|
|
|
|
'undo': [(0, ['UpdateRecord', 'Address', 21, {'amount': 10., 'city': 'NYC'}])],
|
|
|
|
'calc': [],
|
|
|
|
'retValues': [None],
|
|
|
|
'rules': [1],
|
|
|
|
})
|
|
|
|
|
|
|
|
def init_employees_doc(self):
|
|
|
|
# Create a document with non-trivial rules, and check that actions are split correctly,
|
|
|
|
# using col/table/default rules, and including undo and calc actions.
|
|
|
|
#
|
|
|
|
# This is the structure we create:
|
|
|
|
# Columns Name, Position
|
|
|
|
# VIEW permission to group Employees
|
|
|
|
# EDITOR permission to groups Managers, Owners
|
|
|
|
# Default for columns
|
|
|
|
# EDITOR permission to groups Managers, Owners
|
|
|
|
|
|
|
|
self._apply_ua(['InitNewDoc', 'UTC'])
|
|
|
|
self.apply_user_action(["AddTable", "Employees", [
|
|
|
|
{"id": "name", "type": "Text"},
|
|
|
|
{"id": "position", "type": "Text"},
|
|
|
|
{"id": "ssn", "type": "Text"},
|
|
|
|
{"id": "salary", "type": "Numeric", "isFormula": True,
|
|
|
|
"formula": "100000 if $position.startswith('Senior') else 60000"},
|
|
|
|
]])
|
|
|
|
|
|
|
|
# Set up some groups and instances (skip Users for simplicity). See the assert below for
|
|
|
|
# better view of the created structure.
|
|
|
|
self.add_records('_grist_ACLPrincipals', ['id', 'type', 'groupName', 'instanceId'], [
|
|
|
|
[21, 'group', 'Managers', ''],
|
|
|
|
[22, 'group', 'Employees', ''],
|
|
|
|
[23, 'instance', '', 'alice'],
|
|
|
|
[24, 'instance', '', 'bob'],
|
|
|
|
[25, 'instance', '', 'chuck'],
|
|
|
|
[26, 'instance', '', 'eve'],
|
|
|
|
[27, 'instance', '', 'zack'],
|
|
|
|
])
|
|
|
|
# Set up Alice and Bob as Managers; Alice, Chuck, Eve as Employees; and Zack as an Owner.
|
|
|
|
self.add_records('_grist_ACLMemberships', ['parent', 'child'], [
|
|
|
|
[21, 23], [21, 24],
|
|
|
|
[22, 23], [22, 25], [22, 26],
|
|
|
|
[1, 27]
|
|
|
|
])
|
|
|
|
self.assertTableData('_grist_ACLPrincipals', cols="subset", data=[
|
|
|
|
['id', 'name', 'allInstances' ],
|
|
|
|
[1, 'Group:Owners', [27] ],
|
|
|
|
[2, 'Group:Admins', [] ],
|
|
|
|
[3, 'Group:Editors', [] ],
|
|
|
|
[4, 'Group:Viewers', [] ],
|
|
|
|
[21, 'Group:Managers', [23,24] ],
|
|
|
|
[22, 'Group:Employees', [23,25,26] ],
|
|
|
|
[23, 'Inst:alice', [23] ],
|
|
|
|
[24, 'Inst:bob', [24] ],
|
|
|
|
[25, 'Inst:chuck', [25] ],
|
|
|
|
[26, 'Inst:eve', [26] ],
|
|
|
|
[27, 'Inst:zack', [27] ],
|
|
|
|
])
|
|
|
|
|
|
|
|
# Set up some ACL resources and rules: for columns "name,position", give VIEW permission to
|
|
|
|
# Employees, EDITOR to Managers+Owners; for the rest, just Editor to Managers+Owners.
|
|
|
|
self.add_records('_grist_ACLResources', ['id', 'tableId', 'colIds'], [
|
|
|
|
[2, 'Employees', 'name,position'],
|
|
|
|
[3, 'Employees', ''],
|
|
|
|
])
|
|
|
|
self.add_records('_grist_ACLRules', ['id', 'resource', 'permissions', 'principals'], [
|
|
|
|
[12, 2, acl.Permissions.VIEW, ['L', 22]],
|
|
|
|
[13, 2, acl.Permissions.EDITOR, ['L', 21,1]],
|
|
|
|
[14, 3, acl.Permissions.EDITOR, ['L', 21,1]],
|
|
|
|
])
|
|
|
|
|
|
|
|
# OK, now to some actions. The table starts out empty.
|
|
|
|
self.assertTableData('Employees', [['id', 'manualSort', 'name', 'position', 'salary', 'ssn']])
|
|
|
|
|
|
|
|
|
|
|
|
def test_rules_order(self):
|
|
|
|
# Test that shows the problem with the ordering of actions in Envelopes.
|
|
|
|
self.init_employees_doc()
|
|
|
|
self._apply_ua(self.add_records_action('Employees', [
|
|
|
|
['name', 'position', 'ssn'],
|
|
|
|
['John', 'Scientist', '000-00-0000'],
|
|
|
|
['Ellen', 'Senior Scientist', '111-11-1111'],
|
|
|
|
['Susie', 'Manager', '222-22-2222'],
|
|
|
|
['Frank', 'Senior Manager', '222-22-2222'],
|
|
|
|
]))
|
|
|
|
out_bundle = self._apply_ua(['ApplyDocActions', [
|
|
|
|
['UpdateRecord', 'Employees', 1, {'ssn': 'xxx-xx-0000'}],
|
|
|
|
['UpdateRecord', 'Employees', 1, {'position': 'Senior Jester'}],
|
|
|
|
['UpdateRecord', 'Employees', 1, {'ssn': 'yyy-yy-0000'}],
|
|
|
|
]])
|
|
|
|
self.assertTableData('Employees', cols="subset", data=[
|
|
|
|
['id', 'name', 'position', 'salary', 'ssn'],
|
|
|
|
[1, 'John', 'Senior Jester', 100000.0, 'yyy-yy-0000'],
|
|
|
|
[2, 'Ellen', 'Senior Scientist', 100000.0, '111-11-1111'],
|
|
|
|
[3, 'Susie', 'Manager', 60000.0, '222-22-2222'],
|
|
|
|
[4, 'Frank', 'Senior Manager', 100000.0, '222-22-2222'],
|
|
|
|
])
|
|
|
|
|
|
|
|
# Check the main aspects of the created bundles.
|
|
|
|
env = out_bundle.envelopes
|
|
|
|
|
|
|
|
# We expect two envelopes: one for Managers+Owners, one for all including Employees,
|
|
|
|
# because 'ssn' and 'position' columns are resources with different permissions.
|
|
|
|
# Note how non-consecutive actions may belong to the same envelope. This is needed to allow
|
|
|
|
# users (e.g. alice in this example) to process DocActions in the same order as how they were
|
|
|
|
# created, even when alice is present in different sets of recipients.
|
|
|
|
self.assertEqual(env[0].recipients, {"alice", "bob", "zack"})
|
|
|
|
self.assertEqual(env[1].recipients, {"alice", "bob", "zack", "chuck", "eve"})
|
|
|
|
self.assertEqual(out_bundle.stored, [
|
|
|
|
(0, actions.UpdateRecord('Employees', 1, {'ssn': 'xxx-xx-0000'})),
|
|
|
|
(1, actions.UpdateRecord('Employees', 1, {'position': 'Senior Jester'})),
|
|
|
|
(0, actions.UpdateRecord('Employees', 1, {'ssn': 'yyy-yy-0000'})),
|
2020-11-02 15:48:47 +00:00
|
|
|
(0, actions.UpdateRecord('Employees', 1, {'salary': 100000.00})),
|
2020-07-27 18:57:36 +00:00
|
|
|
])
|
2020-11-02 15:48:47 +00:00
|
|
|
self.assertEqual(out_bundle.calc, [])
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_with_rules(self):
|
|
|
|
self.init_employees_doc()
|
|
|
|
|
|
|
|
out_bundle = self._apply_ua(self.add_records_action('Employees', [
|
|
|
|
['name', 'position', 'ssn'],
|
|
|
|
['John', 'Scientist', '000-00-0000'],
|
|
|
|
['Ellen', 'Senior Scientist', '111-11-1111'],
|
|
|
|
['Susie', 'Manager', '222-22-2222'],
|
|
|
|
['Frank', 'Senior Manager', '222-22-2222'],
|
|
|
|
]))
|
|
|
|
|
|
|
|
# Check the main aspects of the output.
|
|
|
|
env = out_bundle.envelopes
|
|
|
|
|
|
|
|
# We expect two envelopes: one for Managers+Owners, one for all including Employees.
|
|
|
|
self.assertEqual([e.recipients for e in env], [
|
|
|
|
{"alice","chuck","eve","bob","zack"},
|
|
|
|
{"alice", "bob", "zack"}
|
|
|
|
])
|
|
|
|
# Only "name" and "position" are sent to Employees; the rest only to Managers+Owners.
|
|
|
|
self.assertEqual([(env, set(a.columns)) for (env, a) in out_bundle.stored], [
|
|
|
|
(0, {"name", "position"}),
|
|
|
|
(1, {"ssn", "manualSort"}),
|
2020-11-02 15:48:47 +00:00
|
|
|
(1, {"salary"}),
|
2020-07-27 18:57:36 +00:00
|
|
|
])
|
2020-11-02 15:48:47 +00:00
|
|
|
self.assertEqual([(env, set(a.columns)) for (env, a) in out_bundle.calc], [])
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
# Full bundle requires careful reading. See the checks above for the essential parts.
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
"envelopes": [
|
|
|
|
{"recipients": [ "alice", "bob", "chuck", "eve", "zack" ]},
|
|
|
|
{"recipients": [ "alice", "bob", "zack" ]},
|
|
|
|
],
|
|
|
|
"stored": [
|
|
|
|
# TODO Yikes, there is a problem here! We have two envelopes, each with BulkAddRecord
|
|
|
|
# actions, but some recipients receive BOTH envelopes. What is "alice" to do with two
|
|
|
|
# separate BulkAddRecord actions that both include rowIds 1, 2, 3, 4?
|
|
|
|
(0, [ "BulkAddRecord", "Employees", [ 1, 2, 3, 4 ], {
|
|
|
|
"position": [ "Scientist", "Senior Scientist", "Manager", "Senior Manager" ],
|
|
|
|
"name": [ "John", "Ellen", "Susie", "Frank" ]
|
|
|
|
}]),
|
|
|
|
(1, [ "BulkAddRecord", "Employees", [ 1, 2, 3, 4 ], {
|
|
|
|
"manualSort": [ 1, 2, 3, 4 ],
|
|
|
|
"ssn": [ "000-00-0000", "111-11-1111", "222-22-2222", "222-22-2222" ]
|
|
|
|
}]),
|
2020-11-02 15:48:47 +00:00
|
|
|
(1, [ "BulkUpdateRecord", "Employees", [ 1, 2, 3, 4 ], {
|
|
|
|
"salary": [ 60000, 100000, 60000, 100000 ]
|
|
|
|
}]),
|
2020-07-27 18:57:36 +00:00
|
|
|
],
|
|
|
|
"undo": [
|
|
|
|
# TODO All recipients now get BulkRemoveRecord (which is correct), but some get it twice,
|
|
|
|
# which is a simpler manifestation of the problem with BulkAddRecord.
|
|
|
|
(0, [ "BulkRemoveRecord", "Employees", [ 1, 2, 3, 4 ] ]),
|
|
|
|
(1, [ "BulkRemoveRecord", "Employees", [ 1, 2, 3, 4 ] ]),
|
|
|
|
],
|
2020-11-02 15:48:47 +00:00
|
|
|
"calc": [],
|
2020-07-27 18:57:36 +00:00
|
|
|
"retValues": [[1, 2, 3, 4]],
|
|
|
|
"rules": [12,13,14],
|
|
|
|
})
|
|
|
|
|
|
|
|
def test_empty_add_record(self):
|
|
|
|
self.init_employees_doc()
|
|
|
|
|
|
|
|
out_bundle = self._apply_ua(['AddRecord', 'Employees', None, {}])
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
"envelopes": [{"recipients": [ "alice", "bob", "chuck", "eve", "zack" ]},
|
|
|
|
{"recipients": [ "alice", "bob", "zack" ]} ],
|
|
|
|
# TODO Note the same issues as in previous test case: some recipients receive duplicate or
|
|
|
|
# near-duplicate AddRecord and RemoveRecord actions, governed by different rules.
|
|
|
|
"stored": [
|
|
|
|
(0, [ "AddRecord", "Employees", 1, {}]),
|
|
|
|
(1, [ "AddRecord", "Employees", 1, {"manualSort": 1.0}]),
|
2020-11-02 15:48:47 +00:00
|
|
|
(1, [ "UpdateRecord", "Employees", 1, { "salary": 60000.0 }]),
|
2020-07-27 18:57:36 +00:00
|
|
|
],
|
|
|
|
"undo": [
|
|
|
|
(0, [ "RemoveRecord", "Employees", 1 ]),
|
|
|
|
(1, [ "RemoveRecord", "Employees", 1 ]),
|
|
|
|
],
|
2020-11-02 15:48:47 +00:00
|
|
|
"calc": [],
|
2020-07-27 18:57:36 +00:00
|
|
|
"retValues": [1],
|
|
|
|
"rules": [12,13,14],
|
|
|
|
})
|
|
|
|
|
|
|
|
out_bundle = self._apply_ua(['UpdateRecord', 'Employees', 1, {"position": "Senior Citizen"}])
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
"envelopes": [{"recipients": [ "alice", "bob", "chuck", "eve", "zack" ]},
|
|
|
|
{"recipients": [ "alice", "bob", "zack" ]} ],
|
|
|
|
"stored": [
|
|
|
|
(0, [ "UpdateRecord", "Employees", 1, {"position": "Senior Citizen"}]),
|
2020-11-02 15:48:47 +00:00
|
|
|
(1, [ "UpdateRecord", "Employees", 1, { "salary": 100000.0 }])
|
2020-07-27 18:57:36 +00:00
|
|
|
],
|
|
|
|
"undo": [
|
|
|
|
(0, [ "UpdateRecord", "Employees", 1, {"position": ""}]),
|
2020-11-02 15:48:47 +00:00
|
|
|
(1, [ "UpdateRecord", "Employees", 1, { "salary": 60000.0 }])
|
2020-07-27 18:57:36 +00:00
|
|
|
],
|
2020-11-02 15:48:47 +00:00
|
|
|
"calc": [],
|
2020-07-27 18:57:36 +00:00
|
|
|
"retValues": [None],
|
|
|
|
"rules": [12,13,14],
|
|
|
|
})
|
|
|
|
|
|
|
|
def test_add_user(self):
|
|
|
|
self.init_employees_doc()
|
|
|
|
|
|
|
|
out_bundle = self._apply_ua(['AddUser', 'f@g.c', 'Fred', ['XXX', 'YYY']])
|
|
|
|
self.assertEqual(out_bundle.to_json_obj(), {
|
|
|
|
# TODO: Only Owners are getting these metadata changes, but all users should get them.
|
|
|
|
"envelopes": [{"recipients": [ "XXX", "YYY", "zack" ]}],
|
|
|
|
"stored": [
|
|
|
|
(0, [ "AddRecord", "_grist_ACLPrincipals", 28, {
|
|
|
|
'type': 'user', 'userEmail': 'f@g.c', 'userName': 'Fred'}]),
|
|
|
|
(0, [ "BulkAddRecord", "_grist_ACLPrincipals", [29, 30], {
|
|
|
|
'type': ['instance', 'instance'],
|
|
|
|
'instanceId': ['XXX', 'YYY']
|
|
|
|
}]),
|
|
|
|
(0, [ "BulkAddRecord", "_grist_ACLMemberships", [7, 8, 9], {
|
|
|
|
# Adds instances (29, 30) to user (28), and user (28) to group owners (1)
|
|
|
|
'parent': [28, 28, 1],
|
|
|
|
'child': [29, 30, 28],
|
|
|
|
}]),
|
|
|
|
],
|
|
|
|
"undo": [
|
|
|
|
(0, [ "RemoveRecord", "_grist_ACLPrincipals", 28]),
|
|
|
|
(0, [ "BulkRemoveRecord", "_grist_ACLPrincipals", [29, 30]]),
|
|
|
|
(0, [ "BulkRemoveRecord", "_grist_ACLMemberships", [7, 8, 9]]),
|
|
|
|
],
|
|
|
|
"calc": [
|
|
|
|
],
|
|
|
|
"retValues": [None],
|
|
|
|
"rules": [1],
|
|
|
|
})
|
|
|
|
|
|
|
|
def test_doc_snapshot(self):
|
|
|
|
self.init_employees_doc()
|
|
|
|
|
|
|
|
# Apply an action to the initial employees doc to make the test case more complex
|
|
|
|
self.add_records('Employees', ['name', 'position', 'ssn'], [
|
|
|
|
['John', 'Scientist', '000-00-0000'],
|
|
|
|
['Ellen', 'Senior Scientist', '111-11-1111'],
|
|
|
|
['Susie', 'Manager', '222-22-2222'],
|
|
|
|
['Frank', 'Senior Manager', '222-22-2222']
|
|
|
|
])
|
|
|
|
|
|
|
|
# Retrieve the doc snapshot and split it
|
|
|
|
snapshot_action_group = self.engine.fetch_snapshot()
|
|
|
|
snapshot_bundle = self.engine.acl_split(snapshot_action_group)
|
|
|
|
|
|
|
|
init_schema_actions = [actions.get_action_repr(a) for a in schema.schema_create_actions()]
|
|
|
|
|
|
|
|
# We check that the unsplit doc snapshot bundle includes all the necessary actions
|
|
|
|
# to rebuild the doc
|
|
|
|
snapshot = snapshot_action_group.get_repr()
|
|
|
|
self.assertEqual(snapshot['calc'], [])
|
|
|
|
self.assertEqual(snapshot['retValues'], [])
|
|
|
|
self.assertEqual(snapshot['undo'], [])
|
|
|
|
|
|
|
|
stored_subset = [
|
|
|
|
['AddTable', 'Employees',
|
|
|
|
[{'formula': '','id': 'manualSort','isFormula': False,'type': 'ManualSortPos'},
|
|
|
|
{'formula': '','id': 'name','isFormula': False,'type': 'Text'},
|
|
|
|
{'formula': '','id': 'position','isFormula': False,'type': 'Text'},
|
|
|
|
{'formula': '','id': 'ssn','isFormula': False,'type': 'Text'},
|
|
|
|
{'formula': "100000 if $position.startswith('Senior') else 60000",
|
|
|
|
'id': 'salary',
|
|
|
|
'isFormula': True,
|
|
|
|
'type': 'Numeric'}]],
|
|
|
|
['BulkAddRecord', '_grist_Tables', [1],
|
|
|
|
{'primaryViewId': [1],
|
|
|
|
'summarySourceTable': [0],
|
|
|
|
'tableId': ['Employees'],
|
|
|
|
'onDemand': [False]}],
|
|
|
|
['BulkAddRecord', 'Employees', [1, 2, 3, 4], {
|
|
|
|
'manualSort': [1.0, 2.0, 3.0, 4.0],
|
|
|
|
'name': ['John', 'Ellen', 'Susie', 'Frank'],
|
|
|
|
'position': ['Scientist', 'Senior Scientist', 'Manager', 'Senior Manager'],
|
|
|
|
'ssn': ['000-00-0000', '111-11-1111', '222-22-2222', '222-22-2222']
|
|
|
|
}],
|
|
|
|
['BulkAddRecord','_grist_Tables_column',[1, 2, 3, 4, 5],
|
|
|
|
{'colId': ['manualSort', 'name', 'position', 'ssn', 'salary'],
|
|
|
|
'displayCol': [0, 0, 0, 0, 0],
|
|
|
|
'formula': ['','','','',"100000 if $position.startswith('Senior') else 60000"],
|
|
|
|
'isFormula': [False, False, False, False, True],
|
|
|
|
'label': ['manualSort', 'name', 'position', 'ssn', 'salary'],
|
|
|
|
'parentId': [1, 1, 1, 1, 1],
|
|
|
|
'parentPos': [1.0, 2.0, 3.0, 4.0, 5.0],
|
|
|
|
'summarySourceCol': [0, 0, 0, 0, 0],
|
|
|
|
'type': ['ManualSortPos', 'Text', 'Text', 'Text', 'Numeric'],
|
|
|
|
'untieColIdFromLabel': [False, False, False, False, False],
|
|
|
|
'widgetOptions': ['', '', '', '', ''],
|
|
|
|
'visibleCol': [0, 0, 0, 0, 0]}]
|
|
|
|
]
|
|
|
|
for action in stored_subset:
|
|
|
|
self.assertIn(action, snapshot['stored'])
|
|
|
|
|
|
|
|
# We check that the full doc snapshot bundle is split as expected
|
|
|
|
snapshot_bundle_json = snapshot_bundle.to_json_obj()
|
|
|
|
self.assertEqual(snapshot_bundle_json['envelopes'], [
|
|
|
|
{'recipients': ['#ALL']},
|
|
|
|
{'recipients': ['zack']},
|
|
|
|
{'recipients': ['alice', 'bob', 'chuck', 'eve', 'zack']},
|
|
|
|
{'recipients': ['alice', 'bob', 'zack']}
|
|
|
|
])
|
|
|
|
self.assertEqual(snapshot_bundle_json['calc'], [])
|
|
|
|
self.assertEqual(snapshot_bundle_json['retValues'], [])
|
|
|
|
self.assertEqual(snapshot_bundle_json['undo'], [])
|
|
|
|
self.assertEqual(snapshot_bundle_json['rules'], [1, 12, 13, 14])
|
|
|
|
|
|
|
|
stored_subset = ([(0, action_repr) for action_repr in init_schema_actions] + [
|
|
|
|
(0, ['AddTable', 'Employees',
|
|
|
|
[{'formula': '','id': 'manualSort','isFormula': False,'type': 'ManualSortPos'},
|
|
|
|
{'formula': '','id': 'name','isFormula': False,'type': 'Text'},
|
|
|
|
{'formula': '','id': 'position','isFormula': False,'type': 'Text'},
|
|
|
|
{'formula': '','id': 'ssn','isFormula': False,'type': 'Text'},
|
|
|
|
{'formula': "100000 if $position.startswith('Senior') else 60000",
|
|
|
|
'id': 'salary',
|
|
|
|
'isFormula': True,
|
|
|
|
'type': 'Numeric'}]]),
|
|
|
|
# TODO (High-priority): The following action only received by 'zack' when it should be
|
|
|
|
# received by everyone.
|
|
|
|
(1, ['BulkAddRecord', '_grist_Tables', [1],
|
|
|
|
{'primaryViewId': [1],
|
|
|
|
'summarySourceTable': [0],
|
|
|
|
'tableId': ['Employees'],
|
|
|
|
'onDemand': [False]}]),
|
|
|
|
(2, ['BulkAddRecord', 'Employees', [1, 2, 3, 4],
|
|
|
|
{'name': ['John', 'Ellen', 'Susie', 'Frank'],
|
|
|
|
'position': ['Scientist', 'Senior Scientist', 'Manager', 'Senior Manager']}]),
|
|
|
|
(3, ['BulkAddRecord', 'Employees', [1, 2, 3, 4],
|
|
|
|
{'manualSort': [1.0, 2.0, 3.0, 4.0],
|
|
|
|
'ssn': ['000-00-0000', '111-11-1111', '222-22-2222', '222-22-2222']}]),
|
|
|
|
(1, ['BulkAddRecord', '_grist_Tables_column', [1, 2, 3, 4, 5],
|
|
|
|
{'colId': ['manualSort','name','position','ssn','salary'],
|
|
|
|
'displayCol': [0, 0, 0, 0, 0],
|
|
|
|
'formula': ['','','','',"100000 if $position.startswith('Senior') else 60000"],
|
|
|
|
'isFormula': [False, False, False, False, True],
|
|
|
|
'label': ['manualSort','name','position','ssn','salary'],
|
|
|
|
'parentId': [1, 1, 1, 1, 1],
|
|
|
|
'parentPos': [1.0, 2.0, 3.0, 4.0, 5.0],
|
|
|
|
'summarySourceCol': [0, 0, 0, 0, 0],
|
|
|
|
'type': ['ManualSortPos','Text','Text','Text','Numeric'],
|
|
|
|
'untieColIdFromLabel': [False, False, False, False, False],
|
|
|
|
'widgetOptions': ['', '', '', '', ''],
|
|
|
|
'visibleCol': [0, 0, 0, 0, 0]}])
|
|
|
|
])
|
|
|
|
for action in stored_subset:
|
|
|
|
self.assertIn(action, snapshot_bundle_json['stored'])
|