(core) discount indirect changes for access control purposes

Summary:
This diff discounts indirect changes for access control purposes.  A UserAction that updates a cell A, which in turn causes changes in other dependent cells, will be considered a change to cell A for access control purposes.

The `engine.apply_user_actions` method now returns a `direct` array, with a boolean for each `stored` action, set to `true` if the action is attributed to the user or `false` if it is attributed to the engine.  `GranularAccess` ignores actions attributed to the engine when checking for edit rights.

Subtleties:
 * Removal of references to a removed row are considered direct changes.
 * Doesn't play well with undos as yet.  An action that indirectly modifies a cell the user doesn't have rights to may succeed, but it will not be reversible.

Test Plan: added tests, updated tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2806
This commit is contained in:
Paul Fitzpatrick
2021-05-12 11:04:37 -04:00
parent 8d62a857e1
commit d0d3d3d0c9
14 changed files with 175 additions and 48 deletions

View File

@@ -123,7 +123,7 @@ class EngineTestCase(unittest.TestCase):
self.assertEqualDocData({table_name: observed}, {table_name: expected})
action_group_action_fields = ("stored", "undo", "calc")
action_group_action_fields = ("stored", "undo", "calc", "direct")
@classmethod
def _formatActionGroup(cls, action_group, use_repr=False):
@@ -149,13 +149,12 @@ class EngineTestCase(unittest.TestCase):
# Do some clean up on the observed data.
observed = testutil.replace_nans(observed)
# Convert expected actions into a comparable form.
# Convert observed and expected actions into a comparable form.
for k in self.action_group_action_fields:
if k in observed:
observed[k] = map(actions.get_action_repr, observed[k])
observed[k] = map(get_comparable_repr, observed[k])
if k in expected:
expected[k] = [actions.get_action_repr(a) if not isinstance(a, list) else a
for a in expected[k]]
expected[k] = map(get_comparable_repr, expected[k])
if observed != expected:
o_lines = self._formatActionGroup(observed)
@@ -552,6 +551,11 @@ def create_test_case(name, body):
self._run_test_body(name, body)
setattr(TestEngine, name, run)
# Convert observed/expected action into a comparable form.
def get_comparable_repr(a):
if isinstance(a, (list, int)):
return a
return actions.get_action_repr(a)
# Parse and create test cases on module load. This way the python unittest feature to run only
# particular test cases can apply to these cases too.