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..5257a3d --- /dev/null +++ b/src/Loaders/TextLoader.php @@ -0,0 +1,39 @@ +<?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 + { + $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