Skip to content

feat: Support Casbin UpdatableAdapter interface #15

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

Merged
merged 1 commit into from
Sep 26, 2021
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '12'
node-version: '14.17'

- name: Run semantic-release
env:
Expand Down
133 changes: 126 additions & 7 deletions src/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Casbin\Persist\Adapter as AdapterContract;
use Casbin\Persist\BatchAdapter as BatchAdapterContract;
use Casbin\Persist\FilteredAdapter as FilteredAdapterContract;
use Casbin\Persist\UpdatableAdapter as UpdatableAdapterContract;
use Casbin\Persist\AdapterHelper;
use Casbin\Persist\Adapters\Filter;
use Casbin\Exceptions\InvalidFilterTypeException;
Expand All @@ -16,7 +17,7 @@
*
* @author techlee@qq.com
*/
class Adapter implements AdapterContract, BatchAdapterContract, FilteredAdapterContract
class Adapter implements AdapterContract, BatchAdapterContract, FilteredAdapterContract, UpdatableAdapterContract
{
use AdapterHelper;

Expand Down Expand Up @@ -162,19 +163,19 @@ public function removePolicies(string $sec, string $ptype, array $rules): void
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
}

/**
* RemoveFilteredPolicy removes policy rules that match the filter from the storage.
* This is part of the Auto-Save feature.
*
* @param string $sec
* @param string $ptype
* @param int $fieldIndex
* @param string ...$fieldValues
* @param int $fieldIndex
* @param string|null ...$fieldValues
* @return array
* @throws Throwable
*/
public function removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, string ...$fieldValues): void
public function _removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, ?string ...$fieldValues): array
{
$where = [];
$where['ptype'] = $ptype;
Expand All @@ -187,7 +188,31 @@ public function removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex
}
}

$removedRules = $this->casbinRule->find()->where($where)->all();
$this->casbinRule->deleteAll($where);

array_walk($removedRules, function (&$removedRule) {
unset($removedRule->id);
unset($removedRule->ptype);
$removedRule = $removedRule->toArray();
$removedRule = $this->filterRule($removedRule);
});

return $removedRules;
}

/**
* RemoveFilteredPolicy removes policy rules that match the filter from the storage.
* This is part of the Auto-Save feature.
*
* @param string $sec
* @param string $ptype
* @param int $fieldIndex
* @param string ...$fieldValues
*/
public function removeFilteredPolicy(string $sec, string $ptype, int $fieldIndex, string ...$fieldValues): void
{
$this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues);
}

/**
Expand Down Expand Up @@ -226,6 +251,100 @@ public function loadFilteredPolicy(Model $model, $filter): void
$this->setFiltered(true);
}

/**
* Updates a policy rule from storage.
* This is part of the Auto-Save feature.
*
* @param string $sec
* @param string $ptype
* @param string[] $oldRule
* @param string[] $newPolicy
*/
public function updatePolicy(string $sec, string $ptype, array $oldRule, array $newPolicy): void
{
$entity = clone $this->casbinRule;

$condition['ptype'] = $ptype;
foreach ($oldRule as $k => $v) {
$condition['v' . $k] = $v;
}
$item = $entity->findOne($condition);
foreach ($newPolicy as $k => $v) {
$key = 'v' . $k;
$item->$key = $v;
}
$item->update();
}

