1: <?php
2: /**
3: * CDbAuthManager class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CDbAuthManager represents an authorization manager that stores authorization information in database.
13: *
14: * The database connection is specified by {@link connectionID}. And the database schema
15: * should be as described in "framework/web/auth/*.sql". You may change the names of
16: * the three tables used to store the authorization data by setting {@link itemTable},
17: * {@link itemChildTable} and {@link assignmentTable}.
18: *
19: * @property array $authItems The authorization items of the specific type.
20: *
21: * @author Qiang Xue <qiang.xue@gmail.com>
22: * @package system.web.auth
23: * @since 1.0
24: */
25: class CDbAuthManager extends CAuthManager
26: {
27: /**
28: * @var string the ID of the {@link CDbConnection} application component. Defaults to 'db'.
29: * The database must have the tables as declared in "framework/web/auth/*.sql".
30: */
31: public $connectionID='db';
32: /**
33: * @var string the name of the table storing authorization items. Defaults to 'AuthItem'.
34: */
35: public $itemTable='AuthItem';
36: /**
37: * @var string the name of the table storing authorization item hierarchy. Defaults to 'AuthItemChild'.
38: */
39: public $itemChildTable='AuthItemChild';
40: /**
41: * @var string the name of the table storing authorization item assignments. Defaults to 'AuthAssignment'.
42: */
43: public $assignmentTable='AuthAssignment';
44: /**
45: * @var CDbConnection the database connection. By default, this is initialized
46: * automatically as the application component whose ID is indicated as {@link connectionID}.
47: */
48: public $db;
49:
50: private $_usingSqlite;
51:
52: /**
53: * Initializes the application component.
54: * This method overrides the parent implementation by establishing the database connection.
55: */
56: public function init()
57: {
58: parent::init();
59: $this->_usingSqlite=!strncmp($this->getDbConnection()->getDriverName(),'sqlite',6);
60: }
61:
62: /**
63: * Performs access check for the specified user.
64: * @param string $itemName the name of the operation that need access check
65: * @param mixed $userId the user ID. This should can be either an integer and a string representing
66: * the unique identifier of a user. See {@link IWebUser::getId}.
67: * @param array $params name-value pairs that would be passed to biz rules associated
68: * with the tasks and roles assigned to the user.
69: * Since version 1.1.11 a param with name 'userId' is added to this array, which holds the value of <code>$userId</code>.
70: * @return boolean whether the operations can be performed by the user.
71: */
72: public function checkAccess($itemName,$userId,$params=array())
73: {
74: $assignments=$this->getAuthAssignments($userId);
75: return $this->checkAccessRecursive($itemName,$userId,$params,$assignments);
76: }
77:
78: /**
79: * Performs access check for the specified user.
80: * This method is internally called by {@link checkAccess}.
81: * @param string $itemName the name of the operation that need access check
82: * @param mixed $userId the user ID. This should can be either an integer and a string representing
83: * the unique identifier of a user. See {@link IWebUser::getId}.
84: * @param array $params name-value pairs that would be passed to biz rules associated
85: * with the tasks and roles assigned to the user.
86: * Since version 1.1.11 a param with name 'userId' is added to this array, which holds the value of <code>$userId</code>.
87: * @param array $assignments the assignments to the specified user
88: * @return boolean whether the operations can be performed by the user.
89: * @since 1.1.3
90: */
91: protected function checkAccessRecursive($itemName,$userId,$params,$assignments)
92: {
93: if(($item=$this->getAuthItem($itemName))===null)
94: return false;
95: Yii::trace('Checking permission "'.$item->getName().'"','system.web.auth.CDbAuthManager');
96: if(!isset($params['userId']))
97: $params['userId'] = $userId;
98: if($this->executeBizRule($item->getBizRule(),$params,$item->getData()))
99: {
100: if(in_array($itemName,$this->defaultRoles))
101: return true;
102: if(isset($assignments[$itemName]))
103: {
104: $assignment=$assignments[$itemName];
105: if($this->executeBizRule($assignment->getBizRule(),$params,$assignment->getData()))
106: return true;
107: }
108: $parents=$this->db->createCommand()
109: ->select('parent')
110: ->from($this->itemChildTable)
111: ->where('child=:name', array(':name'=>$itemName))
112: ->queryColumn();
113: foreach($parents as $parent)
114: {
115: if($this->checkAccessRecursive($parent,$userId,$params,$assignments))
116: return true;
117: }
118: }
119: return false;
120: }
121:
122: /**
123: * Adds an item as a child of another item.
124: * @param string $itemName the parent item name
125: * @param string $childName the child item name
126: * @return boolean whether the item is added successfully
127: * @throws CException if either parent or child doesn't exist or if a loop has been detected.
128: */
129: public function addItemChild($itemName,$childName)
130: {
131: if($itemName===$childName)
132: throw new CException(Yii::t('yii','Cannot add "{name}" as a child of itself.',
133: array('{name}'=>$itemName)));
134:
135: $rows=$this->db->createCommand()
136: ->select()
137: ->from($this->itemTable)
138: ->where('name=:name1 OR name=:name2', array(
139: ':name1'=>$itemName,
140: ':name2'=>$childName
141: ))
142: ->queryAll();
143:
144: if(count($rows)==2)
145: {
146: if($rows[0]['name']===$itemName)
147: {
148: $parentType=$rows[0]['type'];
149: $childType=$rows[1]['type'];
150: }
151: else
152: {
153: $childType=$rows[0]['type'];
154: $parentType=$rows[1]['type'];
155: }
156: $this->checkItemChildType($parentType,$childType);
157: if($this->detectLoop($itemName,$childName))
158: throw new CException(Yii::t('yii','Cannot add "{child}" as a child of "{name}". A loop has been detected.',
159: array('{child}'=>$childName,'{name}'=>$itemName)));
160:
161: $this->db->createCommand()
162: ->insert($this->itemChildTable, array(
163: 'parent'=>$itemName,
164: 'child'=>$childName,
165: ));
166:
167: return true;
168: }
169: else
170: throw new CException(Yii::t('yii','Either "{parent}" or "{child}" does not exist.',array('{child}'=>$childName,'{parent}'=>$itemName)));
171: }
172:
173: /**
174: * Removes a child from its parent.
175: * Note, the child item is not deleted. Only the parent-child relationship is removed.
176: * @param string $itemName the parent item name
177: * @param string $childName the child item name
178: * @return boolean whether the removal is successful
179: */
180: public function removeItemChild($itemName,$childName)
181: {
182: return $this->db->createCommand()
183: ->delete($this->itemChildTable, 'parent=:parent AND child=:child', array(
184: ':parent'=>$itemName,
185: ':child'=>$childName
186: )) > 0;
187: }
188:
189: /**
190: * Returns a value indicating whether a child exists within a parent.
191: * @param string $itemName the parent item name
192: * @param string $childName the child item name
193: * @return boolean whether the child exists
194: */
195: public function hasItemChild($itemName,$childName)
196: {
197: return $this->db->createCommand()
198: ->select('parent')
199: ->from($this->itemChildTable)
200: ->where('parent=:parent AND child=:child', array(
201: ':parent'=>$itemName,
202: ':child'=>$childName))
203: ->queryScalar() !== false;
204: }
205:
206: /**
207: * Returns the children of the specified item.
208: * @param mixed $names the parent item name. This can be either a string or an array.
209: * The latter represents a list of item names.
210: * @return array all child items of the parent
211: */
212: public function getItemChildren($names)
213: {
214: if(is_string($names))
215: $condition='parent='.$this->db->quoteValue($names);
216: elseif(is_array($names) && $names!==array())
217: {
218: foreach($names as &$name)
219: $name=$this->db->quoteValue($name);
220: $condition='parent IN ('.implode(', ',$names).')';
221: }
222:
223: $rows=$this->db->createCommand()
224: ->select('name, type, description, bizrule, data')
225: ->from(array(
226: $this->itemTable,
227: $this->itemChildTable
228: ))
229: ->where($condition.' AND name=child')
230: ->queryAll();
231:
232: $children=array();
233: foreach($rows as $row)
234: {
235: if(($data=@unserialize($row['data']))===false)
236: $data=null;
237: $children[$row['name']]=new CAuthItem($this,$row['name'],$row['type'],$row['description'],$row['bizrule'],$data);
238: }
239: return $children;
240: }
241:
242: /**
243: * Assigns an authorization item to a user.
244: * @param string $itemName the item name
245: * @param mixed $userId the user ID (see {@link IWebUser::getId})
246: * @param string $bizRule the business rule to be executed when {@link checkAccess} is called
247: * for this particular authorization item.
248: * @param mixed $data additional data associated with this assignment
249: * @return CAuthAssignment the authorization assignment information.
250: * @throws CException if the item does not exist or if the item has already been assigned to the user
251: */
252: public function assign($itemName,$userId,$bizRule=null,$data=null)
253: {
254: if($this->usingSqlite() && $this->getAuthItem($itemName)===null)
255: throw new CException(Yii::t('yii','The item "{name}" does not exist.',array('{name}'=>$itemName)));
256:
257: $this->db->createCommand()
258: ->insert($this->assignmentTable, array(
259: 'itemname'=>$itemName,
260: 'userid'=>$userId,
261: 'bizrule'=>$bizRule,
262: 'data'=>serialize($data)
263: ));
264: return new CAuthAssignment($this,$itemName,$userId,$bizRule,$data);
265: }
266:
267: /**
268: * Revokes an authorization assignment from a user.
269: * @param string $itemName the item name
270: * @param mixed $userId the user ID (see {@link IWebUser::getId})
271: * @return boolean whether removal is successful
272: */
273: public function revoke($itemName,$userId)
274: {
275: return $this->db->createCommand()
276: ->delete($this->assignmentTable, 'itemname=:itemname AND userid=:userid', array(
277: ':itemname'=>$itemName,
278: ':userid'=>$userId
279: )) > 0;
280: }
281:
282: /**
283: * Returns a value indicating whether the item has been assigned to the user.
284: * @param string $itemName the item name
285: * @param mixed $userId the user ID (see {@link IWebUser::getId})
286: * @return boolean whether the item has been assigned to the user.
287: */
288: public function isAssigned($itemName,$userId)
289: {
290: return $this->db->createCommand()
291: ->select('itemname')
292: ->from($this->assignmentTable)
293: ->where('itemname=:itemname AND userid=:userid', array(
294: ':itemname'=>$itemName,
295: ':userid'=>$userId))
296: ->queryScalar() !== false;
297: }
298:
299: /**
300: * Returns the item assignment information.
301: * @param string $itemName the item name
302: * @param mixed $userId the user ID (see {@link IWebUser::getId})
303: * @return CAuthAssignment the item assignment information. Null is returned if
304: * the item is not assigned to the user.
305: */
306: public function getAuthAssignment($itemName,$userId)
307: {
308: $row=$this->db->createCommand()
309: ->select()
310: ->from($this->assignmentTable)
311: ->where('itemname=:itemname AND userid=:userid', array(
312: ':itemname'=>$itemName,
313: ':userid'=>$userId))
314: ->queryRow();
315: if($row!==false)
316: {
317: if(($data=@unserialize($row['data']))===false)
318: $data=null;
319: return new CAuthAssignment($this,$row['itemname'],$row['userid'],$row['bizrule'],$data);
320: }
321: else
322: return null;
323: }
324:
325: /**
326: * Returns the item assignments for the specified user.
327: * @param mixed $userId the user ID (see {@link IWebUser::getId})
328: * @return array the item assignment information for the user. An empty array will be
329: * returned if there is no item assigned to the user.
330: */
331: public function getAuthAssignments($userId)
332: {
333: $rows=$this->db->createCommand()
334: ->select()
335: ->from($this->assignmentTable)
336: ->where('userid=:userid', array(':userid'=>$userId))
337: ->queryAll();
338: $assignments=array();
339: foreach($rows as $row)
340: {
341: if(($data=@unserialize($row['data']))===false)
342: $data=null;
343: $assignments[$row['itemname']]=new CAuthAssignment($this,$row['itemname'],$row['userid'],$row['bizrule'],$data);
344: }
345: return $assignments;
346: }
347:
348: /**
349: * Saves the changes to an authorization assignment.
350: * @param CAuthAssignment $assignment the assignment that has been changed.
351: */
352: public function saveAuthAssignment($assignment)
353: {
354: $this->db->createCommand()
355: ->update($this->assignmentTable, array(
356: 'bizrule'=>$assignment->getBizRule(),
357: 'data'=>serialize($assignment->getData()),
358: ), 'itemname=:itemname AND userid=:userid', array(
359: 'itemname'=>$assignment->getItemName(),
360: 'userid'=>$assignment->getUserId()
361: ));
362: }
363:
364: /**
365: * Returns the authorization items of the specific type and user.
366: * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null,
367: * meaning returning all items regardless of their type.
368: * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if
369: * they are not assigned to a user.
370: * @return array the authorization items of the specific type.
371: */
372: public function getAuthItems($type=null,$userId=null)
373: {
374: if($type===null && $userId===null)
375: {
376: $command=$this->db->createCommand()
377: ->select()
378: ->from($this->itemTable);
379: }
380: elseif($userId===null)
381: {
382: $command=$this->db->createCommand()
383: ->select()
384: ->from($this->itemTable)
385: ->where('type=:type', array(':type'=>$type));
386: }
387: elseif($type===null)
388: {
389: $command=$this->db->createCommand()
390: ->select('name,type,description,t1.bizrule,t1.data')
391: ->from(array(
392: $this->itemTable.' t1',
393: $this->assignmentTable.' t2'
394: ))
395: ->where('name=itemname AND userid=:userid', array(':userid'=>$userId));
396: }
397: else
398: {
399: $command=$this->db->createCommand()
400: ->select('name,type,description,t1.bizrule,t1.data')
401: ->from(array(
402: $this->itemTable.' t1',
403: $this->assignmentTable.' t2'
404: ))
405: ->where('name=itemname AND type=:type AND userid=:userid', array(
406: ':type'=>$type,
407: ':userid'=>$userId
408: ));
409: }
410: $items=array();
411: foreach($command->queryAll() as $row)
412: {
413: if(($data=@unserialize($row['data']))===false)
414: $data=null;
415: $items[$row['name']]=new CAuthItem($this,$row['name'],$row['type'],$row['description'],$row['bizrule'],$data);
416: }
417: return $items;
418: }
419:
420: /**
421: * Creates an authorization item.
422: * An authorization item represents an action permission (e.g. creating a post).
423: * It has three types: operation, task and role.
424: * Authorization items form a hierarchy. Higher level items inherit permissions representing
425: * by lower level items.
426: * @param string $name the item name. This must be a unique identifier.
427: * @param integer $type the item type (0: operation, 1: task, 2: role).
428: * @param string $description description of the item
429: * @param string $bizRule business rule associated with the item. This is a piece of
430: * PHP code that will be executed when {@link checkAccess} is called for the item.
431: * @param mixed $data additional data associated with the item.
432: * @return CAuthItem the authorization item
433: * @throws CException if an item with the same name already exists
434: */
435: public function createAuthItem($name,$type,$description='',$bizRule=null,$data=null)
436: {
437: $this->db->createCommand()
438: ->insert($this->itemTable, array(
439: 'name'=>$name,
440: 'type'=>$type,
441: 'description'=>$description,
442: 'bizrule'=>$bizRule,
443: 'data'=>serialize($data)
444: ));
445: return new CAuthItem($this,$name,$type,$description,$bizRule,$data);
446: }
447:
448: /**
449: * Removes the specified authorization item.
450: * @param string $name the name of the item to be removed
451: * @return boolean whether the item exists in the storage and has been removed
452: */
453: public function removeAuthItem($name)
454: {
455: if($this->usingSqlite())
456: {
457: $this->db->createCommand()
458: ->delete($this->itemChildTable, 'parent=:name1 OR child=:name2', array(
459: ':name1'=>$name,
460: ':name2'=>$name
461: ));
462: $this->db->createCommand()
463: ->delete($this->assignmentTable, 'itemname=:name', array(
464: ':name'=>$name,
465: ));
466: }
467:
468: return $this->db->createCommand()
469: ->delete($this->itemTable, 'name=:name', array(
470: ':name'=>$name
471: )) > 0;
472: }
473:
474: /**
475: * Returns the authorization item with the specified name.
476: * @param string $name the name of the item
477: * @return CAuthItem the authorization item. Null if the item cannot be found.
478: */
479: public function getAuthItem($name)
480: {
481: $row=$this->db->createCommand()
482: ->select()
483: ->from($this->itemTable)
484: ->where('name=:name', array(':name'=>$name))
485: ->queryRow();
486:
487: if($row!==false)
488: {
489: if(($data=@unserialize($row['data']))===false)
490: $data=null;
491: return new CAuthItem($this,$row['name'],$row['type'],$row['description'],$row['bizrule'],$data);
492: }
493: else
494: return null;
495: }
496:
497: /**
498: * Saves an authorization item to persistent storage.
499: * @param CAuthItem $item the item to be saved.
500: * @param string $oldName the old item name. If null, it means the item name is not changed.
501: */
502: public function saveAuthItem($item,$oldName=null)
503: {
504: if($this->usingSqlite() && $oldName!==null && $item->getName()!==$oldName)
505: {
506: $this->db->createCommand()
507: ->update($this->itemChildTable, array(
508: 'parent'=>$item->getName(),
509: ), 'parent=:whereName', array(
510: ':whereName'=>$oldName,
511: ));
512: $this->db->createCommand()
513: ->update($this->itemChildTable, array(
514: 'child'=>$item->getName(),
515: ), 'child=:whereName', array(
516: ':whereName'=>$oldName,
517: ));
518: $this->db->createCommand()
519: ->update($this->assignmentTable, array(
520: 'itemname'=>$item->getName(),
521: ), 'itemname=:whereName', array(
522: ':whereName'=>$oldName,
523: ));
524: }
525:
526: $this->db->createCommand()
527: ->update($this->itemTable, array(
528: 'name'=>$item->getName(),
529: 'type'=>$item->getType(),
530: 'description'=>$item->getDescription(),
531: 'bizrule'=>$item->getBizRule(),
532: 'data'=>serialize($item->getData()),
533: ), 'name=:whereName', array(
534: ':whereName'=>$oldName===null?$item->getName():$oldName,
535: ));
536: }
537:
538: /**
539: * Saves the authorization data to persistent storage.
540: */
541: public function save()
542: {
543: }
544:
545: /**
546: * Removes all authorization data.
547: */
548: public function clearAll()
549: {
550: $this->clearAuthAssignments();
551: $this->db->createCommand()->delete($this->itemChildTable);
552: $this->db->createCommand()->delete($this->itemTable);
553: }
554:
555: /**
556: * Removes all authorization assignments.
557: */
558: public function clearAuthAssignments()
559: {
560: $this->db->createCommand()->delete($this->assignmentTable);
561: }
562:
563: /**
564: * Checks whether there is a loop in the authorization item hierarchy.
565: * @param string $itemName parent item name
566: * @param string $childName the name of the child item that is to be added to the hierarchy
567: * @return boolean whether a loop exists
568: */
569: protected function detectLoop($itemName,$childName)
570: {
571: if($childName===$itemName)
572: return true;
573: foreach($this->getItemChildren($childName) as $child)
574: {
575: if($this->detectLoop($itemName,$child->getName()))
576: return true;
577: }
578: return false;
579: }
580:
581: /**
582: * @return CDbConnection the DB connection instance
583: * @throws CException if {@link connectionID} does not point to a valid application component.
584: */
585: protected function getDbConnection()
586: {
587: if($this->db!==null)
588: return $this->db;
589: elseif(($this->db=Yii::app()->getComponent($this->connectionID)) instanceof CDbConnection)
590: return $this->db;
591: else
592: throw new CException(Yii::t('yii','CDbAuthManager.connectionID "{id}" is invalid. Please make sure it refers to the ID of a CDbConnection application component.',
593: array('{id}'=>$this->connectionID)));
594: }
595:
596: /**
597: * @return boolean whether the database is a SQLite database
598: */
599: protected function usingSqlite()
600: {
601: return $this->_usingSqlite;
602: }
603: }
604: