1 <?php
2 /**
3 * Rights authorizer component class file.
4 *
5 * @author Christoffer Niska <cniska@live.com>
6 * @copyright Copyright © 2010 Christoffer Niska
7 * @since 0.5
8 */
9 class RAuthorizer extends CApplicationComponent
10 {
11 /**
12 * @property string the name of the superuser role.
13 */
14 public $superuserName;
15 /**
16 * @property RDbAuthManager the authorization manager.
17 */
18 private $_authManager;
19
20 /**
21 * Initializes the authorizer.
22 */
23 public function init()
24 {
25 parent::init();
26
27 $this->_authManager = Yii::app()->getAuthManager();
28 }
29
30 /**
31 * Returns the a list of all roles.
32 * @param boolean $includeSuperuser whether to include the superuser.
33 * @param boolean $sort whether to sort the items by their weights.
34 * @return the roles.
35 */
36 public function getRoles($includeSuperuser=true, $sort=true)
37 {
38 $exclude = $includeSuperuser===false ? array($this->superuserName) : array();
39 $roles = $this->getAuthItems(CAuthItem::TYPE_ROLE, null, null, $sort, $exclude);
40 $roles = $this->attachAuthItemBehavior($roles);
41 return $roles;
42 }
43
44 /**
45 * Creates an authorization item.
46 * @param string $name the item name. This must be a unique identifier.
47 * @param integer $type the item type (0: operation, 1: task, 2: role).
48 * @param string $description the description for the item.
49 * @param string $bizRule business rule associated with the item. This is a piece of
50 * PHP code that will be executed when {@link checkAccess} is called for the item.
51 * @param mixed $data additional data associated with the item.
52 * @return CAuthItem the authorization item
53 */
54 public function createAuthItem($name, $type, $description='', $bizRule=null, $data=null)
55 {
56 $bizRule = $bizRule!=='' ? $bizRule : null;
57
58 if( $data!==null )
59 $data = $data!=='' ? $this->sanitizeExpression($data.';') : null;
60
61 return $this->_authManager->createAuthItem($name, $type, $description, $bizRule, $data);
62 }
63
64 /**
65 * Updates an authorization item.
66 * @param string $oldName the item name. This must be a unique identifier.
67 * @param integer $name the item type (0: operation, 1: task, 2: role).
68 * @param string $description the description for the item.
69 * @param string $bizRule business rule associated with the item. This is a piece of
70 * PHP code that will be executed when {@link checkAccess} is called for the item.
71 * @param mixed $data additional data associated with the item.
72 */
73 public function updateAuthItem($oldName, $name, $description='', $bizRule=null, $data=null)
74 {
75 $authItem = $this->_authManager->getAuthItem($oldName);
76 $authItem->name = $name;
77 $authItem->description = $description!=='' ? $description : null;
78 $authItem->bizRule = $bizRule!=='' ? $bizRule : null;
79
80 // Make sure that data is not already serialized.
81 if( @unserialize($data)===false )
82 $authItem->data = $data!=='' ? $this->sanitizeExpression($data.';') : null;
83
84 $this->_authManager->saveAuthItem($authItem, $oldName);
85 }
86
87 /**
88 * Returns the authorization items of the specific type and user.
89 * @param mixed $types the item type (0: operation, 1: task, 2: role). Defaults to null,
90 * meaning returning all items regardless of their type.
91 * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if
92 * they are not assigned to a user.
93 * @param CAuthItem $parent the item for which to get the select options.
94 * @param boolean $sort sort items by to weights.
95 * @param array $exclude the items to be excluded.
96 * @return array the authorization items of the specific type.
97 */
98 public function getAuthItems($types=null, $userId=null, CAuthItem $parent=null, $sort=true, $exclude=array())
99 {
100 // We have none or a single type.
101 if( $types!==(array)$types )
102 {
103 $items = $this->_authManager->getAuthItems($types, $userId, $sort);
104 }
105 // We have multiple types.
106 else
107 {
108 $typeItemList = array();
109 foreach( $types as $type )
110 $typeItemList[ $type ] = $this->_authManager->getAuthItems($type, $userId, $sort);
111
112 // Merge the authorization items preserving the keys.
113 $items = array();
114 foreach( $typeItemList as $typeItems )
115 $items = $this->mergeAuthItems($items, $typeItems);
116 }
117
118 $items = $this->excludeInvalidAuthItems($items, $parent, $exclude);
119 $items = $this->attachAuthItemBehavior($items, $userId, $parent);
120
121 return $items;
122 }
123
124 /**
125 * Merges two arrays with authorization items preserving the keys.
126 * @param array $array1 the items to merge to.
127 * @param array $array2 the items to merge from.
128 * @return array the merged items.
129 */
130 protected function mergeAuthItems($array1, $array2)
131 {
132 foreach( $array2 as $itemName=>$item )
133 if( isset($array1[ $itemName ])===false )
134 $array1[ $itemName ] = $item;
135
136 return $array1;
137 }
138
139 /**
140 * Excludes invalid authorization items.
141 * When an item is provided its parents and children are excluded aswell.
142 * @param array $items the authorization items to process.
143 * @param CAuthItem $parent the item to check valid authorization items for.
144 * @param array $exclude additional items to be excluded.
145 * @return array valid authorization items.
146 */
147 protected function excludeInvalidAuthItems($items, CAuthItem $parent=null, $exclude=array())
148 {
149 // We are getting authorization items valid for a certain item
150 // exclude its parents and children aswell.
151 if( $parent!==null )
152 {
153 $exclude[] = $parent->name;
154 foreach( $parent->getChildren() as $childName=>$child )
155 $exclude[] = $childName;
156
157 // Exclude the parents recursively to avoid inheritance loops.
158 $parentNames = array_keys($this->getAuthItemParents($parent->name));
159 $exclude = array_merge($parentNames, $exclude);
160 }
161
162 // Unset the items that are supposed to be excluded.
163 foreach( $exclude as $itemName )
164 if( isset($items[ $itemName ]) )
165 unset($items[ $itemName ]);
166
167 return $items;
168 }
169
170 /**
171 * Returns the parents of the specified authorization item.
172 * @param mixed $item the item name for which to get its parents.
173 * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null,
174 * meaning returning all items regardless of their type.
175 * @param string $parentName the name of the item in which permissions to search.
176 * @param boolean $direct whether we want the specified items parent or all parents.
177 * @return array the names of the parent items.
178 */
179 public function getAuthItemParents($item, $type=null, $parentName=null, $direct=false)
180 {
181 if( ($item instanceof CAuthItem)===false )
182 $item = $this->_authManager->getAuthItem($item);
183
184 $permissions = $this->getPermissions($parentName);
185 $parentNames = $this->getAuthItemParentsRecursive($item->name, $permissions, $direct);
186 $parents = $this->_authManager->getAuthItemsByNames($parentNames);
187 $parents = $this->attachAuthItemBehavior($parents, null, $item);
188
189 if( $type!==null )
190 foreach( $parents as $parentName=>$parent )
191 if( (int)$parent->type!==$type )
192 unset($parents[ $parentName ]);
193
194 return $parents;
195 }
196
197 /**
198 * Returns the parents of the specified authorization item recursively.
199 * @param string $itemName the item name for which to get its parents.
200 * @param array $items the items to process.
201 * @param boolean $direct whether we want the specified items parent or all parents.
202 * @return the names of the parents items recursively.
203 */
204 private function getAuthItemParentsRecursive($itemName, $items, $direct)
205 {
206 $parents = array();
207 foreach( $items as $childName=>$children )
208 {
209 if( $children!==array() )
210 {
211 if( isset($children[ $itemName ]) )
212 {
213 if( isset($parents[ $childName ])===false )
214 $parents[ $childName ] = $childName;
215 }
216 else
217 {
218 if( ($p = $this->getAuthItemParentsRecursive($itemName, $children, $direct))!==array() )
219 {
220 if( $direct===false && isset($parents[ $childName ])===false )
221 $parents[ $childName ] = $childName;
222
223 $parents = array_merge($parents, $p);
224 }
225 }
226 }
227 }
228
229 return $parents;
230 }
231
232 /**
233 * Returns the children for the specified authorization item recursively.
234 * @param mixed $item the item for which to get its children.
235 * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null,
236 * meaning returning all items regardless of their type.
237 * @return array the names of the item's children.
238 */
239 public function getAuthItemChildren($item, $type=null)
240 {
241 if( ($item instanceof CAuthItem)===false )
242 $item = $this->_authManager->getAuthItem($item);
243
244 $childrenNames = array();
245 foreach( $item->getChildren() as $childName=>$child )
246 if( $type===null || (int)$child->type===$type )
247 $childrenNames[] = $childName;
248
249 $children = $this->_authManager->getAuthItemsByNames($childrenNames);
250 $children = $this->attachAuthItemBehavior($children, null, $item);
251
252 return $children;
253 }
254
255 /**
256 * Attaches the rights authorization item behavior to the given item.
257 * @param mixed $items the item or items to which attach the behavior.
258 * @param int $userId the ID of the user to which the item is assigned.
259 * @param CAuthItem $parent the parent of the given item.
260 * @return mixed the item or items with the behavior attached.
261 */
262 public function attachAuthItemBehavior($items, $userId=null, CAuthItem $parent=null)
263 {
264 // We have a single item.
265 if( $items instanceof CAuthItem )
266 {
267 $items->attachBehavior('rights', new RAuthItemBehavior($userId, $parent));
268 }
269 // We have multiple items.
270 else if( $items===(array)$items )
271 {
272 foreach( $items as $item )
273 $item->attachBehavior('rights', new RAuthItemBehavior($userId, $parent));
274 }
275
276 return $items;
277 }
278
279 /**
280 * Returns the users with superuser privileges.
281 * @return the superusers.
282 */
283 public function getSuperusers()
284 {
285 $assignments = $this->_authManager->getAssignmentsByItemName( Rights::module()->superuserName );
286
287 $userIdList = array();
288 foreach( $assignments as $userId=>$assignment )
289 $userIdList[] = $userId;
290
291 $criteria = new CDbCriteria();
292 $criteria->addInCondition(Rights::module()->userIdColumn, $userIdList);
293
294 $userClass = Rights::module()->userClass;
295 $users = CActiveRecord::model($userClass)->findAll($criteria);
296 $users = $this->attachUserBehavior($users);
297
298 $superusers = array();
299 foreach( $users as $user )
300 $superusers[] = $user->name;
301
302 // Make sure that we have superusers, otherwise we would allow full access to Rights
303 // if there for some reason is not any superusers.
304 if( $superusers===array() )
305 throw new CHttpException(403, Rights::t('core', 'There must be at least one superuser!'));
306
307 return $superusers;
308 }
309
310 /**
311 * Attaches the rights user behavior to the given users.
312 * @param mixed $users the user or users to which attach the behavior.
313 * @return mixed the user or users with the behavior attached.
314 */
315 public function attachUserBehavior($users)
316 {
317 $userClass = Rights::module()->userClass;
318
319 // We have a single user.
320 if( $users instanceof $userClass )
321 {
322 $users->attachBehavior('rights', new RUserBehavior);
323 }
324 // We have multiple user.
325 else if( $users===(array)$users )
326 {
327 foreach( $users as $user )
328 $user->attachBehavior('rights', new RUserBehavior);
329 }
330
331 return $users;
332 }
333
334 /**
335 * Returns whether the user is a superuser.
336 * @param integer $userId the id of the user to do the check for.
337 * @return boolean whether the user is a superuser.
338 */
339 public function isSuperuser($userId)
340 {
341 $assignments = $this->_authManager->getAuthAssignments($userId);
342 return isset($assignments[ $this->superuserName ]);
343 }
344
345 /**
346 * Returns the permissions for a specific authorization item.
347 * @param string $itemName the name of the item for which to get permissions. Defaults to null,
348 * meaning that the full permission tree is returned.
349 * @return the permission tree.
350 */
351 public function getPermissions($itemName=null)
352 {
353 $permissions = array();
354
355 if( $itemName!==null )
356 {
357 $item = $this->_authManager->getAuthItem($itemName);
358 $permissions = $this->getPermissionsRecursive($item);
359 }
360 else
361 {
362 foreach( $this->getRoles() as $roleName=>$role )
363 $permissions[ $roleName ] = $this->getPermissionsRecursive($role);
364 }
365
366 return $permissions;
367 }
368
369 /**
370 * Returns the permissions for a specific authorization item recursively.
371 * @param CAuthItem $item the item for which to get permissions.
372 * @return array the section of the permissions tree.
373 */
374 private function getPermissionsRecursive(CAuthItem $item)
375 {
376 $permissions = array();
377 foreach( $item->getChildren() as $childName=>$child )
378 {
379 $permissions[ $childName ] = array();
380 if( ($grandChildren = $this->getPermissionsRecursive($child))!==array() )
381 $permissions[ $childName ] = $grandChildren;
382 }
383
384 return $permissions;
385 }
386
387 /**
388 * Returns the permission type for an authorization item.
389 * @param string $itemName the name of the item to check permission for.
390 * @param string $parentName the name of the item in which permissions to look.
391 * @param array $permissions the permissions.
392 * @return integer the permission type (0: None, 1: Direct, 2: Inherited).
393 */
394 public function hasPermission($itemName, $parentName=null, $permissions=array())
395 {
396 if( $parentName!==null )
397 {
398 if( $parentName===$this->superuserName )
399 return 1;
400
401 $permissions = $this->getPermissions($parentName);
402 }
403
404 if( isset($permissions[ $itemName ]) )
405 return 1;
406
407 foreach( $permissions as $children )
408 if( $children!==array() )
409 if( $this->hasPermission($itemName, null, $children)>0 )
410 return 2;
411
412 return 0;
413 }
414
415 /**
416 * Tries to sanitize code to make it safe for execution.
417 * @param string $code the code to be execute.
418 * @return mixed the return value of eval() or null if the code was unsafe to execute.
419 */
420 protected function sanitizeExpression($code)
421 {
422 // Language consturcts.
423 $languageConstructs = array(
424 'echo',
425 'empty',
426 'isset',
427 'unset',
428 'exit',
429 'die',
430 'include',
431 'include_once',
432 'require',
433 'require_once',
434 );
435
436 // Loop through the language constructs.
437 foreach( $languageConstructs as $lc )
438 if( preg_match('/'.$lc.'\ *\(?\ *[\"\']+/', $code)>0 )
439 return null; // Language construct found, not safe for eval.
440
441 // Get a list of all defined functions
442 $definedFunctions = get_defined_functions();
443 $functions = array_merge($definedFunctions['internal'], $definedFunctions['user']);
444
445 // Loop through the functions and check the code for function calls.
446 // Append a '(' to the functions to avoid confusion between e.g. array() and array_merge().
447 foreach( $functions as $f )
448 if( preg_match('/'.$f.'\ *\({1}/', $code)>0 )
449 return null; // Function call found, not safe for eval.
450
451 // Evaluate the safer code
452 $result = @eval($code);
453
454 // Return the evaluated code or null if the result was false.
455 return $result!==false ? $result : null;
456 }
457
458 /**
459 * @return RAuthManager the authorization manager.
460 */
461 public function getAuthManager()
462 {
463 return $this->_authManager;
464 }
465 }
466