Skip to content

Commit 259a389

Browse files
rev3zleeqvip
andauthored
feat: loading model from remote url (#69)
* feat: loading model from remote url * Apply suggestions from code review --------- Co-authored-by: Jon <[email protected]>
1 parent 783c401 commit 259a389

9 files changed

+335
-7
lines changed

Diff for: config/lauthz.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
* Casbin model setting.
1212
*/
1313
'model' => [
14-
// Available Settings: "file", "text"
14+
// Available Settings: "file", "text", "url"
1515
'config_type' => 'file',
1616

1717
'config_file_path' => __DIR__ . DIRECTORY_SEPARATOR . 'lauthz-rbac-model.conf',
1818

1919
'config_text' => '',
20+
21+
'config_url' => ''
2022
],
2123

2224
/*

Diff for: src/Contracts/ModelLoader.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Lauthz\Contracts;
4+
5+
6+
use Casbin\Model\Model;
7+
8+
interface ModelLoader
9+
{
10+
/**
11+
* Loads model definitions into the provided model object.
12+
*
13+
* @param Model $model
14+
* @return void
15+
*/
16+
function loadModel(Model $model): void;
17+
}

Diff for: src/EnforcerManager.php

+4-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Casbin\Model\Model;
88
use Casbin\Log\Log;
99
use Lauthz\Contracts\Factory;
10+
use Lauthz\Contracts\ModelLoader;
1011
use Lauthz\Models\Rule;
1112
use Illuminate\Support\Arr;
1213
use InvalidArgumentException;
@@ -86,12 +87,9 @@ protected function resolve($name)
8687
}
8788

