From 4be784944c64effea3a29a170a31dd9e41901147 Mon Sep 17 00:00:00 2001 From: rev3z <backuade@gmail.com> Date: Tue, 11 Jun 2024 22:00:54 +0800 Subject: [PATCH 1/2] feat: loading model from remote url --- 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 | 40 ++++++++++ src/Loaders/UrlLoader.php | 58 ++++++++++++++ tests/ModelLoaderTest.php | 121 +++++++++++++++++++++++++++++ 9 files changed, 336 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 @@ +<?php + +namespace Lauthz\Contracts; + + +use Casbin\Model\Model; + +interface ModelLoader +{ + /** + * Loads model definitions into the provided model object. + * + * @param Model $model + * @return void + */ + function loadModel(Model $model): void; +} \ No newline at end of file diff --git a/src/EnforcerManager.php b/src/EnforcerManager.php index aafa98b..dc5da35 100755 --- a/src/EnforcerManager.php +++ b/src/EnforcerManager.php @@ -7,6 +7,7 @@ use Casbin\Model\Model; use Casbin\Log\Log; use Lauthz\Contracts\Factory; +use Lauthz\Contracts\ModelLoader; use Lauthz\Models\Rule; use Illuminate\Support\Arr; use InvalidArgumentException; @@ -86,12 +87,9 @@ protected function resolve($name) } $model = new Model(); - $configType = Arr::get($config, 'model.config_type'); - if ('file' == $configType) { - $model->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 @@ +<?php + +namespace Lauthz\Loaders; + +use Casbin\Model\Model; +use Illuminate\Support\Arr; +use Lauthz\Contracts\ModelLoader; + +class FileLoader implements ModelLoader +{ + /** + * The path to the model file. + * + * @var string + */ + private $filePath; + + /** + * Constructor to initialize the file path. + * + * @param array $config + */ + public function __construct(array $config) + { + $this->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 @@ +<?php + +namespace Lauthz\Loaders; + +use Illuminate\Support\Arr; +use Lauthz\Contracts\Factory; +use InvalidArgumentException; + +class ModelLoaderFactory implements Factory +{ + /** + * Create a model loader from configuration. + * + * A model loader is responsible for a loading model from an arbitrary source. + * Developers can customize loading behavior by implementing + * the ModelLoader interface and specifying their custom class + * via 'model.config_loader_class' in the configuration. + * + * Built-in loader implementations include: + * - FileLoader: For loading model from file. + * - TextLoader: Suitable for model defined as a multi-line string. + * - UrlLoader: Handles model loading from URL. + * + * To utilize a built-in loader, set 'model.config_type' to match one of the above types. + * + * @param array $config + * @return \Lauthz\Contracts\ModelLoader + * @throws InvalidArgumentException + */ + public static function createFromConfig(array $config) { + $customLoader = Arr::get($config, 'model.config_loader_class', ''); + if (class_exists($customLoader)) { + return new $customLoader($config); + } + + $loaderType = Arr::get($config, 'model.config_type', ''); + switch ($loaderType) { + case 'file': + return new FileLoader($config); + case 'text': + return new TextLoader($config); + case 'url': + return new UrlLoader($config); + default: + throw new InvalidArgumentException("Unsupported model loader type: {$loaderType}"); + } + } +} \ No newline at end of file diff --git a/src/Loaders/TextLoader.php b/src/Loaders/TextLoader.php new file mode 100644 index 0000000..ff4fa78 --- /dev/null +++ b/src/Loaders/TextLoader.php @@ -0,0 +1,40 @@ +<?php + +namespace Lauthz\Loaders; + +use Casbin\Model\Model; +use Illuminate\Support\Arr; +use Lauthz\Contracts\ModelLoader; + +class TextLoader implements ModelLoader +{ + /** + * Model text. + * + * @var string + */ + private $text; + + /** + * Constructor to initialize the model text. + * + * @param array $config + */ + public function __construct(array $config) + { + $this->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 + { +// dd($this->text); + $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 @@ +<?php + +namespace Lauthz\Loaders; + +use Casbin\Model\Model; +use Illuminate\Support\Arr; +use Lauthz\Contracts\ModelLoader; +use RuntimeException; + +class UrlLoader implements ModelLoader +{ + /** + * The url to fetch the remote model string. + * + * @var string + */ + private $url; + + /** + * Constructor to initialize the url path. + * + * @param array $config + */ + public function __construct(array $config) + { + $this->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 @@ +<?php + +namespace Lauthz\Tests; + +use Lauthz\Facades\Enforcer; +use InvalidArgumentException; +use RuntimeException; + + +class ModelLoaderTest extends TestCase +{ + public function testUrlLoader(): void + { + $this->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 <<<EOT +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act +EOT; + } +} \ No newline at end of file From 96028609a9a04c3aad82b9498a3b814badf706ea Mon Sep 17 00:00:00 2001 From: Jon <techlee@qq.com> Date: Sun, 16 Jun 2024 01:01:02 +0800 Subject: [PATCH 2/2] Apply suggestions from code review --- src/Loaders/TextLoader.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Loaders/TextLoader.php b/src/Loaders/TextLoader.php index ff4fa78..5257a3d 100644 --- a/src/Loaders/TextLoader.php +++ b/src/Loaders/TextLoader.php @@ -34,7 +34,6 @@ public function __construct(array $config) */ public function loadModel(Model $model): void { -// dd($this->text); $model->loadModelFromText($this->text); } } \ No newline at end of file