*/
class FormDataModel {
+ protected $defaults = ['security' => 'RBAC', 'actions' => ['create' => TRUE, 'update' => TRUE]];
+
/**
* @var array
* Ex: $entities['spouse']['type'] = 'Contact';
$root = AHQ::makeRoot($layout);
$this->entities = array_column(AHQ::getTags($root, 'af-entity'), NULL, 'name');
foreach (array_keys($this->entities) as $entity) {
+ $this->entities[$entity] = array_merge($this->defaults, $this->entities[$entity]);
$this->entities[$entity]['fields'] = $this->entities[$entity]['joins'] = [];
}
// Pre-load full list of afforms in case this layout embeds other afform directives
throw new UnauthorizedException("Cannot delegate APIv4 calls on behalf of unrecognized entity ($entityName)");
}
$this->secureApi4s[$entityName] = function(string $entity, string $action, $params = [], $index = NULL) use ($entityName) {
- // FIXME Pick real value of checkPermissions. Possibly limit by ID.
- // \Civi::log()->info("secureApi4($entityName): call($entity, $action)");
- // $params['checkPermissions'] = FALSE;
- $params['checkPermissions'] = TRUE;
+ $entityDefn = $this->entities[$entityName];
+
+ switch ($entityDefn['security']) {
+ // Role-based access control. Limits driven by the current user's role/group/permissions.
+ case 'RBAC':
+ $params['checkPermissions'] = TRUE;
+ break;
+
+ // Form-based access control. Limits driven by form configuration.
+ case 'FBAC':
+ $params['checkPermissions'] = FALSE;
+ break;
+
+ default:
+ throw new UnauthorizedException("Cannot process APIv4 request for $entityName ($entity.$action): Unrecognized security model");
+ }
+
+ if (!$this->isActionAllowed($entityDefn, $entity, $action, $params)) {
+ throw new UnauthorizedException("Cannot process APIv4 request for $entityName ($entity.$action): Action is not approved");
+ }
+
return civicrm_api4($entity, $action, $params, $index);
};
}
return $this->secureApi4s[$entityName];
}
+ /**
+ * Determine if we are allowed to perform a given action for this entity.
+ *
+ * @param $entityDefn
+ * @param $entity
+ * @param $action
+ * @param $params
+ *
+ * @return bool
+ */
+ protected function isActionAllowed($entityDefn, $entity, $action, $params) {
+ if ($action === 'save') {
+ foreach ($params['records'] ?? [] as $record) {
+ $nextAction = !isset($record['id']) ? 'create' : 'update';
+ if (!$this->isActionAllowed($entityDefn, $entity, $nextAction, $record)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+
+ // "Update" effectively means "read+save".
+ if ($action === 'get') {
+ $action = 'update';
+ }
+
+ $result = !empty($entityDefn['actions'][$action]);
+ return $result;
+ }
+
/**
* @param array $nodes
* @param string $entity
'propB' => ['name' => 'propB', 'defn' => ['title' => 'Whiz']],
],
'joins' => [],
+ 'security' => 'RBAC',
+ 'actions' => ['create' => 1, 'update' => 1],
],
],
];
'name' => 'foobar',
'fields' => [],
'joins' => [],
+ 'security' => 'RBAC',
+ 'actions' => ['create' => 1, 'update' => 1],
],
'whiz_bang' => [
'type' => 'Whiz',
'name' => 'whiz_bang',
'fields' => [],
'joins' => [],
+ 'security' => 'RBAC',
+ 'actions' => ['create' => 1, 'update' => 1],
+ ],
+ ],
+ ];
+
+ $cases[] = [
+ 'html' => '<af-form><div><af-entity type="Foo" name="foobar" security="FBAC" actions="{create: false, update: true}"/></div></af-form>',
+ 'entities' => [
+ 'foobar' => [
+ 'type' => 'Foo',
+ 'name' => 'foobar',
+ 'fields' => [],
+ 'joins' => [],
+ 'security' => 'FBAC',
+ 'actions' => ['create' => FALSE, 'update' => TRUE],
],
],
];
+<!-- This example is entirely public; anonymous users may use it to submit a `Contact`, but they cannot view or modify data. -->
<af-form ctrl="afform">
- <af-entity data="{contact_type: 'Individual', source: 'Hello'}" url-autofill="1" type="Contact" name="me" label="Myself" />
+ <af-entity data="{contact_type: 'Individual', source: 'Hello'}" url-autofill="1" security="FBAC" actions="{create: true, update: false}" type="Contact" name="me" label="Myself" />
<fieldset af-fieldset="me">
<legend class="af-text">Individual 1</legend>
<div class="af-container">
--- /dev/null
+<?php
+
+return [
+ 'html' => '<af-form ctrl="afform"><af-entity security="FBAC" type="Contact" name="me" data="{contact_type: \'Individual\', source: \'Hello\'}" actions="{create: 1, update: 0}" /></af-form>',
+ 'pretty' => '<af-form ctrl="afform">
+ <af-entity security="FBAC" type="Contact" name="me" data="{contact_type: \'Individual\', source: \'Hello\'}" actions="{create: 1, update: 0}" />
+</af-form>
+',
+ 'stripped' => [
+ [
+ '#tag' => 'af-form',
+ 'ctrl' => 'afform',
+ '#children' => [
+ [
+ '#tag' => 'af-entity',
+ 'security' => 'FBAC',
+ 'type' => 'Contact',
+ 'name' => 'me',
+ 'data' => '{contact_type: \'Individual\', source: \'Hello\'}',
+ 'actions' => '{create: 1, update: 0}',
+ ],
+ ],
+ ],
+ ],
+ 'shallow' => [
+ [
+ '#tag' => 'af-form',
+ 'ctrl' => 'afform',
+ '#children' => [
+ [
+ '#tag' => 'af-entity',
+ 'security' => 'FBAC',
+ 'type' => 'Contact',
+ 'name' => 'me',
+ 'data' => '{contact_type: \'Individual\', source: \'Hello\'}',
+ 'actions' => '{create: 1, update: 0}',
+ ],
+ ],
+ ],
+ ],
+ 'deep' => [
+ [
+ '#tag' => 'af-form',
+ 'ctrl' => 'afform',
+ '#children' => [
+ [
+ '#tag' => 'af-entity',
+ 'security' => 'FBAC',
+ 'type' => 'Contact',
+ 'name' => 'me',
+ 'data' => ['contact_type' => 'Individual', 'source' => 'Hello'],
+ 'actions' => ['create' => 1, 'update' => 0],
+ ],
+ ],
+ ],
+ ],
+];