--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\API\Subscriber;
+
+use Civi\API\Events;
+use Civi\API\Event\AuthorizeEvent;
+use Civi\API\Event\RespondEvent;
+use Civi\API\WhitelistRule;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * The WhitelistSubscriber listens to API requests and matches them against
+ * a whitelist of allowed API calls. If an API call does NOT appear in the
+ * whitelist, then it generates an error.
+ *
+ * @package Civi
+ * @copyright CiviCRM LLC (c) 2004-2014
+ */
+class WhitelistSubscriber implements EventSubscriberInterface {
+
+ /**
+ * @return array
+ */
+ public static function getSubscribedEvents() {
+ return array(
+ Events::AUTHORIZE => array('onApiAuthorize', Events::W_EARLY),
+ Events::RESPOND => array('onApiRespond', Events::W_MIDDLE),
+ );
+ }
+
+ /**
+ * Array(WhitelistRule).
+ *
+ * @var array
+ */
+ protected $rules;
+
+ /**
+ * Array (scalar $reqId => WhitelistRule $rule).
+ *
+ * @var array
+ */
+ protected $activeRules;
+
+ /**
+ * @param array $rules
+ * Array of WhitelistRule.
+ * @see WhitelistRule
+ */
+ public function __construct($rules) {
+ $this->rules = array();
+ foreach ($rules as $rule) {
+ /** @var WhitelistRule $rule */
+ if ($rule->isValid()) {
+ $this->rules[] = $rule;
+ }
+ else {
+ throw new \CRM_Core_Exception("Invalid rule");
+ }
+ }
+ }
+
+ /**
+ * Determine which, if any, whitelist rules apply this request.
+ * Reject unauthorized requests.
+ *
+ * @param AuthorizeEvent $event
+ * @throws \CRM_Core_Exception
+ */
+ public function onApiAuthorize(AuthorizeEvent $event) {
+ $apiRequest = $event->getApiRequest();
+ if (empty($apiRequest['params']['check_permissions']) || $apiRequest['params']['check_permissions'] !== 'whitelist') {
+ return;
+ }
+ foreach ($this->rules as $rule) {
+ if (TRUE === $rule->matches($apiRequest)) {
+ $this->activeRules[$apiRequest['id']] = $rule;
+ return;
+ }
+ }
+ throw new \CRM_Core_Exception('The request does not match any active API authorizations.');
+ }
+
+ /**
+ * Apply any filtering rules based on the chosen whitelist rule.
+ * @param RespondEvent $event
+ */
+ public function onApiRespond(RespondEvent $event) {
+ $apiRequest = $event->getApiRequest();
+ $id = $apiRequest['id'];
+ if (isset($this->activeRules[$id])) {
+ $event->setResponse($this->activeRules[$id]->filter($apiRequest, $event->getResponse()));
+ unset($this->activeRules[$id]);
+ }
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6 |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014 |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM. |
+ | |
+ | CiviCRM is free software; you can copy, modify, and distribute it |
+ | under the terms of the GNU Affero General Public License |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
+ | |
+ | CiviCRM is distributed in the hope that it will be useful, but |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
+ | See the GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public |
+ | License and the CiviCRM Licensing Exception along |
+ | with this program; if not, contact CiviCRM LLC |
+ | at info[AT]civicrm[DOT]org. If you have questions about the |
+ | GNU Affero General Public License or the licensing of CiviCRM, |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\API;
+
+/**
+ * A WhitelistRule is used to determine if an API call is authorized.
+ * For example:
+ *
+ * @code
+ * new WhitelistRule(array(
+ * 'entity' => 'Contact',
+ * 'actions' => array('get','getsingle'),
+ * 'required' => array('contact_type' => 'Organization'),
+ * 'fields' => array('id', 'display_name', 'sort_name', 'created_date'),
+ * ));
+ * @endcode
+ *
+ * This rule would allow API requests that attempt to get contacts of type "Organization",
+ * but only a handful of fields ('id', 'display_name', 'sort_name', 'created_date')
+ * can be filtered or returned.
+ *
+ * Class WhitelistRule
+ * @package Civi\API\Subscriber
+ */
+class WhitelistRule {
+
+ static $IGNORE_FIELDS = array(
+ 'check_permissions',
+ 'debug',
+ 'offset',
+ 'option_offset',
+ 'option_limit',
+ 'option_sort',
+ 'options',
+ 'return',
+ 'rowCount',
+ 'sequential',
+ 'sort',
+ 'version',
+ );
+
+ /**
+ * Create a batch of rules from an array.
+ *
+ * @param array $rules
+ * @return array
+ */
+ public static function createAll($rules) {
+ $whitelist = array();
+ foreach ($rules as $rule) {
+ $whitelist[] = new WhitelistRule($rule);
+ }
+ return $whitelist;
+ }
+
+ /**
+ * @var int
+ */
+ public $version;
+
+ /**
+ * Entity name or '*' (all entities)
+ *
+ * @var string
+ */
+ public $entity;
+
+ /**
+ * List of actions which match, or '*' (all actions)
+ *
+ * @var string|array
+ */
+ public $actions;
+
+ /**
+ * List of key=>value pairs that *must* appear in $params.
+ *
+ * If there are no required fields, use an empty array.
+ *
+ * @var array
+ */
+ public $required;
+
+ /**
+ * List of fields which may be optionally inputted or returned, or '*" (all fields)
+ *
+ * @var array
+ */
+ public $fields;
+
+ public function __construct($ruleSpec) {
+ $this->version = $ruleSpec['version'];
+
+ if ($ruleSpec['entity'] === '*') {
+ $this->entity = '*';
+ }
+ else {
+ $this->entity = Request::normalizeEntityName($ruleSpec['entity'], $ruleSpec['version']);
+ }
+
+ if ($ruleSpec['actions'] === '*') {
+ $this->actions = '*';
+ }
+ else {
+ $this->actions = array();
+ foreach ((array) $ruleSpec['actions'] as $action) {
+ $this->actions[] = Request::normalizeActionName($action, $ruleSpec['version']);
+ }
+ }
+
+ $this->required = $ruleSpec['required'];
+ $this->fields = $ruleSpec['fields'];
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid() {
+ if (empty($this->version)) {
+ return FALSE;
+ }
+ if (empty($this->entity)) {
+ return FALSE;
+ }
+ if (!is_array($this->actions) && $this->actions !== '*') {
+ return FALSE;
+ }
+ if (!is_array($this->fields) && $this->fields !== '*') {
+ return FALSE;
+ }
+ if (!is_array($this->required)) {
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * @param array $apiRequest
+ * Parsed API request.
+ * @return string|TRUE
+ * If match, return TRUE. Otherwise, return a string with an error code.
+ */
+ public function matches($apiRequest) {
+ if (!$this->isValid()) {
+ return 'invalid';
+ }
+
+ if ($this->version != $apiRequest['version']) {
+ return 'version';
+ }
+ if ($this->entity !== '*' && $this->entity !== $apiRequest['entity']) {
+ return 'entity';
+ }
+ if ($this->actions !== '*' && !in_array($apiRequest['action'], $this->actions)) {
+ return 'action';
+ }
+
+ // These params *must* be included for the API request to proceed.
+ foreach ($this->required as $param => $value) {
+ if (!isset($apiRequest['params'][$param])) {
+ return 'required-missing-' . $param;
+ }
+ if ($value !== '*' && $apiRequest['params'][$param] != $value) {
+ return 'required-wrong-' . $param;
+ }
+ }
+
+ // These params *may* be included at the caller's discretion
+ if ($this->fields !== '*') {
+ $activatedFields = array_keys($apiRequest['params']);
+ $activatedFields = preg_grep('/^api\./', $activatedFields, PREG_GREP_INVERT);
+ if ($apiRequest['action'] == 'get') {
+ // Kind'a silly we need to (re(re))parse here for each rule; would be more
+ // performant if pre-parsed by Request::create().
+ $options = _civicrm_api3_get_options_from_params($apiRequest['params'], TRUE, $apiRequest['entity'], 'get');
+ $return = \CRM_Utils_Array::value('return', $options, array());
+ $activatedFields = array_merge($activatedFields, array_keys($return));
+ }
+
+ $unknowns = array_diff(
+ $activatedFields,
+ array_keys($this->required),
+ $this->fields,
+ self::$IGNORE_FIELDS
+ );
+
+ if (!empty($unknowns)) {
+ return 'unknown-' . implode(',', $unknowns);
+ }
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Ensure that the return values comply with the whitelist's
+ * "fields" policy.
+ *
+ * Most API's follow a convention where the result includes
+ * a 'values' array (which in turn is a list of records). Unfortunately,
+ * some don't. If the API result doesn't meet our expectation,
+ * then we probably don't know what's going on, so we abort the
+ * request.
+ *
+ * This will probably break some of the layered-sugar APIs (like
+ * getsingle, getvalue). Just use the meat-and-potatoes API instead.
+ * Or craft a suitably targeted patch.
+ *
+ * @param array $apiRequest
+ * API request.
+ * @param array $apiResult
+ * API result.
+ * @return array
+ * Modified API result.
+ * @throws \API_Exception
+ */
+ public function filter($apiRequest, $apiResult) {
+ if ($this->fields === '*') {
+ return $apiResult;
+ }
+ if (isset($apiResult['values']) && empty($apiResult['values'])) {
+ // No data; filtering doesn't matter.
+ return $apiResult;
+ }
+ if (is_array($apiResult['values'])) {
+ $firstRow = \CRM_Utils_Array::first($apiResult['values']);
+ if (is_array($firstRow)) {
+ $fields = $this->filterFields(array_keys($firstRow));
+ $apiResult['values'] = \CRM_Utils_Array::filterColumns($apiResult['values'], $fields);
+ return $apiResult;
+ }
+ }
+ throw new \API_Exception(sprintf('Filtering failed for %s.%s. Unrecognized result format.', $apiRequest['entity'], $apiRequest['action']));
+ }
+
+ /**
+ * Determine which elements in $keys are acceptable under
+ * the whitelist policy.
+ *
+ * @param array $keys
+ * List of possible keys.
+ * @return array
+ * List of acceptable keys.
+ */
+ protected function filterFields($keys) {
+ $r = array();
+ foreach ($keys as $key) {
+ if (in_array($key, $this->fields)) {
+ $r[] = $key;
+ }
+ elseif (preg_match('/^api\./', $key)) {
+ $r[] = $key;
+ }
+ }
+ return $r;
+ }
+
+}
--- /dev/null
+<?php
+namespace Civi\API\Subscriber;
+
+use Civi\API\Kernel;
+use Civi\API\WhitelistRule;
+use Civi\Core\Container;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+
+require_once 'CiviTest/CiviUnitTestCase.php';
+
+/**
+ * The WhitelistSubscriber enforces security policies
+ * based on API whitelists. This test combines a number
+ * of different policies with different requests and
+ * determines if the policies are correctly enforced.
+ *
+ * Testing breaks down into a few major elements:
+ * - A pair of hypothetical API entities, "Widget"
+ * and "Sprocket".
+ * - A library of possible Widget and Sprocket API
+ * calls (and their expected results).
+ * - A library of possible whitelist rules.
+ * - A list of test cases which attempt to execute
+ * each API call while applying different
+ * whitelist rules.
+ *
+ */
+class WhitelistSubscriberTest extends \CiviUnitTestCase {
+
+ protected function getFixtures() {
+ $recs = array();
+
+ $recs['widget'] = array(
+ 1 => array(
+ 'id' => 1,
+ 'widget_type' => 'foo',
+ 'provider' => 'george jetson',
+ 'title' => 'first widget',
+ 'comments' => 'this widget is the bomb',
+ ),
+ 2 => array(
+ 'id' => 2,
+ 'widget_type' => 'bar',
+ 'provider' => 'george jetson',
+ 'title' => 'second widget',
+ 'comments' => 'this widget is a bomb',
+ ),
+ 3 => array(
+ 'id' => 3,
+ 'widget_type' => 'foo',
+ 'provider' => 'cosmo spacely',
+ 'title' => 'third widget',
+ 'comments' => 'omg, that thing is a bomb! widgets are bombs! get out!',
+ ),
+ 8 => array(
+ 'id' => 8,
+ 'widget_type' => 'bax',
+ 'provider' => 'cosmo spacely',
+ 'title' => 'fourth widget',
+ 'comments' => 'todo: rebuild garage',
+ ),
+ );
+
+ $recs['sprocket'] = array(
+ 1 => array(
+ 'id' => 1,
+ 'sprocket_type' => 'whiz',
+ 'provider' => 'cosmo spacely',
+ 'title' => 'first sprocket',
+ 'comment' => 'this sprocket is so good i could eat it up',
+ 'widget_id' => 2,
+ ),
+ 5 => array(
+ 'id' => 5,
+ 'sprocket_type' => 'bang',
+ 'provider' => 'george jetson',
+ 'title' => 'second sprocket',
+ 'comment' => 'this green sprocket was made by soylent',
+ 'widget_id' => 2,
+ ),
+ 7 => array(
+ 'id' => 7,
+ 'sprocket_type' => 'quux',
+ 'provider' => 'cosmo spacely',
+ 'title' => 'third sprocket',
+ 'comment' => 'sprocket green is people! sprocket green is people!',
+ 'widget_id' => 3,
+ ),
+ 8 => array(
+ 'id' => 8,
+ 'sprocket_type' => 'baz',
+ 'provider' => 'george jetson',
+ 'title' => 'fourth sprocket',
+ 'comment' => 'see also: cooking.com/hannibal/1981420-sprocket-fava',
+ 'widget_id' => 3,
+ ),
+ );
+
+ return $recs;
+ }
+
+ public function restrictionCases() {
+ $calls = $rules = array();
+ $recs = $this->getFixtures();
+
+ $calls['Widget.get-all'] = array(
+ 'entity' => 'Widget',
+ 'action' => 'get',
+ 'params' => array('version' => 3),
+ 'expectedResults' => $recs['widget'],
+ );
+ $calls['Widget.get-foo'] = array(
+ 'entity' => 'Widget',
+ 'action' => 'get',
+ 'params' => array('version' => 3, 'widget_type' => 'foo'),
+ 'expectedResults' => array(1 => $recs['widget'][1], 3 => $recs['widget'][3]),
+ );
+ $calls['Widget.get-spacely'] = array(
+ 'entity' => 'Widget',
+ 'action' => 'get',
+ 'params' => array('version' => 3, 'provider' => 'cosmo spacely'),
+ 'expectedResults' => array(3 => $recs['widget'][3], 8 => $recs['widget'][8]),
+ );
+ $calls['Widget.get-spacely=>title'] = array(
+ 'entity' => 'Widget',
+ 'action' => 'get',
+ 'params' => array('version' => 3, 'provider' => 'cosmo spacely', 'return' => array('title')),
+ 'expectedResults' => array(
+ 3 => array('id' => 3, 'title' => 'third widget'),
+ 8 => array('id' => 8, 'title' => 'fourth widget'),
+ ),
+ );
+ $calls['Widget.get-spacely-foo'] = array(
+ 'entity' => 'Widget',
+ 'action' => 'get',
+ 'params' => array('version' => 3, 'provider' => 'cosmo spacely', 'widget_type' => 'foo'),
+ 'expectedResults' => array(3 => $recs['widget'][3]),
+ );
+ $calls['Sprocket.get-all'] = array(
+ 'entity' => 'Sprocket',
+ 'action' => 'get',
+ 'params' => array('version' => 3),
+ 'expectedResults' => $recs['sprocket'],
+ );
+ $calls['Widget.get-bar=>title + Sprocket.get=>provider'] = array(
+ 'entity' => 'Widget',
+ 'action' => 'get',
+ 'params' => array(
+ 'version' => 3,
+ 'widget_type' => 'bar',
+ 'return' => array('title'),
+ 'api.Sprocket.get' => array(
+ 'widget_id' => '$value.id',
+ 'return' => array('provider'),
+ ),
+ ),
+ 'expectedResults' => array(
+ 2 => array(
+ 'id' => 2,
+ 'title' => 'second widget',
+ 'api.Sprocket.get' => array(
+ 'count' => 2,
+ 'version' => 3,
+ 'values' => array(
+ 0 => array('id' => 1, 'provider' => 'cosmo spacely'),
+ 1 => array('id' => 5, 'provider' => 'george jetson'),
+ ),
+ // This is silly:
+ 'undefined_fields' => array('entity_id', 'entity_table', 'widget_id', 'api.has_parent'),
+ ),
+ ),
+ ),
+ );
+
+ $rules['*.*'] = array(
+ 'version' => 3,
+ 'entity' => '*',
+ 'actions' => '*',
+ 'required' => array(),
+ 'fields' => '*',
+ );
+ $rules['Widget.*'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => '*',
+ 'required' => array(),
+ 'fields' => '*',
+ );
+ $rules['Sprocket.*'] = array(
+ 'version' => 3,
+ 'entity' => 'Sprocket',
+ 'actions' => '*',
+ 'required' => array(),
+ 'fields' => '*',
+ );
+ $rules['Widget.get'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'get',
+ 'required' => array(),
+ 'fields' => '*',
+ );
+ $rules['Sprocket.get'] = array(
+ 'version' => 3,
+ 'entity' => 'Sprocket',
+ 'actions' => 'get',
+ 'required' => array(),
+ 'fields' => '*',
+ );
+ $rules['Sprocket.get=>title,misc'] = array(
+ 'version' => 3,
+ 'entity' => 'Sprocket',
+ 'actions' => 'get',
+ 'required' => array(),
+ // To call api.Sprocket.get via chaining, you must accept superfluous fields.
+ // It would be a mistake for the whitelist mechanism to approve these
+ // automatically, so instead we have to enumerate them. Ideally, ChainSubscriber
+ // wouldn't generate superfluous fields.
+ 'fields' => array('id', 'title', 'widget_id', 'entity_id', 'entity_table'),
+ );
+ $rules['Sprocket.get=>provider,misc'] = array(
+ 'version' => 3,
+ 'entity' => 'Sprocket',
+ 'actions' => 'get',
+ 'required' => array(),
+ // To call api.Sprocket.get via chaining, you must accept superfluous fields.
+ // It would be a mistake for the whitelist mechanism to approve these
+ // automatically, so instead we have to enumerate them. Ideally, ChainSubscriber
+ // wouldn't generate superfluous fields.
+ 'fields' => array('id', 'provider', 'widget_id', 'entity_id', 'entity_table'),
+ );
+ $rules['Widget.get-foo'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'get',
+ 'required' => array('widget_type' => 'foo'),
+ 'fields' => '*',
+ );
+ $rules['Widget.get-spacely'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'get',
+ 'required' => array('provider' => 'cosmo spacely'),
+ 'fields' => '*',
+ );
+ $rules['Widget.get-bar=>title'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'get',
+ 'required' => array('widget_type' => 'bar'),
+ 'fields' => array('id', 'title'),
+ );
+ $rules['Widget.get-spacely=>title'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'get',
+ 'required' => array('provider' => 'cosmo spacely'),
+ 'fields' => array('id', 'title'),
+ );
+ $rules['Widget.get-spacely=>widget_type'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'get',
+ 'required' => array('provider' => 'cosmo spacely'),
+ 'fields' => array('id', 'widget_type'),
+ );
+ $rules['Widget.getcreate'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => array('get', 'create'),
+ 'required' => array(),
+ 'fields' => '*',
+ );
+ $rules['Widget.create'] = array(
+ 'version' => 3,
+ 'entity' => 'Widget',
+ 'actions' => 'create',
+ 'required' => array(),
+ 'fields' => '*',
+ );
+
+ $c = array();
+
+ $c[] = array($calls['Widget.get-all'], array($rules['*.*']), TRUE);
+ $c[] = array($calls['Widget.get-all'], array($rules['Widget.*']), TRUE);
+ $c[] = array($calls['Widget.get-all'], array($rules['Widget.get']), TRUE);
+ $c[] = array($calls['Widget.get-all'], array($rules['Widget.create']), FALSE);
+ $c[] = array($calls['Widget.get-all'], array($rules['Widget.getcreate']), TRUE);
+ $c[] = array($calls['Widget.get-all'], array($rules['Sprocket.*']), FALSE);
+
+ $c[] = array($calls['Sprocket.get-all'], array($rules['*.*']), TRUE);
+ $c[] = array($calls['Sprocket.get-all'], array($rules['Sprocket.*']), TRUE);
+ $c[] = array($calls['Sprocket.get-all'], array($rules['Widget.*']), FALSE);
+ $c[] = array($calls['Sprocket.get-all'], array($rules['Widget.get']), FALSE);
+
+ $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.*']), TRUE);
+ $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.get-spacely']), TRUE);
+ $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.get-foo']), FALSE);
+ $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.get-foo'], $rules['Sprocket.*']), FALSE);
+ $c[] = array(
+ // we do a broad get, but 'fields' filtering kicks in and restricts the results
+ array_merge($calls['Widget.get-spacely'], array(
+ 'expectedResults' => $calls['Widget.get-spacely=>title']['expectedResults'],
+ )),
+ array($rules['Widget.get-spacely=>title']),
+ TRUE,
+ );
+
+ $c[] = array($calls['Widget.get-foo'], array($rules['Widget.*']), TRUE);
+ $c[] = array($calls['Widget.get-foo'], array($rules['Widget.get-foo']), TRUE);
+ $c[] = array($calls['Widget.get-foo'], array($rules['Widget.get-spacely']), FALSE);
+
+ $c[] = array($calls['Widget.get-spacely=>title'], array($rules['*.*']), TRUE);
+ $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.*']), TRUE);
+ $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.get-spacely']), TRUE);
+ $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.get-spacely=>title']), TRUE);
+
+ // We request returning title field, but the rule doesn't allow title to be returned.
+ // Need it to fail so that control could pass to another rule which does allow it.
+ $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.get-spacely=>widget_type']), FALSE);
+
+ // One rule would allow, one would be irrelevant. The order of the two rules shouldn't matter.
+ $c[] = array(
+ $calls['Widget.get-spacely=>title'],
+ array($rules['Widget.get-spacely=>widget_type'], $rules['Widget.get-spacely=>title']),
+ TRUE,
+ );
+ $c[] = array(
+ $calls['Widget.get-spacely=>title'],
+ array($rules['Widget.get-spacely=>title'], $rules['Widget.get-spacely=>widget_type']),
+ TRUE,
+ );
+
+ $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['*.*']), TRUE);
+ $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get-bar=>title'], $rules['Sprocket.get']), TRUE);
+ $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get'], $rules['Sprocket.get=>title,misc']), FALSE);
+ $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get'], $rules['Sprocket.get=>provider,misc']), TRUE);
+ $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get-foo'], $rules['Sprocket.get']), FALSE);
+ $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get']), FALSE);
+
+ return $c;
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * @param array $apiRequest
+ * Array(entity=>$,action=>$,params=>$,expectedResults=>$).
+ * @param array $rules
+ * Whitelist - list of allowed API calls/patterns.
+ * @param bool $expectSuccess
+ * TRUE if the call should succeed.
+ * Success implies that the 'expectedResults' are returned.
+ * Failure implies that the standard error message is returned.
+ * @dataProvider restrictionCases
+ */
+ public function testEach($apiRequest, $rules, $expectSuccess) {
+ \CRM_Core_DAO_AllCoreTables::init(TRUE);
+
+ $recs = $this->getFixtures();
+
+ \CRM_Core_DAO_AllCoreTables::registerEntityType('Widget', 'CRM_Fake_DAO_Widget', 'fake_widget');
+ $widgetProvider = new \Civi\API\Provider\StaticProvider(3, 'Widget',
+ array('id', 'widget_type', 'provider', 'title'),
+ array(),
+ $recs['widget']
+ );
+
+ \CRM_Core_DAO_AllCoreTables::registerEntityType('Sprocket', 'CRM_Fake_DAO_Sprocket', 'fake_sprocket');
+ $sprocketProvider = new \Civi\API\Provider\StaticProvider(
+ 3,
+ 'Sprocket',
+ array('id', 'sprocket_type', 'widget_id', 'provider', 'title', 'comment'),
+ array(),
+ $recs['sprocket']
+ );
+
+ $whitelist = WhitelistRule::createAll($rules);
+
+ $dispatcher = new EventDispatcher();
+ $kernel = new Kernel($dispatcher);
+ $kernel->registerApiProvider($sprocketProvider);
+ $kernel->registerApiProvider($widgetProvider);
+ $dispatcher->addSubscriber(new WhitelistSubscriber($whitelist));
+ $dispatcher->addSubscriber(new ChainSubscriber());
+
+ $apiRequest['params']['debug'] = 1;
+ $apiRequest['params']['check_permissions'] = 'whitelist';
+ $result = $kernel->run($apiRequest['entity'], $apiRequest['action'], $apiRequest['params']);
+
+ if ($expectSuccess) {
+ $this->assertAPISuccess($result);
+ $this->assertTrue(is_array($apiRequest['expectedResults']));
+ $this->assertTreeEquals($apiRequest['expectedResults'], $result['values']);
+ }
+ else {
+ $this->assertAPIFailure($result);
+ $this->assertRegExp('/The request does not match any active API authorizations./', $result['error_message']);
+ }
+ }
+
+}