Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: loading model from remote url #69

Merged
merged 2 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
Loading