Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Yii authorization integration with AuthManager and Behaviors methods #18

Merged
merged 2 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading