From 259a389595985becdae80cf650ca3f6b67c56449 Mon Sep 17 00:00:00 2001 From: Pike Date: Sun, 16 Jun 2024 01:18:49 +0800 Subject: [PATCH] feat: loading model from remote url (#69) * feat: loading model from remote url * Apply suggestions from code review --------- Co-authored-by: Jon --- config/lauthz.php | 4 +- src/Contracts/ModelLoader.php | 17 ++++ src/EnforcerManager.php | 10 +-- src/LauthzServiceProvider.php | 6 ++ src/Loaders/FileLoader.php | 39 ++++++++++ src/Loaders/ModelLoaderFactory.php | 48 ++++++++++++ src/Loaders/TextLoader.php | 39 ++++++++++ src/Loaders/UrlLoader.php | 58 ++++++++++++++ tests/ModelLoaderTest.php | 121 +++++++++++++++++++++++++++++ 9 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 src/Contracts/ModelLoader.php create mode 100644 src/Loaders/FileLoader.php create mode 100644 src/Loaders/ModelLoaderFactory.php create mode 100644 src/Loaders/TextLoader.php create mode 100644 src/Loaders/UrlLoader.php create mode 100644 tests/ModelLoaderTest.php diff --git a/config/lauthz.php b/config/lauthz.php index 0cb16f8..b958495 100644 --- a/config/lauthz.php +++ b/config/lauthz.php @@ -11,12 +11,14 @@ * Casbin model setting. */ 'model' => [ - // Available Settings: "file", "text" + // Available Settings: "file", "text", "url" 'config_type' => 'file', 'config_file_path' => __DIR__ . DIRECTORY_SEPARATOR . 'lauthz-rbac-model.conf', 'config_text' => '', + + 'config_url' => '' ], /* diff --git a/src/Contracts/ModelLoader.php b/src/Contracts/ModelLoader.php new file mode 100644 index 0000000..fc209c8 --- /dev/null +++ b/src/Contracts/ModelLoader.php @@ -0,0 +1,17 @@ +loadModel(Arr::get($config, 'model.config_file_path', '')); - } elseif ('text' == $configType) { - $model->loadModelFromText(Arr::get($config, 'model.config_text', '')); - } + $loader = $this->app->make(ModelLoader::class, $config); + $loader->loadModel($model); + $adapter = Arr::get($config, 'adapter'); if (!is_null($adapter)) { $adapter = $this->app->make($adapter, [ diff --git a/src/LauthzServiceProvider.php b/src/LauthzServiceProvider.php index 56d1375..273f42b 100644 --- a/src/LauthzServiceProvider.php +++ b/src/LauthzServiceProvider.php @@ -3,6 +3,8 @@ namespace Lauthz; use Illuminate\Support\ServiceProvider; +use Lauthz\Contracts\ModelLoader; +use Lauthz\Loaders\ModelLoaderFactory; use Lauthz\Models\Rule; use Lauthz\Observers\RuleObserver; @@ -50,5 +52,9 @@ public function register() $this->app->singleton('enforcer', function ($app) { return new EnforcerManager($app); }); + + $this->app->bind(ModelLoader::class, function($app, $config) { + return ModelLoaderFactory::createFromConfig($config); + }); } } diff --git a/src/Loaders/FileLoader.php b/src/Loaders/FileLoader.php new file mode 100644 index 0000000..e2845c2 --- /dev/null +++ b/src/Loaders/FileLoader.php @@ -0,0 +1,39 @@ +filePath = Arr::get($config, 'model.config_file_path', ''); + } + + /** + * Loads model from file. + * + * @param Model $model + * @return void + * @throws \Casbin\Exceptions\CasbinException + */ + public function loadModel(Model $model): void + { + $model->loadModel($this->filePath); + } +} \ No newline at end of file diff --git a/src/Loaders/ModelLoaderFactory.php b/src/Loaders/ModelLoaderFactory.php new file mode 100644 index 0000000..8a7ef9f --- /dev/null +++ b/src/Loaders/ModelLoaderFactory.php @@ -0,0 +1,48 @@ +text = Arr::get($config, 'model.config_text', ''); + } + + /** + * Loads model from text. + * + * @param Model $model + * @return void + * @throws \Casbin\Exceptions\CasbinException + */ + public function loadModel(Model $model): void + { + $model->loadModelFromText($this->text); + } +} \ No newline at end of file diff --git a/src/Loaders/UrlLoader.php b/src/Loaders/UrlLoader.php new file mode 100644 index 0000000..f07c27c --- /dev/null +++ b/src/Loaders/UrlLoader.php @@ -0,0 +1,58 @@ +url = Arr::get($config, 'model.config_url', ''); + } + + /** + * Loads model from remote url. + * + * @param Model $model + * @return void + * @throws \Casbin\Exceptions\CasbinException + * @throws RuntimeException + */ + public function loadModel(Model $model): void + { + $contextOptions = [ + 'http' => [ + 'method' => 'GET', + 'header' => "Accept: text/plain\r\n", + 'timeout' => 3 + ] + ]; + + $context = stream_context_create($contextOptions); + $response = @file_get_contents($this->url, false, $context); + if ($response === false) { + $error = error_get_last(); + throw new RuntimeException( + "Failed to fetch remote model " . $this->url . ": " . $error['message'] + ); + } + + $model->loadModelFromText($response); + } +} \ No newline at end of file diff --git a/tests/ModelLoaderTest.php b/tests/ModelLoaderTest.php new file mode 100644 index 0000000..75a93f3 --- /dev/null +++ b/tests/ModelLoaderTest.php @@ -0,0 +1,121 @@ +initUrlConfig(); + + $this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); + + Enforcer::addPolicy('data_admin', 'data', 'read'); + Enforcer::addRoleForUser('alice', 'data_admin'); + $this->assertTrue(Enforcer::enforce('alice', 'data', 'read')); + } + + public function testTextLoader(): void + { + $this->initTextConfig(); + + Enforcer::addPolicy('data_admin', 'data', 'read'); + $this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); + $this->assertTrue(Enforcer::enforce('data_admin', 'data', 'read')); + } + + public function testFileLoader(): void + { + $this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); + + Enforcer::addPolicy('data_admin', 'data', 'read'); + Enforcer::addRoleForUser('alice', 'data_admin'); + $this->assertTrue(Enforcer::enforce('alice', 'data', 'read')); + } + + public function testCustomLoader(): void + { + $this->initCustomConfig(); + Enforcer::guard('second')->addPolicy('data_admin', 'data', 'read'); + $this->assertFalse(Enforcer::guard('second')->enforce('alice', 'data', 'read')); + $this->assertTrue(Enforcer::guard('second')->enforce('data_admin', 'data', 'read')); + } + + public function testMultipleLoader(): void + { + $this->testFileLoader(); + $this->testCustomLoader(); + } + + public function testEmptyModel(): void + { + Enforcer::shouldUse('third'); + $this->expectException(InvalidArgumentException::class); + $this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); + } + + public function testEmptyLoaderType(): void + { + $this->app['config']->set('lauthz.basic.model.config_type', ''); + $this->expectException(InvalidArgumentException::class); + + $this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); + } + + public function testBadUlrConnection(): void + { + $this->initUrlConfig(); + $this->app['config']->set('lauthz.basic.model.config_url', 'http://filenoexists'); + $this->expectException(RuntimeException::class); + + $this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); + } + + protected function initUrlConfig(): void + { + $this->app['config']->set('lauthz.basic.model.config_type', 'url'); + $this->app['config']->set( + 'lauthz.basic.model.config_url', + 'https://raw.githubusercontent.com/casbin/casbin/master/examples/rbac_model.conf' + ); + } + + protected function initTextConfig(): void + { + $this->app['config']->set('lauthz.basic.model.config_type', 'text'); + $this->app['config']->set( + 'lauthz.basic.model.config_text', + $this->getModelText() + ); + } + + protected function initCustomConfig(): void { + $this->app['config']->set('lauthz.second.model.config_loader_class', '\Lauthz\Loaders\TextLoader'); + $this->app['config']->set( + 'lauthz.second.model.config_text', + $this->getModelText() + ); + } + + protected function getModelText(): string + { + return <<