Merge branch 'master' of https://github.com/php-casbin/laravel-authz into feature-use-gates-1.0

This commit is contained in:
Dobando 2024-08-02 22:11:55 +08:00
commit 3a3132a09b
15 changed files with 345 additions and 108 deletions

View File

@ -279,14 +279,17 @@ Route::group(['middleware' => ['http_request']], function () {
### Using Gates ### Using Gates
You can use Laravel Gates to check if a user has a permission, provided that you have set an existing user instance as the currently authenticated user using `Auth::login`. See [Gates](https://laravel.com/docs/11.x/authorization#gates) for more details. You can use Laravel Gates to check if a user has a permission, provided that you have set an existing user instance as the currently authenticated user.
```php ```php
if(Gate::allows('enforcer', ['articles', 'read'])) { $user->can('articles,read');
// The user can read articles // For multiple enforcers
}; $user->can('articles,read', 'second');
// The methods cant, cannot, canAny, etc. also work
``` ```
If you require custom Laravel Gates, you can disable the automatic registration by setting `enabled_register_at_gates` to `false` in the lauthz file. After that, you can use `Gates::before` or `Gates::after` in your ServiceProvider to register custom Gates. See [Gates](https://laravel.com/docs/11.x/authorization#gates) for more details.
### Multiple enforcers ### Multiple enforcers
If you need multiple permission controls in your project, you can configure multiple enforcers. If you need multiple permission controls in your project, you can configure multiple enforcers.

View File

@ -6,6 +6,14 @@ return [
*/ */
'default' => 'basic', 'default' => 'basic',
/*
* Lauthz Localizer
*/
'localizer' => [
// changes whether enforcer will register at gates.
'enabled_register_at_gates' => true
],
'basic' => [ 'basic' => [
/* /*
* Casbin model setting. * Casbin model setting.

61
src/EnforcerLocalizer.php Normal file
View File

@ -0,0 +1,61 @@
<?php
namespace Lauthz;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Access\Gate;
use Lauthz\Facades\Enforcer;
class EnforcerLocalizer
{
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;
/**
* Create a new localizer instance.
*
* @param \Illuminate\Foundation\Application $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Register the localizer based on the configuration.
*/
public function register()
{
if ($this->app->config->get('lauthz.localizer.enabled_register_at_gates')) {
$this->registerAtGate();
}
}
/**
* Register the localizer at the gate.
*/
protected function registerAtGate()
{
$this->app->make(Gate::class)->before(function (Authorizable $user, string $ability, array $guards) {
/** @var \Illuminate\Contracts\Auth\Authenticatable $user */
$identifier = $user->getAuthIdentifier();
if (method_exists($user, 'getAuthzIdentifier')) {
/** @var \Lauthz\Tests\Models\User $user */
$identifier = $user->getAuthzIdentifier();
}
$identifier = strval($identifier);
$ability = explode(',', $ability);
if (empty($guards)) {
return Enforcer::enforce($identifier, ...$ability);
}
foreach ($guards as $guard) {
return Enforcer::guard($guard)->enforce($identifier, ...$ability);
}
});
}
}

View File

@ -7,10 +7,10 @@ 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;
use Lauthz\Loaders\ModelLoaderManager;
/** /**
* @mixin \Casbin\Enforcer * @mixin \Casbin\Enforcer
@ -87,7 +87,8 @@ class EnforcerManager implements Factory
} }
$model = new Model(); $model = new Model();
$loader = $this->app->make(ModelLoader::class, $config); $loader = $this->app->make(ModelLoaderManager::class);
$loader->initFromConfig($config);
$loader->loadModel($model); $loader->loadModel($model);
$adapter = Arr::get($config, 'adapter'); $adapter = Arr::get($config, 'adapter');

View File

@ -2,11 +2,9 @@
namespace Lauthz; namespace Lauthz;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Lauthz\Contracts\ModelLoader; use Lauthz\EnforcerLocalizer;
use Lauthz\Facades\Enforcer; use Lauthz\Loaders\ModelLoaderManager;
use Lauthz\Loaders\ModelLoaderFactory;
use Lauthz\Models\Rule; use Lauthz\Models\Rule;
use Lauthz\Observers\RuleObserver; use Lauthz\Observers\RuleObserver;
@ -34,6 +32,8 @@ class LauthzServiceProvider extends ServiceProvider
$this->mergeConfigFrom(__DIR__ . '/../config/lauthz.php', 'lauthz'); $this->mergeConfigFrom(__DIR__ . '/../config/lauthz.php', 'lauthz');
$this->bootObserver(); $this->bootObserver();
$this->registerLocalizer();
} }
/** /**
@ -55,11 +55,13 @@ class LauthzServiceProvider extends ServiceProvider
return new EnforcerManager($app); return new EnforcerManager($app);
}); });
$this->app->bind(ModelLoader::class, function($app, $config) { $this->app->singleton(ModelLoaderManager::class, function ($app) {
return ModelLoaderFactory::createFromConfig($config); return new ModelLoaderManager($app);
}); });
$this->registerGates(); $this->app->singleton(EnforcerLocalizer::class, function ($app) {
return new EnforcerLocalizer($app);
});
} }
/** /**
@ -67,16 +69,8 @@ class LauthzServiceProvider extends ServiceProvider
* *
* @return void * @return void
*/ */
protected function registerGates() protected function registerLocalizer()
{ {
Gate::define('enforcer', function ($user, ...$args) { $this->app->make(EnforcerLocalizer::class)->register();
$identifier = $user->getAuthIdentifier();
if (method_exists($user, 'getAuthzIdentifier')) {
$identifier = $user->getAuthzIdentifier();
}
$identifier = strval($identifier);
return Enforcer::enforce($identifier, ...$args);
});
} }
} }

View File

@ -1,48 +0,0 @@
<?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,108 @@
<?php
namespace Lauthz\Loaders;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Manager;
use InvalidArgumentException;
/**
* The model loader manager.
*
* A model loader is responsible for a loading model from an arbitrary source.
* Developers can customize loading behavior by implementing
* and register the custom loader in AppServiceProvider through `app(LoaderManager::class)->extend()`.
*
* 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 or custom loader, set 'model.config_type' in the configuration to match one of the above types.
*/
class ModelLoaderManager extends Manager
{
/**
* The array of the lauthz driver configuration.
*
* @var array
*/
protected $config;
/**
* Initialize configuration for the loader manager instance.
*
* @param array $config the lauthz driver configuration.
*/
public function initFromConfig(array $config)
{
$this->config = $config;
}
/**
* Get the default driver from the configuration.
*
* @return string The default driver name.
*/
public function getDefaultDriver()
{
return Arr::get($this->config, 'model.config_type', '');
}
/**
* Create a new TextLoader instance.
*
* @return TextLoader
*/
public function createTextDriver()
{
return new TextLoader($this->config);
}
/**
* Create a new UrlLoader instance.
*
* @return UrlLoader
*/
public function createUrlDriver()
{
return new UrlLoader($this->config);
}
/**
* Create a new FileLoader instance.
*
* @return FileLoader
*/
public function createFileDriver()
{
return new FileLoader($this->config);
}
/**
* Create a new driver instance.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
if(empty($driver)) {
throw new InvalidArgumentException('Unsupported empty model loader type.');
}
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
}
$method = 'create' . Str::studly($driver) . 'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
throw new InvalidArgumentException("Unsupported model loader type: {$driver}.");
}
}

View File

@ -31,6 +31,7 @@ class EnforcerMiddleware
$user = Auth::user(); $user = Auth::user();
$identifier = $user->getAuthIdentifier(); $identifier = $user->getAuthIdentifier();
if (method_exists($user, 'getAuthzIdentifier')) { if (method_exists($user, 'getAuthzIdentifier')) {
/** @var \Lauthz\Tests\Models\User $user */
$identifier = $user->getAuthzIdentifier(); $identifier = $user->getAuthzIdentifier();
} }
$identifier = strval($identifier); $identifier = strval($identifier);

View File

@ -2,10 +2,10 @@
namespace Lauthz\Tests; namespace Lauthz\Tests;
use Enforcer;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Casbin\Persist\Adapters\Filter; use Casbin\Persist\Adapters\Filter;
use Casbin\Exceptions\InvalidFilterTypeException; use Casbin\Exceptions\InvalidFilterTypeException;
use Lauthz\Facades\Enforcer;
class DatabaseAdapterTest extends TestCase class DatabaseAdapterTest extends TestCase
{ {
@ -309,7 +309,7 @@ class DatabaseAdapterTest extends TestCase
$this->assertEquals([ $this->assertEquals([
['bob', 'data2', 'write'] ['bob', 'data2', 'write']
], Enforcer::getPolicy()); ], Enforcer::getPolicy());
// Filter // Filter
$filter = new Filter(['v2'], ['read']); $filter = new Filter(['v2'], ['read']);
Enforcer::loadFilteredPolicy($filter); Enforcer::loadFilteredPolicy($filter);

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Lauthz\Tests\TestCase;
class EnforcerCustomLocalizerTest extends TestCase
{
use DatabaseMigrations;
public function testCustomRegisterAtGatesBefore()
{
$user = $this->user("alice");
$this->assertFalse($user->can('data3,read'));
app(Gate::class)->before(function () {
return true;
});
$this->assertTrue($user->can('data3,read'));
}
public function testCustomRegisterAtGatesDefine()
{
$user = $this->user("alice");
$this->assertFalse($user->can('data3,read'));
app(Gate::class)->define('data3,read', function () {
return true;
});
$this->assertTrue($user->can('data3,read'));
}
public function initConfig()
{
parent::initConfig();
$this->app['config']->set('lauthz.localizer.enabled_register_at_gates', false);
}
}

View File

@ -0,0 +1,69 @@
<?php
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Lauthz\Facades\Enforcer;
use Lauthz\Tests\TestCase;
class EnforcerLocalizerTest extends TestCase
{
use DatabaseMigrations;
public function testRegisterAtGates()
{
$user = $this->user('alice');
$this->assertTrue($user->can('data1,read'));
$this->assertFalse($user->can('data1,write'));
$this->assertFalse($user->cannot('data2,read'));
Enforcer::guard('second')->addPolicy('alice', 'data1', 'read');
$this->assertTrue($user->can('data1,read', 'second'));
$this->assertFalse($user->can('data3,read', 'second'));
}
public function testNotLogin()
{
$this->assertFalse(app(Gate::class)->allows('data1,read'));
$this->assertTrue(app(Gate::class)->forUser($this->user('alice'))->allows('data1,read'));
$this->assertFalse(app(Gate::class)->forUser($this->user('bob'))->allows('data1,read'));
}
public function testAfterLogin()
{
$this->login('alice');
$this->assertTrue(app(Gate::class)->allows('data1,read'));
$this->assertTrue(app(Gate::class)->allows('data2,read'));
$this->assertTrue(app(Gate::class)->allows('data2,write'));
$this->login('bob');
$this->assertFalse(app(Gate::class)->allows('data1,read'));
$this->assertTrue(app(Gate::class)->allows('data2,write'));
}
public function initConfig()
{
parent::initConfig();
$this->app['config']->set('lauthz.second.model.config_type', 'text');
$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;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Lauthz\Tests;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Gate;
class GatesAuthorizationTest extends TestCase
{
use DatabaseMigrations;
public function testNotLogin()
{
$this->assertFalse(Gate::allows('enforcer', ['data1', 'read']));
}
public function testAfterLogin()
{
$this->login('alice');
$this->assertTrue(Gate::allows('enforcer', ['data1', 'read']));
$this->assertTrue(Gate::allows('enforcer', ['data2', 'read']));
$this->assertTrue(Gate::allows('enforcer', ['data2', 'write']));
$this->login('bob');
$this->assertFalse(Gate::allows('enforcer', ['data1', 'read']));
$this->assertTrue(Gate::allows('enforcer', ['data2', 'write']));
}
}

View File

@ -3,6 +3,7 @@
namespace Lauthz\Tests; namespace Lauthz\Tests;
use Lauthz\Facades\Enforcer; use Lauthz\Facades\Enforcer;
use Lauthz\Loaders\ModelLoaderManager;
use InvalidArgumentException; use InvalidArgumentException;
use RuntimeException; use RuntimeException;
@ -67,7 +68,15 @@ class ModelLoaderTest extends TestCase
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read')); $this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
} }
public function testBadUlrConnection(): void public function testNotExistLoaderType(): void
{
$this->app['config']->set('lauthz.basic.model.config_type', 'not_exist');
$this->expectException(InvalidArgumentException::class);
$this->assertFalse(Enforcer::enforce('alice', 'data', 'read'));
}
public function testBadUrlConnection(): void
{ {
$this->initUrlConfig(); $this->initUrlConfig();
$this->app['config']->set('lauthz.basic.model.config_url', 'http://filenoexists'); $this->app['config']->set('lauthz.basic.model.config_url', 'http://filenoexists');
@ -94,12 +103,20 @@ class ModelLoaderTest extends TestCase
); );
} }
protected function initCustomConfig(): void { 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_type', 'custom');
$this->app['config']->set( $this->app['config']->set(
'lauthz.second.model.config_text', 'lauthz.second.model.config_text',
$this->getModelText() $this->getModelText()
); );
$config = $this->app['config']->get('lauthz.second');
$loader = $this->app->make(ModelLoaderManager::class);
$loader->extend('custom', function () use ($config) {
return new \Lauthz\Loaders\TextLoader($config);
});
} }
protected function getModelText(): string protected function getModelText(): string
@ -118,4 +135,4 @@ e = some(where (p.eft == allow))
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
EOT; EOT;
} }
} }

