Skip to content

Commit

Permalink
feat: Add Yii authorization integration with AuthManager and Behavior…
Browse files Browse the repository at this point in the history
…s methods (#18)

* feat: add Yii authorization integration with AuthManager and Behaviors methods

* docs: update README with new Yii Authorization features and usage examples
  • Loading branch information
Dobmod authored Aug 21, 2024
1 parent c8cb31b commit c333eef
Show file tree
Hide file tree
Showing 9 changed files with 638 additions and 81 deletions.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,64 @@ Determines whether a user has a permission.
$permission->hasPermissionForUser('eve', 'articles', 'read'); // true or false
```

### Using Yii Authorization

It allows you to integrate Yii's authorization with the Casbin permission management system.

**(1) AccessChecker**

Add the accessChecker configuration in your application's `config/web.php` file:

```php
$config = [
'components' => [
'user' => [
...
'accessChecker' => 'yii\permission\components\PermissionChecker',
]
];
```

Once configured, you can use the `can()` method to check if a user has permission to perform certain actions:

```php
$user->can('acrticles,read');
```

**(2) Behaviors**

The `PermissionControl` behavior allows you to enforce permission checks at the controller level. Add the PermissionControl behavior to your controller's behaviors() method:

```php
public function behaviors()
{
return [
'permission' => [
'class' => \yii\permission\components\PermissionControl::class,
'user' => $user, // optional, defaults to \Yii::$app->user
'only' => ['read-articles', 'write-articles'],
'policy' => [
[
'allow' => true,
'actions' => ['read-articles'],
'enforce' => ['articles', 'read']
],
[
'allow' => true,
'actions' => ['write-articles'],
'enforce' => ['articles', 'write']
]
],
'denyCallback' => function ($policy, $action) {
// custom action when access is denied
} // optional, defaults to throwing an exception
]
];
}
```

**Note:** Additionally,You can also configure a `denyCallback` for each `policy`, which will be invoked when the user does not meet the required permission. This callback takes precedence. The configuration is similar to Yii's official [AccessControl](https://www.yiiframework.com/doc/guide/2.0/zh-cn/security-authorization#access-control-filter).

See [Casbin API](https://casbin.org/docs/en/management-api) for more APIs.

## Define your own model.conf
Expand Down
24 changes: 24 additions & 0 deletions src/components/PermissionChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace yii\permission\components;

use Yii;
use yii\rbac\CheckAccessInterface;

class PermissionChecker implements CheckAccessInterface
{
/**
* Checks if the user has access to a certain policy.
*
* @param int $userId The ID of the user to check.
* @param string $policy The policy to check access for.
* @param array $guards Optional guards to check, not supported yet.
*
* @return bool Whether the user has access to the policy.
*/
public function checkAccess($userId, $policy, $guards = [])
{
$params = explode(',', $policy);
return Yii::$app->permission->enforce($userId, ...$params);
}
}
100 changes: 100 additions & 0 deletions src/components/PermissionControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace yii\permission\components;

use Yii;
use yii\base\ActionFilter;
use yii\di\Instance;
use yii\web\ForbiddenHttpException;
use yii\web\User;

class PermissionControl extends ActionFilter
{
/**
* @var User|array|string|false the user object.
*/
public $user = 'user';

/**
* @var callable|null a callback that will be called if the access should be denied
*/
public $denyCallback;

/**
* @var array the default configuration of the policy
*/
public $policyConfig = ['class' => 'yii\permission\components\PermissionPolicy'];

/**
* @var array the policies.
*/
public $policy = [];

/**
* Initializes the PermissionControl component.
*
* @return void
*/
public function init()
{
parent::init();
if ($this->user !== false) {
$this->user = Instance::ensure($this->user, User::class);
}
foreach ($this->policy as $i => $policy) {
if (is_array($policy)) {
$this->policy[$i] = Yii::createObject(array_merge($this->policyConfig, $policy));
}
}
}

/**
* Checks if the current user has permission to perform the given action.
*
* @param Action $action the action to be performed
* @throws ForbiddenHttpException if the user does not have permission
* @return bool true if the user has permission, false otherwise
*/
public function beforeAction($action)
{
$user = $this->user;
foreach ($this->policy as $policy) {
if ($allow = $policy->allows($action, $user)) {
return true;
} elseif ($allow === false) {
if (isset($policy->denyCallback)) {
call_user_func($policy->denyCallback, $policy, $action);
} elseif ($this->denyCallback !== null) {
call_user_func($this->denyCallback, $policy, $action);
} else {
$this->denyAccess($user);
}

return false;
}
}

if ($this->denyCallback !== null) {
call_user_func($this->denyCallback, null, $action);
} else {
$this->denyAccess($user);
}
return false;
}
/**
* Denies the access of the user.
* The default implementation will redirect the user to the login page if he is a guest;
* if the user is already logged, a 403 HTTP exception will be thrown.
*
* @param User|false $user the current user or boolean `false` in case of detached User component
* @throws ForbiddenHttpException if the user is already logged in or in case of detached User component.
*/
protected function denyAccess($user)
{
if ($user !== false && $user->getIsGuest()) {
$user->loginRequired();
} else {
throw new ForbiddenHttpException(Yii::t('yii', 'You are not allowed to perform this action.'));
}
}
}
74 changes: 74 additions & 0 deletions src/components/PermissionPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace yii\permission\components;

use Yii;
use yii\base\Component;
use yii\web\User;

class PermissionPolicy extends Component
{
/**
* @var bool whether this is an 'allow' rule or 'deny' rule.
*/
public $allow = false;

/**
* @var array|null list of the controller IDs that this rule applies to.
*/
public $actions = [];

/**
* @var array|null list of params that passed to Casbin.
*/
public $enforce = [];

/**
* @var callable|null a callback that will be called if the access should be denied
*/
public $denyCallback;

/**
* Checks whether the given action is allowed for the specified user.
*
* @param string $action the action to be checked
* @param User $user the user to be checked
*
* @return bool|null true if the action is allowed, false if not, null if the rule does not apply
*/
public function allows($action, $user)
{
if (
$this->matchAction($action)
&& $this->matchEnforce($user, $this->enforce)
) {
return $this->allow ? true : false;
}

return null;
}

/**
* Checks if the rule applies to the specified action.
*
* @param Action $action the action
* @return bool whether the rule applies to the action
*/
protected function matchAction($action)
{
return empty($this->actions) || in_array($action->id, $this->actions, true);
}

/**
* Checks if the rule applies to the specified user.
*
* @param User $user
* @param array $params
*
* @return bool
*/
protected function matchEnforce($user, $params)
{
return Yii::$app->permission->enforce($user->getId(), ...$params);
}
}
81 changes: 0 additions & 81 deletions tests/AdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@

namespace yii\permission\tests;

use PHPUnit\Framework\TestCase;
use yii\web\Application;
use Yii;
use yii\permission\models\CasbinRule;
use Casbin\Persist\Adapters\Filter;
use Casbin\Exceptions\InvalidFilterTypeException;
use yii\db\ActiveQueryInterface;

class AdapterTest extends TestCase
{
protected $app;

public function testEnforce()
{
$this->assertTrue(Yii::$app->permission->enforce('alice', 'data1', 'read'));
Expand Down Expand Up @@ -331,80 +326,4 @@ public function testLoadFilteredPolicy()
['alice', 'data1', 'read'],
], Yii::$app->permission->getPolicy());
}

public function createApplication()
{
$config = require __DIR__ . '/../vendor/yiisoft/yii2-app-basic/config/web.php';
$config['components']['permission'] = require __DIR__ . '/../config/permission.php';

$config['components']['db']['dsn'] = 'mysql:host=' . $this->env('DB_HOST', '127.0.0.1') . ';port=' . $this->env('DB_PORT', '3306') . ';dbname=' . $this->env('DB_DATABASE', 'casbin');
$config['components']['db']['username'] = $this->env('DB_USERNAME', 'root');
$config['components']['db']['password'] = $this->env('DB_PASSWORD', '');

return new Application($config);
}

/**
* init table.
*/
protected function initTable()
{
$db = CasbinRule::getDb();
$tableName = CasbinRule::tableName();
$table = $db->getTableSchema($tableName);
if ($table) {
$db->createCommand()->dropTable($tableName)->execute();
}

Yii::$app->permission->init();

Yii::$app->db->createCommand()->batchInsert(
$tableName,
['ptype', 'v0', 'v1', 'v2'],
[
['p', 'alice', 'data1', 'read'],
['p', 'bob', 'data2', 'write'],
['p', 'data2_admin', 'data2', 'read'],
['p', 'data2_admin', 'data2', 'write'],
['g', 'alice', 'data2_admin', null],
]
)->execute();
}

/**
* Refresh the application instance.
*/
protected function refreshApplication()
{
$this->app = $this->createApplication();
}

/**
* This method is called before each test.
*/
protected function setUp(): void/* The :void return type declaration that should be here would cause a BC issue */
{
if (!$this->app) {
$this->refreshApplication();
}

$this->initTable();
}

/**
* This method is called after each test.
*/
protected function tearDown(): void/* The :void return type declaration that should be here would cause a BC issue */
{
}

protected function env($key, $default = null)
{
$value = getenv($key);
if (is_null($default)) {
return $value;
}

return false === $value ? $default : $value;
}
}
Loading

0 comments on commit c333eef

Please sign in to comment.