commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-old / civicrm / tests / phpunit / Civi / API / Subscriber / WhitelistSubscriberTest.php
1 <?php
2 namespace Civi\API\Subscriber;
3
4 use Civi\API\Kernel;
5 use Civi\API\WhitelistRule;
6 use Civi\Core\Container;
7 use Symfony\Component\EventDispatcher\EventDispatcher;
8
9 require_once 'CiviTest/CiviUnitTestCase.php';
10
11 /**
12 * The WhitelistSubscriber enforces security policies
13 * based on API whitelists. This test combines a number
14 * of different policies with different requests and
15 * determines if the policies are correctly enforced.
16 *
17 * Testing breaks down into a few major elements:
18 * - A pair of hypothetical API entities, "Widget"
19 * and "Sprocket".
20 * - A library of possible Widget and Sprocket API
21 * calls (and their expected results).
22 * - A library of possible whitelist rules.
23 * - A list of test cases which attempt to execute
24 * each API call while applying different
25 * whitelist rules.
26 *
27 */
28 class WhitelistSubscriberTest extends \CiviUnitTestCase {
29
30 protected function getFixtures() {
31 $recs = array();
32
33 $recs['widget'] = array(
34 1 => array(
35 'id' => 1,
36 'widget_type' => 'foo',
37 'provider' => 'george jetson',
38 'title' => 'first widget',
39 'comments' => 'this widget is the bomb',
40 ),
41 2 => array(
42 'id' => 2,
43 'widget_type' => 'bar',
44 'provider' => 'george jetson',
45 'title' => 'second widget',
46 'comments' => 'this widget is a bomb',
47 ),
48 3 => array(
49 'id' => 3,
50 'widget_type' => 'foo',
51 'provider' => 'cosmo spacely',
52 'title' => 'third widget',
53 'comments' => 'omg, that thing is a bomb! widgets are bombs! get out!',
54 ),
55 8 => array(
56 'id' => 8,
57 'widget_type' => 'bax',
58 'provider' => 'cosmo spacely',
59 'title' => 'fourth widget',
60 'comments' => 'todo: rebuild garage',
61 ),
62 );
63
64 $recs['sprocket'] = array(
65 1 => array(
66 'id' => 1,
67 'sprocket_type' => 'whiz',
68 'provider' => 'cosmo spacely',
69 'title' => 'first sprocket',
70 'comment' => 'this sprocket is so good i could eat it up',
71 'widget_id' => 2,
72 ),
73 5 => array(
74 'id' => 5,
75 'sprocket_type' => 'bang',
76 'provider' => 'george jetson',
77 'title' => 'second sprocket',
78 'comment' => 'this green sprocket was made by soylent',
79 'widget_id' => 2,
80 ),
81 7 => array(
82 'id' => 7,
83 'sprocket_type' => 'quux',
84 'provider' => 'cosmo spacely',
85 'title' => 'third sprocket',
86 'comment' => 'sprocket green is people! sprocket green is people!',
87 'widget_id' => 3,
88 ),
89 8 => array(
90 'id' => 8,
91 'sprocket_type' => 'baz',
92 'provider' => 'george jetson',
93 'title' => 'fourth sprocket',
94 'comment' => 'see also: cooking.com/hannibal/1981420-sprocket-fava',
95 'widget_id' => 3,
96 ),
97 );
98
99 return $recs;
100 }
101
102 public function restrictionCases() {
103 $calls = $rules = array();
104 $recs = $this->getFixtures();
105
106 $calls['Widget.get-all'] = array(
107 'entity' => 'Widget',
108 'action' => 'get',
109 'params' => array('version' => 3),
110 'expectedResults' => $recs['widget'],
111 );
112 $calls['Widget.get-foo'] = array(
113 'entity' => 'Widget',
114 'action' => 'get',
115 'params' => array('version' => 3, 'widget_type' => 'foo'),
116 'expectedResults' => array(1 => $recs['widget'][1], 3 => $recs['widget'][3]),
117 );
118 $calls['Widget.get-spacely'] = array(
119 'entity' => 'Widget',
120 'action' => 'get',
121 'params' => array('version' => 3, 'provider' => 'cosmo spacely'),
122 'expectedResults' => array(3 => $recs['widget'][3], 8 => $recs['widget'][8]),
123 );
124 $calls['Widget.get-spacely=>title'] = array(
125 'entity' => 'Widget',
126 'action' => 'get',
127 'params' => array('version' => 3, 'provider' => 'cosmo spacely', 'return' => array('title')),
128 'expectedResults' => array(
129 3 => array('id' => 3, 'title' => 'third widget'),
130 8 => array('id' => 8, 'title' => 'fourth widget'),
131 ),
132 );
133 $calls['Widget.get-spacely-foo'] = array(
134 'entity' => 'Widget',
135 'action' => 'get',
136 'params' => array('version' => 3, 'provider' => 'cosmo spacely', 'widget_type' => 'foo'),
137 'expectedResults' => array(3 => $recs['widget'][3]),
138 );
139 $calls['Sprocket.get-all'] = array(
140 'entity' => 'Sprocket',
141 'action' => 'get',
142 'params' => array('version' => 3),
143 'expectedResults' => $recs['sprocket'],
144 );
145 $calls['Widget.get-bar=>title + Sprocket.get=>provider'] = array(
146 'entity' => 'Widget',
147 'action' => 'get',
148 'params' => array(
149 'version' => 3,
150 'widget_type' => 'bar',
151 'return' => array('title'),
152 'api.Sprocket.get' => array(
153 'widget_id' => '$value.id',
154 'return' => array('provider'),
155 ),
156 ),
157 'expectedResults' => array(
158 2 => array(
159 'id' => 2,
160 'title' => 'second widget',
161 'api.Sprocket.get' => array(
162 'count' => 2,
163 'version' => 3,
164 'values' => array(
165 0 => array('id' => 1, 'provider' => 'cosmo spacely'),
166 1 => array('id' => 5, 'provider' => 'george jetson'),
167 ),
168 // This is silly:
169 'undefined_fields' => array('entity_id', 'entity_table', 'widget_id', 'api.has_parent'),
170 ),
171 ),
172 ),
173 );
174
175 $rules['*.*'] = array(
176 'version' => 3,
177 'entity' => '*',
178 'actions' => '*',
179 'required' => array(),
180 'fields' => '*',
181 );
182 $rules['Widget.*'] = array(
183 'version' => 3,
184 'entity' => 'Widget',
185 'actions' => '*',
186 'required' => array(),
187 'fields' => '*',
188 );
189 $rules['Sprocket.*'] = array(
190 'version' => 3,
191 'entity' => 'Sprocket',
192 'actions' => '*',
193 'required' => array(),
194 'fields' => '*',
195 );
196 $rules['Widget.get'] = array(
197 'version' => 3,
198 'entity' => 'Widget',
199 'actions' => 'get',
200 'required' => array(),
201 'fields' => '*',
202 );
203 $rules['Sprocket.get'] = array(
204 'version' => 3,
205 'entity' => 'Sprocket',
206 'actions' => 'get',
207 'required' => array(),
208 'fields' => '*',
209 );
210 $rules['Sprocket.get=>title,misc'] = array(
211 'version' => 3,
212 'entity' => 'Sprocket',
213 'actions' => 'get',
214 'required' => array(),
215 // To call api.Sprocket.get via chaining, you must accept superfluous fields.
216 // It would be a mistake for the whitelist mechanism to approve these
217 // automatically, so instead we have to enumerate them. Ideally, ChainSubscriber
218 // wouldn't generate superfluous fields.
219 'fields' => array('id', 'title', 'widget_id', 'entity_id', 'entity_table'),
220 );
221 $rules['Sprocket.get=>provider,misc'] = array(
222 'version' => 3,
223 'entity' => 'Sprocket',
224 'actions' => 'get',
225 'required' => array(),
226 // To call api.Sprocket.get via chaining, you must accept superfluous fields.
227 // It would be a mistake for the whitelist mechanism to approve these
228 // automatically, so instead we have to enumerate them. Ideally, ChainSubscriber
229 // wouldn't generate superfluous fields.
230 'fields' => array('id', 'provider', 'widget_id', 'entity_id', 'entity_table'),
231 );
232 $rules['Widget.get-foo'] = array(
233 'version' => 3,
234 'entity' => 'Widget',
235 'actions' => 'get',
236 'required' => array('widget_type' => 'foo'),
237 'fields' => '*',
238 );
239 $rules['Widget.get-spacely'] = array(
240 'version' => 3,
241 'entity' => 'Widget',
242 'actions' => 'get',
243 'required' => array('provider' => 'cosmo spacely'),
244 'fields' => '*',
245 );
246 $rules['Widget.get-bar=>title'] = array(
247 'version' => 3,
248 'entity' => 'Widget',
249 'actions' => 'get',
250 'required' => array('widget_type' => 'bar'),
251 'fields' => array('id', 'title'),
252 );
253 $rules['Widget.get-spacely=>title'] = array(
254 'version' => 3,
255 'entity' => 'Widget',
256 'actions' => 'get',
257 'required' => array('provider' => 'cosmo spacely'),
258 'fields' => array('id', 'title'),
259 );
260 $rules['Widget.get-spacely=>widget_type'] = array(
261 'version' => 3,
262 'entity' => 'Widget',
263 'actions' => 'get',
264 'required' => array('provider' => 'cosmo spacely'),
265 'fields' => array('id', 'widget_type'),
266 );
267 $rules['Widget.getcreate'] = array(
268 'version' => 3,
269 'entity' => 'Widget',
270 'actions' => array('get', 'create'),
271 'required' => array(),
272 'fields' => '*',
273 );
274 $rules['Widget.create'] = array(
275 'version' => 3,
276 'entity' => 'Widget',
277 'actions' => 'create',
278 'required' => array(),
279 'fields' => '*',
280 );
281
282 $c = array();
283
284 $c[] = array($calls['Widget.get-all'], array($rules['*.*']), TRUE);
285 $c[] = array($calls['Widget.get-all'], array($rules['Widget.*']), TRUE);
286 $c[] = array($calls['Widget.get-all'], array($rules['Widget.get']), TRUE);
287 $c[] = array($calls['Widget.get-all'], array($rules['Widget.create']), FALSE);
288 $c[] = array($calls['Widget.get-all'], array($rules['Widget.getcreate']), TRUE);
289 $c[] = array($calls['Widget.get-all'], array($rules['Sprocket.*']), FALSE);
290
291 $c[] = array($calls['Sprocket.get-all'], array($rules['*.*']), TRUE);
292 $c[] = array($calls['Sprocket.get-all'], array($rules['Sprocket.*']), TRUE);
293 $c[] = array($calls['Sprocket.get-all'], array($rules['Widget.*']), FALSE);
294 $c[] = array($calls['Sprocket.get-all'], array($rules['Widget.get']), FALSE);
295
296 $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.*']), TRUE);
297 $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.get-spacely']), TRUE);
298 $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.get-foo']), FALSE);
299 $c[] = array($calls['Widget.get-spacely'], array($rules['Widget.get-foo'], $rules['Sprocket.*']), FALSE);
300 $c[] = array(
301 // we do a broad get, but 'fields' filtering kicks in and restricts the results
302 array_merge($calls['Widget.get-spacely'], array(
303 'expectedResults' => $calls['Widget.get-spacely=>title']['expectedResults'],
304 )),
305 array($rules['Widget.get-spacely=>title']),
306 TRUE,
307 );
308
309 $c[] = array($calls['Widget.get-foo'], array($rules['Widget.*']), TRUE);
310 $c[] = array($calls['Widget.get-foo'], array($rules['Widget.get-foo']), TRUE);
311 $c[] = array($calls['Widget.get-foo'], array($rules['Widget.get-spacely']), FALSE);
312
313 $c[] = array($calls['Widget.get-spacely=>title'], array($rules['*.*']), TRUE);
314 $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.*']), TRUE);
315 $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.get-spacely']), TRUE);
316 $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.get-spacely=>title']), TRUE);
317
318 // We request returning title field, but the rule doesn't allow title to be returned.
319 // Need it to fail so that control could pass to another rule which does allow it.
320 $c[] = array($calls['Widget.get-spacely=>title'], array($rules['Widget.get-spacely=>widget_type']), FALSE);
321
322 // One rule would allow, one would be irrelevant. The order of the two rules shouldn't matter.
323 $c[] = array(
324 $calls['Widget.get-spacely=>title'],
325 array($rules['Widget.get-spacely=>widget_type'], $rules['Widget.get-spacely=>title']),
326 TRUE,
327 );
328 $c[] = array(
329 $calls['Widget.get-spacely=>title'],
330 array($rules['Widget.get-spacely=>title'], $rules['Widget.get-spacely=>widget_type']),
331 TRUE,
332 );
333
334 $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['*.*']), TRUE);
335 $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get-bar=>title'], $rules['Sprocket.get']), TRUE);
336 $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get'], $rules['Sprocket.get=>title,misc']), FALSE);
337 $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get'], $rules['Sprocket.get=>provider,misc']), TRUE);
338 $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get-foo'], $rules['Sprocket.get']), FALSE);
339 $c[] = array($calls['Widget.get-bar=>title + Sprocket.get=>provider'], array($rules['Widget.get']), FALSE);
340
341 return $c;
342 }
343
344 protected function setUp() {
345 parent::setUp();
346 }
347
348 /**
349 * @param array $apiRequest
350 * Array(entity=>$,action=>$,params=>$,expectedResults=>$).
351 * @param array $rules
352 * Whitelist - list of allowed API calls/patterns.
353 * @param bool $expectSuccess
354 * TRUE if the call should succeed.
355 * Success implies that the 'expectedResults' are returned.
356 * Failure implies that the standard error message is returned.
357 * @dataProvider restrictionCases
358 */
359 public function testEach($apiRequest, $rules, $expectSuccess) {
360 \CRM_Core_DAO_AllCoreTables::init(TRUE);
361
362 $recs = $this->getFixtures();
363
364 \CRM_Core_DAO_AllCoreTables::registerEntityType('Widget', 'CRM_Fake_DAO_Widget', 'fake_widget');
365 $widgetProvider = new \Civi\API\Provider\StaticProvider(3, 'Widget',
366 array('id', 'widget_type', 'provider', 'title'),
367 array(),
368 $recs['widget']
369 );
370
371 \CRM_Core_DAO_AllCoreTables::registerEntityType('Sprocket', 'CRM_Fake_DAO_Sprocket', 'fake_sprocket');
372 $sprocketProvider = new \Civi\API\Provider\StaticProvider(
373 3,
374 'Sprocket',
375 array('id', 'sprocket_type', 'widget_id', 'provider', 'title', 'comment'),
376 array(),
377 $recs['sprocket']
378 );
379
380 $whitelist = WhitelistRule::createAll($rules);
381
382 $dispatcher = new EventDispatcher();
383 $kernel = new Kernel($dispatcher);
384 $kernel->registerApiProvider($sprocketProvider);
385 $kernel->registerApiProvider($widgetProvider);
386 $dispatcher->addSubscriber(new WhitelistSubscriber($whitelist));
387 $dispatcher->addSubscriber(new ChainSubscriber());
388
389 $apiRequest['params']['debug'] = 1;
390 $apiRequest['params']['check_permissions'] = 'whitelist';
391 $result = $kernel->run($apiRequest['entity'], $apiRequest['action'], $apiRequest['params']);
392
393 if ($expectSuccess) {
394 $this->assertAPISuccess($result);
395 $this->assertTrue(is_array($apiRequest['expectedResults']));
396 $this->assertTreeEquals($apiRequest['expectedResults'], $result['values']);
397 }
398 else {
399 $this->assertAPIFailure($result);
400 $this->assertRegExp('/The request does not match any active API authorizations./', $result['error_message']);
401 }
402 }
403
404 }