View File

@ -5,6 +5,7 @@ namespace Lauthz\Tests;
use Lauthz\Middlewares\RequestMiddleware; use Lauthz\Middlewares\RequestMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Lauthz\Facades\Enforcer;
use Lauthz\Models\Rule; use Lauthz\Models\Rule;
class RequestMiddlewareTest extends TestCase class RequestMiddlewareTest extends TestCase
@ -34,11 +35,19 @@ class RequestMiddlewareTest extends TestCase
$this->assertEquals($this->middleware(Request::create('/foo1/123', 'PUT')), 'Unauthorized Exception'); $this->assertEquals($this->middleware(Request::create('/foo1/123', 'PUT')), 'Unauthorized Exception');
$this->assertEquals($this->middleware(Request::create('/proxy', 'GET')), 'Unauthorized Exception'); $this->assertEquals($this->middleware(Request::create('/proxy', 'GET')), 'Unauthorized Exception');
Enforcer::guard('second')->addPolicy('alice', '/foo1/*', '(GET|POST)');
$this->assertEquals($this->middleware(Request::create('/foo1/123', 'GET'), 'second'), 200);
$this->assertEquals($this->middleware(Request::create('/foo1/123', 'POST'), 'second'), 200);
$this->assertEquals($this->middleware(Request::create('/foo1/123', 'PUT'), 'second'), 'Unauthorized Exception');
$this->assertEquals($this->middleware(Request::create('/proxy', 'GET'), 'second'), 'Unauthorized Exception');
} }
protected function middleware($request) protected function middleware($request, ...$guards)
{ {
return parent::runMiddleware(RequestMiddleware::class, $request); return parent::runMiddleware(RequestMiddleware::class, $request, ...$guards);
} }
protected function initConfig() protected function initConfig()
@ -62,6 +71,8 @@ e = some(where (p.eft == allow))
m = g(r.sub, p.sub) && r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) m = g(r.sub, p.sub) && r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
EOT; EOT;
$this->app['config']->set('lauthz.basic.model.config_text', $text); $this->app['config']->set('lauthz.basic.model.config_text', $text);
$this->app['config']->set('lauthz.second.model.config_type', 'text');
$this->app['config']->set('lauthz.second.model.config_text', $text);
} }
protected function initTable() protected function initTable()

View File

@ -27,9 +27,9 @@ abstract class TestCase extends BaseTestCase
}); });
$this->app->make(Kernel::class)->bootstrap(); $this->app->make(Kernel::class)->bootstrap();
$this->initConfig();
$this->app->register(\Lauthz\LauthzServiceProvider::class); $this->app->register(\Lauthz\LauthzServiceProvider::class);
$this->initConfig();
$this->artisan('vendor:publish', ['--provider' => 'Lauthz\LauthzServiceProvider']); $this->artisan('vendor:publish', ['--provider' => 'Lauthz\LauthzServiceProvider']);
$this->artisan('migrate', ['--force' => true]); $this->artisan('migrate', ['--force' => true]);