8889
$model = new Model();
89-
$configType = Arr::get($config, 'model.config_type');
90-
if ('file' == $configType) {
91-
$model->loadModel(Arr::get($config, 'model.config_file_path', ''));
92-
} elseif ('text' == $configType) {
93-
$model->loadModelFromText(Arr::get($config, 'model.config_text', ''));
94-
}
90+
$loader = $this->app->make(ModelLoader::class, $config);
91+
$loader->loadModel($model);
92+
9593
$adapter = Arr::get($config, 'adapter');
9694
if (!is_null($adapter)) {
9795
$adapter = $this->app->make($adapter, [

Diff for: src/LauthzServiceProvider.php

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Lauthz;
44

55
use Illuminate\Support\ServiceProvider;
6+
use Lauthz\Contracts\ModelLoader;
7+
use Lauthz\Loaders\ModelLoaderFactory;
68
use Lauthz\Models\Rule;
79
use Lauthz\Observers\RuleObserver;
810

@@ -50,5 +52,9 @@ public function register()
5052
$this->app->singleton('enforcer', function ($app) {
5153
return new EnforcerManager($app);
5254
});
55+
56+
$this->app->bind(ModelLoader::class, function($app, $config) {
57+
return ModelLoaderFactory::createFromConfig($config);
58+
});
5359
}
5460
}

Diff for: src/Loaders/FileLoader.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Lauthz\Loaders;
4+
5+
use Casbin\Model\Model;
6+
use Illuminate\Support\Arr;
7+
use Lauthz\Contracts\ModelLoader;
8+
9+
class FileLoader implements ModelLoader
10+
{
11+
/**
12+
* The path to the model file.
13+
*
14+
* @var string
15+
*/
16+
private $filePath;
17+
18+
/**
19+
* Constructor to initialize the file path.
20+
*
21+
* @param array $config
22+
*/
23+
public function __construct(array $config)
24+
{
25+
$this->filePath = Arr::get($config, 'model.config_file_path', '');
26+
}
27+
28+
/**
29+
* Loads model from file.
30+
*
31+
* @param Model $model
32+
* @return void
33+
* @throws \Casbin\Exceptions\CasbinException
34+
*/
35+
public function loadModel(Model $model): void
36+
{
37+
$model->loadModel($this->filePath);
38+
}
39+
}

Diff for: src/Loaders/ModelLoaderFactory.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Lauthz\Loaders;
4+
5+
use Illuminate\Support\Arr;
6+
use Lauthz\Contracts\Factory;
7+
use InvalidArgumentException;
8+
9+
class ModelLoaderFactory implements Factory
10+
{
11+
/**
12+
* Create a model loader from configuration.
13+
*
14+
* A model loader is responsible for a loading model from an arbitrary source.
15+
* Developers can customize loading behavior by implementing
16+
* the ModelLoader interface and specifying their custom class
17+
* via 'model.config_loader_class' in the configuration.
18+
*
19+
* Built-in loader implementations include:
20+
* - FileLoader: For loading model from file.
21+
* - TextLoader: Suitable for model defined as a multi-line string.
22+
* - UrlLoader: Handles model loading from URL.
23+
*
24+
* To utilize a built-in loader, set 'model.config_type' to match one of the above types.
25+
*
26+
* @param array $config
27+
* @return \Lauthz\Contracts\ModelLoader
28+
* @throws InvalidArgumentException
29+
*/
30+
public static function createFromConfig(array $config) {
31+
$customLoader = Arr::get($config, 'model.config_loader_class', '');
32+
if (class_exists($customLoader)) {
33+
return new $customLoader($config);
34+
}
35+
36+
$loaderType = Arr::get($config, 'model.config_type', '');
37+
switch ($loaderType) {
38+
case 'file':
39+
return new FileLoader($config);
40+
case 'text':
41+
return new TextLoader($config);
42+
case 'url':
43+
return new UrlLoader($config);
44+
default:
45+
throw new InvalidArgumentException("Unsupported model loader type: {$loaderType}");
46+
}
47+
}
48+
}

Diff for: src/Loaders/TextLoader.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Lauthz\Loaders;
4+
5+
use Casbin\Model\Model;
6+
use Illuminate\Support\Arr;
7+
use Lauthz\Contracts\ModelLoader;
8+
9+
class TextLoader implements ModelLoader
10+
{
11+
/**
12+
* Model text.
13+
*
14+
* @var string
15+
*/
16+
private $text;
17+
18+
/**
19+
* Constructor to initialize the model text.
20+
*
21+
* @param array $config
22+
*/
23+
public function __construct(array $config)
24+
{
25+
$this->text = Arr::get($config, 'model.config_text', '');
26+
}
27+
28+
/**
29+
* Loads model from text.
30+
*
31+
* @param Model $model
32+
* @return void
33+
* @throws \Casbin\Exceptions\CasbinException
34+
*/
35+
public function loadModel(Model $model): void
36+
{
37+
$model->loadModelFromText($this->text);
38+
}
39+
}

Diff for: src/Loaders/UrlLoader.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Lauthz\Loaders;
4+
5+
use Casbin\Model\Model;
6+
use Illuminate\Support\Arr;
7+
use Lauthz\Contracts\ModelLoader;
8+
use RuntimeException;
9+
10+
class UrlLoader implements ModelLoader
11+
{
12+
/**
13+
* The url to fetch the remote model string.
14+
*
15+
* @var string
16+
*/
17+
private $url;
18+
19+
/**
20+
* Constructor to initialize the url path.
21+
*
22+
* @param array $config
23+
*/
24+
public function __construct(array $config)
25+
{
26+
$this->url = Arr::get($config, 'model.config_url', '');
27+
}
28+
29+
/**
30+
* Loads model from remote url.
31+
*
32+
* @param Model $model
33+
* @return void
34+
* @throws \Casbin\Exceptions\CasbinException
35+
* @throws RuntimeException
36+
*/
37+
public function loadModel(Model $model): void
38+
{
39+
$contextOptions = [
40+
'http' => [
41+
'method' => 'GET',
42+
'header' => "Accept: text/plain\r\n",
43+
'timeout' => 3
44+
]
45+
];
46+
47+
$context = stream_context_create($contextOptions);
48+
$response = @file_get_contents($this->url, false, $context);
49+
if ($response === false) {
50+
$error = error_get_last();
51+
throw new RuntimeException(
52+
"Failed to fetch remote model " . $this->url . ": " . $error['message']
53+
);
54+
}
55+
56+
$model->loadModelFromText($response);
57+
}
58+
}

Diff for: tests/ModelLoaderTest.php

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
namespace Lauthz\Tests;
4+
5+
use Lauthz\Facades\Enforcer;
6+
use InvalidArgumentException;
7+
use RuntimeException;
8+
9+
10+
class ModelLoaderTest extends TestCase
11+
{
12+
public function testUrlLoader(): void
13+
{
14+
$this->initUrlConfig();
15+
16+
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
17+
18+
Enforcer::addPolicy('data_admin', 'data', 'read');
19+
Enforcer::addRoleForUser('alice', 'data_admin');
20+
$this->assertTrue(Enforcer::enforce('alice', 'data', 'read'));
21+
}
22+
23+
public function testTextLoader(): void
24+
{
25+
$this->initTextConfig();
26+
27+
Enforcer::addPolicy('data_admin', 'data', 'read');
28+
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
29+
$this->assertTrue(Enforcer::enforce('data_admin', 'data', 'read'));
30+
}
31+
32+
public function testFileLoader(): void
33+
{
34+
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
35+
36+
Enforcer::addPolicy('data_admin', 'data', 'read');
37+
Enforcer::addRoleForUser('alice', 'data_admin');
38+
$this->assertTrue(Enforcer::enforce('alice', 'data', 'read'));
39+
}
40+
41+
public function testCustomLoader(): void
42+
{
43+
$this->initCustomConfig();
44+
Enforcer::guard('second')->addPolicy('data_admin', 'data', 'read');
45+
$this->assertFalse(Enforcer::guard('second')->enforce('alice', 'data', 'read'));
46+
$this->assertTrue(Enforcer::guard('second')->enforce('data_admin', 'data', 'read'));
47+
}
48+
49+
public function testMultipleLoader(): void
50+
{
51+
$this->testFileLoader();
52+
$this->testCustomLoader();
53+
}
54+
55+
public function testEmptyModel(): void
56+
{
57+
Enforcer::shouldUse('third');
58+
$this->expectException(InvalidArgumentException::class);
59+
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
60+
}
61+
62+
public function testEmptyLoaderType(): void
63+
{
64+
$this->app['config']->set('lauthz.basic.model.config_type', '');
65+
$this->expectException(InvalidArgumentException::class);
66+
67+
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
68+
}
69+
70+
public function testBadUlrConnection(): void
71+
{
72+
$this->initUrlConfig();
73+
$this->app['config']->set('lauthz.basic.model.config_url', 'http://filenoexists');
74+
$this->expectException(RuntimeException::class);
75+
76+
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
77+
}
78+
79+
protected function initUrlConfig(): void
80+
{
81+
$this->app['config']->set('lauthz.basic.model.config_type', 'url');
82+
$this->app['config']->set(
83+
'lauthz.basic.model.config_url',
84+
'https://raw.githubusercontent.com/casbin/casbin/master/examples/rbac_model.conf'
85+
);
86+
}
87+
88+
protected function initTextConfig(): void
89+
{
90+
$this->app['config']->set('lauthz.basic.model.config_type', 'text');
91+
$this->app['config']->set(
92+
'lauthz.basic.model.config_text',
93+
$this->getModelText()
94+
);
95+
}
96+
97+
protected function initCustomConfig(): void {
98+
$this->app['config']->set('lauthz.second.model.config_loader_class', '\Lauthz\Loaders\TextLoader');
99+
$this->app['config']->set(
100+
'lauthz.second.model.config_text',
101+
$this->getModelText()
102+
);
103+
}
104+
105+
protected function getModelText(): string
106+
{
107+
return <<<EOT
108+
[request_definition]
109+
r = sub, obj, act
110+
111+
[policy_definition]
112+
p = sub, obj, act
113+
114+
[policy_effect]
115+
e = some(where (p.eft == allow))
116+
117+
[matchers]
118+
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
119+
EOT;
120+
}
121+
}

0 commit comments

Comments
 (0)