Skip to content

Commit

Permalink
feat: loading model from remote url (#69)
Browse files Browse the repository at this point in the history
* feat: loading model from remote url

* Apply suggestions from code review

---------

Co-authored-by: Jon <[email protected]>
  • Loading branch information
rev3z and leeqvip authored Jun 15, 2024
1 parent 783c401 commit 259a389
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 7 deletions.
4 changes: 3 additions & 1 deletion config/lauthz.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => ''
],

/*
Expand Down
17 changes: 17 additions & 0 deletions src/Contracts/ModelLoader.php
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 4 additions & 6 deletions src/EnforcerManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, [
Expand Down
6 changes: 6 additions & 0 deletions src/LauthzServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
});
}
}
39 changes: 39 additions & 0 deletions src/Loaders/FileLoader.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
48 changes: 48 additions & 0 deletions src/Loaders/ModelLoaderFactory.php
Original file line number Diff line number Diff line change
@@ -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}");
}
}
}
39 changes: 39 additions & 0 deletions src/Loaders/TextLoader.php
Original file line number Diff line number Diff line change
@@ -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 changes: 58 additions & 0 deletions src/Loaders/UrlLoader.php
Original file line number Diff line number Diff line change
@@ -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 changes: 121 additions & 0 deletions tests/ModelLoaderTest.php
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 259a389

Please sign in to comment.