From 163412f46f6ad3b61538bc652d6251c9ce371bf7 Mon Sep 17 00:00:00 2001 From: basakest Date: Tue, 21 Sep 2021 23:35:45 +0800 Subject: [PATCH] feat: Support Casbin UpdatableAdapter interface feat: Support Casbin UpdatableAdapter interface feat: Support Casbin UpdatableAdapter interface --- .github/workflows/build.yml | 2 +- src/Adapter.php | 133 ++++++++++++++++++++++++-- tests/AdapterTest.php | 184 ++++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e440e98..b096b44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/src/Adapter.php b/src/Adapter.php index f6cbcc5..d6a4340 100755 --- a/src/Adapter.php +++ b/src/Adapter.php @@ -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; @@ -16,7 +17,7 @@ * * @author techlee@qq.com */ -class Adapter implements AdapterContract, BatchAdapterContract, FilteredAdapterContract +class Adapter implements AdapterContract, BatchAdapterContract, FilteredAdapterContract, UpdatableAdapterContract { use AdapterHelper; @@ -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; @@ -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); } /** @@ -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(); + 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. * diff --git a/tests/AdapterTest.php b/tests/AdapterTest.php index 21b196a..9dd8b83 100755 --- a/tests/AdapterTest.php +++ b/tests/AdapterTest.php @@ -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();