diff --git a/src/InternalEnforcer.php b/src/InternalEnforcer.php index fcd0764..ecdc806 100644 --- a/src/InternalEnforcer.php +++ b/src/InternalEnforcer.php @@ -148,6 +148,44 @@ protected function updatePolicyInternal(string $sec, string $ptype, array $oldRu return true; } + protected function updatePoliciesInternal(string $sec, string $ptype, array $oldRules, array $newRules): bool + { + if ($this->shouldPersist() && $this->adapter instanceof UpdatableAdapter) { + try { + $this->adapter->updatePolicies($sec, $ptype, $oldRules, $newRules); + } catch (NotImplementedException $e) { + } + } + + $ruleUpdated = $this->model->updatePolicies($sec, $ptype, $oldRules, $newRules); + if (!$ruleUpdated) { + return false; + } + + if ($sec == "g") { + // remove the old rule + $this->buildIncrementalRoleLinks(Policy::POLICY_REMOVE, $ptype, $oldRules); + + // add the new rule + $this->buildIncrementalRoleLinks(Policy::POLICY_ADD, $ptype, $newRules); + } + + if ($this->watcher !== null && $this->autoNotifyWatcher) { + try { + if ($this->watcher instanceof WatcherUpdatable) { + $this->watcher->updateForUpdatePolicies($oldRules, $newRules); + } else { + $this->watcher->update(); + } + } catch (\Exception $e) { + Log::logPrint("An exception occurred:" . $e->getMessage()); + return false; + } + } + + return true; + } + /** * Removes a rule from the current policy. * diff --git a/src/ManagementEnforcer.php b/src/ManagementEnforcer.php index 0b866a2..4f945d2 100644 --- a/src/ManagementEnforcer.php +++ b/src/ManagementEnforcer.php @@ -343,6 +343,31 @@ public function updateNamedPolicy(string $ptype, array $oldRule, array $newRule) return $this->updatePolicyInternal("p", $ptype, $oldRule, $newRule); } + /** + * UpdatePolicies updates authorization rules from the current policies. + * + * @param string[][] $oldPolices + * @param string[][] $newPolicies + * @return boolean + */ + public function updatePolicies(array $oldPolices, array $newPolicies): bool + { + return $this->updateNamedPolicies("p", $oldPolices, $newPolicies); + } + + /** + * Updates authorization rules from the current policy. + * + * @param string $ptype + * @param string[][] $oldPolices + * @param string[][] $newPolicies + * @return boolean + */ + public function updateNamedPolicies(string $ptype, array $oldPolices, array $newPolicies): bool + { + return $this->updatePoliciesInternal("p", $ptype, $oldPolices, $newPolicies); + } + /** * Removes an authorization rule from the current policy, field filters can be specified. * diff --git a/src/Model/Policy.php b/src/Model/Policy.php index 785ffb2..a36c4dd 100644 --- a/src/Model/Policy.php +++ b/src/Model/Policy.php @@ -241,6 +241,45 @@ public function updatePolicy(string $sec, string $ptype, array $oldRule, array $ return true; } + /** + * UpdatePolicies updates a policy rule from the model. + * + * @param string $sec + * @param string $ptype + * @param string[][] $oldRules + * @param string[][] $newRules + * @return boolean + */ + public function updatePolicies(string $sec, string $ptype, array $oldRules, array $newRules): bool + { + $modifiedRuleIndex = []; + + $newIndex = 0; + foreach ($oldRules as $oldIndex => $oldRule) { + $oldPolicy = implode(self::DEFAULT_SEP, $oldRule); + $index = $this->items[$sec][$ptype]->policyMap[$oldPolicy] ?? null; + if (is_null($index)) { + // rollback + foreach ($modifiedRuleIndex as $index => $oldNewIndex) { + $this->items[$sec][$ptype]->policy[$index] = $oldRules[$oldNewIndex[0]]; + $oldPolicy = implode(self::DEFAULT_SEP, $oldRules[$oldNewIndex[0]]); + $newPolicy = implode(self::DEFAULT_SEP, $newRules[$oldNewIndex[1]]); + unset($this->items[$sec][$ptype]->policyMap[$newPolicy]); + $this->items[$sec][$ptype]->policyMap[$oldPolicy] = $index; + } + return false; + } + + $this->items[$sec][$ptype]->policy[$index] = $newRules[$newIndex]; + unset($this->items[$sec][$ptype]->policyMap[$oldPolicy]); + $this->items[$sec][$ptype]->policyMap[implode(self::DEFAULT_SEP, $newRules[$newIndex])] = $index; + $modifiedRuleIndex[$index] = [$oldIndex, $newIndex]; + $newIndex++; + } + + return true; + } + /** * Removes a policy rule from the model. * diff --git a/src/Persist/UpdatableAdapter.php b/src/Persist/UpdatableAdapter.php index 7b30ccb..febb262 100644 --- a/src/Persist/UpdatableAdapter.php +++ b/src/Persist/UpdatableAdapter.php @@ -21,4 +21,15 @@ interface UpdatableAdapter extends Adapter * @param string[] $newPolicy */ public function updatePolicy(string $sec, string $ptype, array $oldRule, array $newPolicy): void; -} \ No newline at end of file + + /** + * 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; +} diff --git a/tests/Unit/ManagementEnforcerTest.php b/tests/Unit/ManagementEnforcerTest.php index e152068..8ce9d64 100644 --- a/tests/Unit/ManagementEnforcerTest.php +++ b/tests/Unit/ManagementEnforcerTest.php @@ -5,6 +5,7 @@ use Casbin\Exceptions\BatchOperationException; use PHPUnit\Framework\TestCase; use Casbin\Enforcer; +use Casbin\Tests\Watcher\SampleWatcherEx; /** * ManagementEnforcerTest. @@ -98,6 +99,8 @@ public function testGetPolicyAPI() public function testModifyPolicyAPI() { $e = new Enforcer($this->modelAndPolicyPath . '/rbac_model.conf', $this->modelAndPolicyPath . '/rbac_policy.csv'); + $watcherEx = new SampleWatcherEx(); + $this->watcher = $watcherEx; $this->assertEquals($e->getPolicy(), [ ['alice', 'data1', 'read'], @@ -229,4 +232,36 @@ public function testUpdatePolicy() $this->assertFalse($e->hasPolicy('alice', 'data1', 'read')); $this->assertTrue($e->hasPolicy('alice', 'data1', 'write')); } + + public function testUpdatePolicies() + { + // p, alice, data1, read + // p, bob, data2, write + // p, data2_admin, data2, read + // p, data2_admin, data2, write + // + // g, alice, data2_admin + + $e = new Enforcer($this->modelAndPolicyPath . '/rbac_model.conf', $this->modelAndPolicyPath . '/rbac_policy.csv'); + + $this->assertTrue($e->hasPolicy('alice', 'data1', 'read')); + $this->assertFalse($e->hasPolicy('alice', 'data1', 'write')); + $this->assertTrue($e->hasPolicy('bob', 'data2', 'write')); + $this->assertFalse($e->hasPolicy('bob', 'data2', 'read')); + + $oldPolicies = [ + ['alice', 'data1', 'read'], + ['bob', 'data2', 'write'] + ]; + $newPolicies = [ + ['alice', 'data1', 'write'], + ['bob', 'data2', 'read'] + ]; + $e->updatePolicies($oldPolicies, $newPolicies); + + $this->assertFalse($e->hasPolicy('alice', 'data1', 'read')); + $this->assertTrue($e->hasPolicy('alice', 'data1', 'write')); + $this->assertFalse($e->hasPolicy('bob', 'data2', 'write')); + $this->assertTrue($e->hasPolicy('bob', 'data2', 'read')); + } } diff --git a/tests/Unit/Model/PolicyTest.php b/tests/Unit/Model/PolicyTest.php index d5c1517..bba316a 100644 --- a/tests/Unit/Model/PolicyTest.php +++ b/tests/Unit/Model/PolicyTest.php @@ -100,6 +100,48 @@ public function testUpdatePolicy() ], $m->getPolicy('p', 'p')); } + public function testUpdatePolicies() + { + $m = Model::newModelFromFile($this->modelAndPolicyPath . '/basic_model.conf'); + $rules = [ + ['alice', 'domain1', 'data1', 'read'], + ['alice', 'domain1', 'data2', 'read'], + ['bob', 'domain2', 'data1', 'write'], + ['bob', 'domain2', 'data2', 'write'], + ]; + + $m->addPolicies('p', 'p', $rules); + + $this->assertEquals($rules, $m->getPolicy('p', 'p')); + $this->assertFalse($m->hasPolicies('p', 'p', [ + ['alice', 'domain1', 'data1', 'write'], + ])); + + $oldRules = [ + ['alice', 'domain1', 'data1', 'read'], + ['alice', 'domain1', 'data2', 'read'] + ]; + $newRules = [ + ['alice', 'domain1', 'data1', 'write'], + ['alice', 'domain1', 'data2', 'write'] + ]; + $m->updatePolicies('p', 'p', $oldRules, $newRules); + + $this->assertEquals([ + ['alice', 'domain1', 'data1', 'write'], + ['alice', 'domain1', 'data2', 'write'], + ['bob', 'domain2', 'data1', 'write'], + ['bob', 'domain2', 'data2', 'write'], + ], $m->getPolicy('p', 'p')); + + // trigger callback of addPolicies + $oldRules = [ + ['alice', 'domain1', 'data1', 'write'], + ['alice', 'domain1', 'data2', 'read'] + ]; + $this->assertFalse($m->updatePolicies('p', 'p', $oldRules, $newRules)); + } + public function testRemovePolicy() { $m = new Model();