1: <?php
2: /**
3: * CAccessControlFilter 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: * CAccessControlFilter performs authorization checks for the specified actions.
13: *
14: * By enabling this filter, controller actions can be checked for access permissions.
15: * When the user is not denied by one of the security rules or allowed by a rule explicitly,
16: * he will be able to access the action.
17: *
18: * For maximum security consider adding
19: * <pre>array('deny')</pre>
20: * as a last rule in a list so all actions will be denied by default.
21: *
22: * To specify the access rules, set the {@link setRules rules} property, which should
23: * be an array of the rules. Each rule is specified as an array of the following structure:
24: * <pre>
25: * array(
26: * 'allow', // or 'deny'
27: *
28: * // optional, list of action IDs (case insensitive) that this rule applies to
29: * // if not specified or empty, rule applies to all actions
30: * 'actions'=>array('edit', 'delete'),
31: *
32: * // optional, list of controller IDs (case insensitive) that this rule applies to
33: * 'controllers'=>array('post', 'admin/user'),
34: *
35: * // optional, list of usernames (case insensitive) that this rule applies to
36: * // Use * to represent all users, ? guest users, and @ authenticated users
37: * 'users'=>array('thomas', 'kevin'),
38: *
39: * // optional, list of roles (case sensitive!) that this rule applies to.
40: * 'roles'=>array('admin', 'editor'),
41: *
42: * // since version 1.1.11 you can pass parameters for RBAC bizRules
43: * 'roles'=>array('updateTopic'=>array('topic'=>$topic))
44: *
45: * // optional, list of IP address/patterns that this rule applies to
46: * // e.g. 127.0.0.1, 127.0.0.*
47: * 'ips'=>array('127.0.0.1'),
48: *
49: * // optional, list of request types (case insensitive) that this rule applies to
50: * 'verbs'=>array('GET', 'POST'),
51: *
52: * // optional, a PHP expression whose value indicates whether this rule applies
53: * // The PHP expression will be evaluated using {@link evaluateExpression}.
54: * // A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
55: * // please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
56: * 'expression'=>'!$user->isGuest && $user->level==2',
57: *
58: * // optional, the customized error message to be displayed
59: * // This option is available since version 1.1.1.
60: * 'message'=>'Access Denied.',
61: *
62: * // optional, the denied method callback name, that will be called once the
63: * // access is denied, instead of showing the customized error message. It can also be
64: * // a valid PHP callback, including class method name (array(ClassName/Object, MethodName)),
65: * // or anonymous function (PHP 5.3.0+). The function/method signature should be as follows:
66: * // function foo($user, $rule) { ... }
67: * // where $user is the current application user object and $rule is this access rule.
68: * // This option is available since version 1.1.11.
69: * 'deniedCallback'=>'redirectToDeniedMethod',
70: * )
71: * </pre>
72: *
73: * @property array $rules List of access rules.
74: *
75: * @author Qiang Xue <qiang.xue@gmail.com>
76: * @package system.web.auth
77: * @since 1.0
78: */
79: class CAccessControlFilter extends CFilter
80: {
81: /**
82: * @var string the error message to be displayed when authorization fails.
83: * This property can be overridden by individual access rule via {@link CAccessRule::message}.
84: * If this property is not set, a default error message will be displayed.
85: * @since 1.1.1
86: */
87: public $message;
88:
89: private $_rules=array();
90:
91: /**
92: * @return array list of access rules.
93: */
94: public function getRules()
95: {
96: return $this->_rules;
97: }
98:
99: /**
100: * @param array $rules list of access rules.
101: */
102: public function setRules($rules)
103: {
104: foreach($rules as $rule)
105: {
106: if(is_array($rule) && isset($rule[0]))
107: {
108: $r=new CAccessRule;
109: $r->allow=$rule[0]==='allow';
110: foreach(array_slice($rule,1) as $name=>$value)
111: {
112: if($name==='expression' || $name==='roles' || $name==='message' || $name==='deniedCallback')
113: $r->$name=$value;
114: else
115: $r->$name=array_map('strtolower',$value);
116: }
117: $this->_rules[]=$r;
118: }
119: }
120: }
121:
122: /**
123: * Performs the pre-action filtering.
124: * @param CFilterChain $filterChain the filter chain that the filter is on.
125: * @return boolean whether the filtering process should continue and the action
126: * should be executed.
127: */
128: protected function preFilter($filterChain)
129: {
130: $app=Yii::app();
131: $request=$app->getRequest();
132: $user=$app->getUser();
133: $verb=$request->getRequestType();
134: $ip=$request->getUserHostAddress();
135:
136: foreach($this->getRules() as $rule)
137: {
138: if(($allow=$rule->isUserAllowed($user,$filterChain->controller,$filterChain->action,$ip,$verb))>0) // allowed
139: break;
140: elseif($allow<0) // denied
141: {
142: if(isset($rule->deniedCallback))
143: call_user_func($rule->deniedCallback, $rule);
144: else
145: $this->accessDenied($user,$this->resolveErrorMessage($rule));
146: return false;
147: }
148: }
149:
150: return true;
151: }
152:
153: /**
154: * Resolves the error message to be displayed.
155: * This method will check {@link message} and {@link CAccessRule::message} to see
156: * what error message should be displayed.
157: * @param CAccessRule $rule the access rule
158: * @return string the error message
159: * @since 1.1.1
160: */
161: protected function resolveErrorMessage($rule)
162: {
163: if($rule->message!==null)
164: return $rule->message;
165: elseif($this->message!==null)
166: return $this->message;
167: else
168: return Yii::t('yii','You are not authorized to perform this action.');
169: }
170:
171: /**
172: * Denies the access of the user.
173: * This method is invoked when access check fails.
174: * @param IWebUser $user the current user
175: * @param string $message the error message to be displayed
176: */
177: protected function accessDenied($user,$message)
178: {
179: if($user->getIsGuest())
180: $user->loginRequired();
181: else
182: throw new CHttpException(403,$message);
183: }
184: }
185:
186:
187: /**
188: * CAccessRule represents an access rule that is managed by {@link CAccessControlFilter}.
189: *
190: * @author Qiang Xue <qiang.xue@gmail.com>
191: * @package system.web.auth
192: * @since 1.0
193: */
194: class CAccessRule extends CComponent
195: {
196: /**
197: * @var boolean whether this is an 'allow' rule or 'deny' rule.
198: */
199: public $allow;
200: /**
201: * @var array list of action IDs that this rule applies to. The comparison is case-insensitive.
202: * If no actions are specified, rule applies to all actions.
203: */
204: public $actions;
205: /**
206: * @var array list of controller IDs that this rule applies to. The comparison is case-insensitive.
207: */
208: public $controllers;
209: /**
210: * @var array list of user names that this rule applies to. The comparison is case-insensitive.
211: * If no user names are specified, rule applies to all users.
212: */
213: public $users;
214: /**
215: * @var array list of roles this rule applies to. For each role, the current user's
216: * {@link CWebUser::checkAccess} method will be invoked. If one of the invocations
217: * returns true, the rule will be applied.
218: * Note, you should mainly use roles in an "allow" rule because by definition,
219: * a role represents a permission collection.
220: * @see CAuthManager
221: */
222: public $roles;
223: /**
224: * @var array IP patterns.
225: */
226: public $ips;
227: /**
228: * @var array list of request types (e.g. GET, POST) that this rule applies to.
229: */
230: public $verbs;
231: /**
232: * @var string a PHP expression whose value indicates whether this rule should be applied.
233: * In this expression, you can use <code>$user</code> which refers to <code>Yii::app()->user</code>.
234: * The expression can also be a valid PHP callback,
235: * including class method name (array(ClassName/Object, MethodName)),
236: * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows:
237: * <pre>
238: * function foo($user, $rule) { ... }
239: * </pre>
240: * where $user is the current application user object and $rule is this access rule.
241: *
242: * The PHP expression will be evaluated using {@link evaluateExpression}.
243: *
244: * A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
245: * please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
246: */
247: public $expression;
248: /**
249: * @var string the error message to be displayed when authorization is denied by this rule.
250: * If not set, a default error message will be displayed.
251: * @since 1.1.1
252: */
253: public $message;
254: /**
255: * @var mixed the denied method callback that will be called once the
256: * access is denied. It replaces the behavior that shows an error message.
257: * It can be a valid PHP callback including class method name (array(ClassName/Object, MethodName)),
258: * or anonymous function (PHP 5.3.0+). For more information, on different options, check
259: * @link http://www.php.net/manual/en/language.pseudo-types.php#language.types.callback
260: * The function/method signature should be as follows:
261: * <pre>
262: * function foo($rule) { ... }
263: * </pre>
264: * where $rule is this access rule.
265: *
266: * @since 1.1.11
267: */
268: public $deniedCallback;
269:
270:
271: /**
272: * Checks whether the Web user is allowed to perform the specified action.
273: * @param CWebUser $user the user object
274: * @param CController $controller the controller currently being executed
275: * @param CAction $action the action to be performed
276: * @param string $ip the request IP address
277: * @param string $verb the request verb (GET, POST, etc.)
278: * @return integer 1 if the user is allowed, -1 if the user is denied, 0 if the rule does not apply to the user
279: */
280: public function isUserAllowed($user,$controller,$action,$ip,$verb)
281: {
282: if($this->isActionMatched($action)
283: && $this->isUserMatched($user)
284: && $this->isRoleMatched($user)
285: && $this->isIpMatched($ip)
286: && $this->isVerbMatched($verb)
287: && $this->isControllerMatched($controller)
288: && $this->isExpressionMatched($user))
289: return $this->allow ? 1 : -1;
290: else
291: return 0;
292: }
293:
294: /**
295: * @param CAction $action the action
296: * @return boolean whether the rule applies to the action
297: */
298: protected function isActionMatched($action)
299: {
300: return empty($this->actions) || in_array(strtolower($action->getId()),$this->actions);
301: }
302:
303: /**
304: * @param CController $controller the controller
305: * @return boolean whether the rule applies to the controller
306: */
307: protected function isControllerMatched($controller)
308: {
309: return empty($this->controllers) || in_array(strtolower($controller->getUniqueId()),$this->controllers);
310: }
311:
312: /**
313: * @param IWebUser $user the user
314: * @return boolean whether the rule applies to the user
315: */
316: protected function isUserMatched($user)
317: {
318: if(empty($this->users))
319: return true;
320: foreach($this->users as $u)
321: {
322: if($u==='*')
323: return true;
324: elseif($u==='?' && $user->getIsGuest())
325: return true;
326: elseif($u==='@' && !$user->getIsGuest())
327: return true;
328: elseif(!strcasecmp($u,$user->getName()))
329: return true;
330: }
331: return false;
332: }
333:
334: /**
335: * @param IWebUser $user the user object
336: * @return boolean whether the rule applies to the role
337: */
338: protected function isRoleMatched($user)
339: {
340: if(empty($this->roles))
341: return true;
342: foreach($this->roles as $key=>$role)
343: {
344: if(is_numeric($key))
345: {
346: if($user->checkAccess($role))
347: return true;
348: }
349: else
350: {
351: if($user->checkAccess($key,$role))
352: return true;
353: }
354: }
355: return false;
356: }
357:
358: /**
359: * @param string $ip the IP address
360: * @return boolean whether the rule applies to the IP address
361: */
362: protected function isIpMatched($ip)
363: {
364: if(empty($this->ips))
365: return true;
366: foreach($this->ips as $rule)
367: {
368: if($rule==='*' || $rule===$ip || (($pos=strpos($rule,'*'))!==false && !strncmp($ip,$rule,$pos)))
369: return true;
370: }
371: return false;
372: }
373:
374: /**
375: * @param string $verb the request method
376: * @return boolean whether the rule applies to the request
377: */
378: protected function isVerbMatched($verb)
379: {
380: return empty($this->verbs) || in_array(strtolower($verb),$this->verbs);
381: }
382:
383: /**
384: * @param IWebUser $user the user
385: * @return boolean the expression value. True if the expression is not specified.
386: */
387: protected function isExpressionMatched($user)
388: {
389: if($this->expression===null)
390: return true;
391: else
392: return $this->evaluateExpression($this->expression, array('user'=>$user));
393: }
394: }
395: