feat: loading model from remote url (#69)

* feat: loading model from remote url

* Apply suggestions from code review

---------

Co-authored-by: Jon <techlee@qq.com>
This commit is contained in:
Pike 2024-06-16 01:18:49 +08:00 committed by GitHub
parent 783c4017b0
commit 259a389595
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 335 additions and 7 deletions

View File

@ -11,12 +11,14 @@ return [
* Casbin model setting. * Casbin model setting.
*/ */
'model' => [ 'model' => [
// Available Settings: "file", "text" // Available Settings: "file", "text", "url"
'config_type' => 'file', 'config_type' => 'file',
'config_file_path' => __DIR__ . DIRECTORY_SEPARATOR . 'lauthz-rbac-model.conf', 'config_file_path' => __DIR__ . DIRECTORY_SEPARATOR . 'lauthz-rbac-model.conf',
'config_text' => '', 'config_text' => '',
'config_url' => ''
], ],
/* /*

View File

@ -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;
}

View File

@ -7,6 +7,7 @@ use Casbin\Enforcer;
use Casbin\Model\Model; use Casbin\Model\Model;
use Casbin\Log\Log; use Casbin\Log\Log;
use Lauthz\Contracts\Factory; use Lauthz\Contracts\Factory;
use Lauthz\Contracts\ModelLoader;
use Lauthz\Models\Rule; use Lauthz\Models\Rule;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use InvalidArgumentException; use InvalidArgumentException;
@ -86,12 +87,9 @@ class EnforcerManager implements Factory
} }
$model = new Model(); $model = new Model();
$configType = Arr::get($config, 'model.config_type'); $loader = $this->app->make(ModelLoader::class, $config);
if ('file' == $configType) { $loader->loadModel($model);
$model->loadModel(Arr::get($config, 'model.config_file_path', ''));
} elseif ('text' == $configType) {
$model->loadModelFromText(Arr::get($config, 'model.config_text', ''));
}
$adapter = Arr::get($config, 'adapter'); $adapter = Arr::get($config, 'adapter');
if (!is_null($adapter)) { if (!is_null($adapter)) {
$adapter = $this->app->make($adapter, [ $adapter = $this->app->make($adapter, [

View File

@ -3,6 +3,8 @@
namespace Lauthz; namespace Lauthz;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Lauthz\Contracts\ModelLoader;
use Lauthz\Loaders\ModelLoaderFactory;
use Lauthz\Models\Rule; use Lauthz\Models\Rule;
use Lauthz\Observers\RuleObserver; use Lauthz\Observers\RuleObserver;
@ -50,5 +52,9 @@ class LauthzServiceProvider extends ServiceProvider
$this->app->singleton('enforcer', function ($app) { $this->app->singleton('enforcer', function ($app) {
return new EnforcerManager($app); return new EnforcerManager($app);
}); });
$this->app->bind(ModelLoader::class, function($app, $config) {
return ModelLoaderFactory::createFromConfig($config);
});
} }
} }

View File

@ -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);
}
}

View File

@ -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}");
}
}
}

View File

@ -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);
}
}

58
src/Loaders/UrlLoader.php Normal file
View File

@ -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);
}
}

121
tests/ModelLoaderTest.php Normal file
View File

@ -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;
}
}