/**
* UpdatePolicies updates some policy rules to storage, like db, redis.
*
* @param string $sec
* @param string $ptype
* @param string[][] $oldRules
* @param string[][] $newRules
* @return void
*/
public function updatePolicies(string $sec, string $ptype, array $oldRules, array $newRules): void
{
$transaction = $this->casbinRule->getDb()->beginTransaction();
try {
foreach ($oldRules as $i => $oldRule) {
$this->updatePolicy($sec, $ptype, $oldRule, $newRules[$i]);
}
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
}

/**
* UpdateFilteredPolicies deletes old rules and adds new rules.
*
* @param string $sec
* @param string $ptype
* @param array $newPolicies
* @param integer $fieldIndex
* @param string ...$fieldValues
* @return array
*/
public function updateFilteredPolicies(string $sec, string $ptype, array $newRules, int $fieldIndex, ?string ...$fieldValues): array
{
$oldRules = [];
$transaction = $this->casbinRule->getDb()->beginTransaction();
try {
$oldRules = $this->_removeFilteredPolicy($sec, $ptype, $fieldIndex, ...$fieldValues);
$this->addPolicies($sec, $ptype, $newRules);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@basakest The exception should not be ignored here, same as updatePolicies().

throw $e;
}

return $oldRules;
}

/**
* Filter the rule.
*
* @param array $rule
* @return array
*/
public function filterRule(array $rule): array
{
$rule = array_values($rule);

$i = count($rule) - 1;
for (; $i >= 0; $i--) {
if ($rule[$i] != "" && !is_null($rule[$i])) {
break;
}
}

return array_slice($rule, 0, $i + 1);
}

/**
* Returns true if the loaded policy has been filtered.
*
Expand Down
184 changes: 184 additions & 0 deletions tests/AdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,190 @@ public function testRemoveFilteredPolicy()
$this->assertFalse(Yii::$app->permission->enforce('alice', 'data2', 'write'));
}

public function testUpdatePolicy()
{
$this->assertEquals([
['alice', 'data1', 'read'],
['bob', 'data2', 'write'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
], Yii::$app->permission->getPolicy());

Yii::$app->permission->updatePolicy(
['alice', 'data1', 'read'],
['alice', 'data1', 'write']
);

Yii::$app->permission->updatePolicy(
['bob', 'data2', 'write'],
['bob', 'data2', 'read']
);

$this->assertEquals([
['alice', 'data1', 'write'],
['bob', 'data2', 'read'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
], Yii::$app->permission->getPolicy());
}

public function testUpdatePolicies()
{
$this->assertEquals([
['alice', 'data1', 'read'],
['bob', 'data2', 'write'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
], Yii::$app->permission->getPolicy());

$oldPolicies = [
['alice', 'data1', 'read'],
['bob', 'data2', 'write']
];
$newPolicies = [
['alice', 'data1', 'write'],
['bob', 'data2', 'read']
];

Yii::$app->permission->updatePolicies($oldPolicies, $newPolicies);

$this->assertEquals([
['alice', 'data1', 'write'],
['bob', 'data2', 'read'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
], Yii::$app->permission->getPolicy());
}

public function arrayEqualsWithoutOrder(array $expected, array $actual)
{
if (method_exists($this, 'assertEqualsCanonicalizing')) {
$this->assertEqualsCanonicalizing($expected, $actual);
} else {
array_multisort($expected);
array_multisort($actual);
$this->assertEquals($expected, $actual);
}
}

public function testUpdateFilteredPolicies()
{
$this->assertEquals([
['alice', 'data1', 'read'],
['bob', 'data2', 'write'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
], Yii::$app->permission->getPolicy());

Yii::$app->permission->updateFilteredPolicies([["alice", "data1", "write"]], 0, "alice", "data1", "read");
Yii::$app->permission->updateFilteredPolicies([["bob", "data2", "read"]], 0, "bob", "data2", "write");

$policies = [
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
['alice', 'data1', 'write'],
['bob', 'data2', 'read'],
];
$this->arrayEqualsWithoutOrder($policies, Yii::$app->permission->getPolicy());

// test use updateFilteredPolicies to update all policies of a user
$this->initTable();
$this->refreshApplication();

$policies = [
['alice', 'data2', 'write'],
['bob', 'data1', 'read']
];

Yii::$app->permission->addPolicies($policies);
$this->arrayEqualsWithoutOrder([
['alice', 'data1', 'read'],
['bob', 'data2', 'write'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
['alice', 'data2', 'write'],
['bob', 'data1', 'read']
], Yii::$app->permission->getPolicy());

Yii::$app->permission->updateFilteredPolicies([['alice', 'data1', 'write'], ['alice', 'data2', 'read']], 0, 'alice');
Yii::$app->permission->updateFilteredPolicies([['bob', 'data1', 'write'], ["bob", "data2", "read"]], 0, 'bob');

$policies = [
['alice', 'data1', 'write'],
['alice', 'data2', 'read'],
['bob', 'data1', 'write'],
['bob', 'data2', 'read'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write']
];

$this->arrayEqualsWithoutOrder($policies, Yii::$app->permission->getPolicy());

// test if $fieldValues contains empty string
$this->initTable();
$this->refreshApplication();

$policies = [
['alice', 'data2', 'write'],
['bob', 'data1', 'read']
];
Yii::$app->permission->addPolicies($policies);

$this->assertEquals([
['alice', 'data1', 'read'],
['bob', 'data2', 'write'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
['alice', 'data2', 'write'],
['bob', 'data1', 'read']
], Yii::$app->permission->getPolicy());

Yii::$app->permission->updateFilteredPolicies([['alice', 'data1', 'write'], ['alice', 'data2', 'read']], 0, 'alice', '', '');
Yii::$app->permission->updateFilteredPolicies([['bob', 'data1', 'write'], ["bob", "data2", "read"]], 0, 'bob', '', '');

$policies = [
['alice', 'data1', 'write'],
['alice', 'data2', 'read'],
['bob', 'data1', 'write'],
['bob', 'data2', 'read'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write']
];

$this->arrayEqualsWithoutOrder($policies, Yii::$app->permission->getPolicy());

// test if $fieldIndex is not zero
$this->initTable();
$this->refreshApplication();

$policies = [
['alice', 'data2', 'write'],
['bob', 'data1', 'read']
];
Yii::$app->permission->addPolicies($policies);

$this->assertEquals([
['alice', 'data1', 'read'],
['bob', 'data2', 'write'],
['data2_admin', 'data2', 'read'],
['data2_admin', 'data2', 'write'],
['alice', 'data2', 'write'],
['bob', 'data1', 'read']
], Yii::$app->permission->getPolicy());

Yii::$app->permission->updateFilteredPolicies([['alice', 'data1', 'edit'], ['bob', 'data1', 'edit']], 2, 'read');
Yii::$app->permission->updateFilteredPolicies([['alice', 'data2', 'read'], ["bob", "data2", "read"]], 2, 'write');

$policies = [
['alice', 'data1', 'edit'],
['alice', 'data2', 'read'],
['bob', 'data1', 'edit'],
['bob', 'data2', 'read'],
];

$this->arrayEqualsWithoutOrder($policies, Yii::$app->permission->getPolicy());
}

public function testLoadFilteredPolicy()
{
Yii::$app->permission->clearPolicy();
Expand Down