Role/permission mechanism for E2E/integration testing
Example Use Case
Suppose you're writing an automated test which hits the end-point
civicrm/mailing/view?id=XYZ. The behavior of this end-point depends on several variables -- such as the value of
civicrm_mailing.visibility, whether the permission
view public CiviMail content is active, the content of any IDs or hash-codes in the URL, and the identity of the user in the current CMS. Good test-coverage would exercise several of those permutations (by manipulating or mocking the variables involved).
Specifically, this issue is about manipulating/mocking the active permissions (
view public CiviMail content, et al) for use in a multi-process test.
- For CiviCRM's
WebTestsuite, there was a long-time dependency on
civicrm_webtest.module, which defined a role and a bunch of permissions. However, it defined exactly one role with a static list of permissions, which isn't useful for testing multiple variations -- and the implementation only worked on D7.
- It is possible to mock permissions with one of the following:
hook_civicrm_permission_check. However, these only applies to the current process - if you make an HTTP request to test an end-point, the mocking disappears.
- Each CMS has a model and an API for manipulating users/roles/permissions. The models appear similar from a 30k ft view, but they're different in a low-level view.
- The ACL system provides a fine-grained access control matrix (e.g. access to view or edit a specific list of contacts), but it doesn't provide coarse grained permissions.
The Stateful Concrete Option: Define a small data-store (SQL table or setting). Expose an API for granting permissions to specific contacts. In writing a test, one might say:
civicrm_api4('Contact', 'grant',[ 'where' => [['id', '=', 123]], 'permissions' => ['view public CiviMail content'], ]);
The Stateless Concrete Option: Define a cookie or HTTP header which can be used to temporarily grant permissions. Require a signed+timestamped token. In writing a test, one might say:
$jar = \GuzzleHttp\Cookie\CookieJar::fromArray([ 'CIVI_PERM' => createSignedToken(['view public CiviMail content']), ], 'localhost'); $client = new \GuzzleHttp\Client(['cookies' => $jar, ...]);
- The Abstract Option: Create an abstraction which wraps around the user/role/group/permission. Implement it on every CMS.
- It's possible to build such a system on top of existing APIs (eg
hook_civicrm_permission_check). The functionality could be coded as an extension. However, one does need to consider the dependency-graph/distribution-mechanics. Does it make sense for a core test to depend on an external extension?
- To avoid confusion with the normal CMS-based permissioning and to prevent its use as an escalation vector:
- The mechanism should have a toggle and be disabled by default.
- Enabling it should require some kind of super-admin or sysadmin right. (Enabling this feature would have the same
Considerations (Stateful Option)
- To be useful for testing:
- Should allow granting permissions specifically to an example contact.
- Should allow granting permissions generally to anonymous and any/all contacts.
- The following elements are not needed for general testing/mocking purposes. I don't have any strong objection to their existing -- they're just different/out-of-scope for my issue here.
- Providing a GUI for CRUD'ing the permissions
- Granting permissions to static-groups
- Granting permissions to smart-groups
Considerations (Abstract Option)
- An abstract service has a larger footprint (i.e. needs to be written/tested/maintained in multiple environments, each of which are subject to change).
- CMS's generally assign permissions on per-role/per-group basis. This makes sense, but it's unnecessary indirection if your goal is to write an automated test. (It is easier to (a) mock contact + permission than (b) mock contact + role + role/contact-assignment + role/contact-permission.)