From 11ffe28750f22b0aec4bd86f6899d2c5bfc2a724 Mon Sep 17 00:00:00 2001 From: Dobando <33273950+Dobmod@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:18:59 +0800 Subject: [PATCH] feat: Integrate Laravel's built-in authorization Gates (#73) - Integrate Laravel's built-in authorization Gates (#70) - Added guidance for Gates in README.md --- README.md | 13 +++++ config/lauthz.php | 8 +++ src/EnforcerLocalizer.php | 61 +++++++++++++++++++++++ src/LauthzServiceProvider.php | 17 +++++++ src/Middlewares/EnforcerMiddleware.php | 1 + tests/DatabaseAdapterTest.php | 2 +- tests/EnforcerCustomLocalizerTest.php | 40 +++++++++++++++ tests/EnforcerLocalizerTest.php | 69 ++++++++++++++++++++++++++ tests/TestCase.php | 2 +- 9 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/EnforcerLocalizer.php create mode 100644 tests/EnforcerCustomLocalizerTest.php create mode 100644 tests/EnforcerLocalizerTest.php diff --git a/README.md b/README.md index cd2b3ed..cd8f53a 100755 --- a/README.md +++ b/README.md @@ -277,6 +277,19 @@ Route::group(['middleware' => ['http_request']], function () { }); ``` +### 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. + +```php +$user->can('articles,read'); +// 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 If you need multiple permission controls in your project, you can configure multiple enforcers. diff --git a/config/lauthz.php b/config/lauthz.php index b958495..617d6ff 100644 --- a/config/lauthz.php +++ b/config/lauthz.php @@ -6,6 +6,14 @@ */ 'default' => 'basic', + /* + * Lauthz Localizer + */ + 'localizer' => [ + // changes whether enforcer will register at gates. + 'enabled_register_at_gates' => true + ], + 'basic' => [ /* * Casbin model setting. diff --git a/src/EnforcerLocalizer.php b/src/EnforcerLocalizer.php new file mode 100644 index 0000000..81f4929 --- /dev/null +++ b/src/EnforcerLocalizer.php @@ -0,0 +1,61 @@ +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); + } + }); + } +} diff --git a/src/LauthzServiceProvider.php b/src/LauthzServiceProvider.php index 84f0fb9..7d60cfe 100644 --- a/src/LauthzServiceProvider.php +++ b/src/LauthzServiceProvider.php @@ -3,6 +3,7 @@ namespace Lauthz; use Illuminate\Support\ServiceProvider; +use Lauthz\EnforcerLocalizer; use Lauthz\Loaders\ModelLoaderManager; use Lauthz\Models\Rule; use Lauthz\Observers\RuleObserver; @@ -31,6 +32,8 @@ public function boot() $this->mergeConfigFrom(__DIR__ . '/../config/lauthz.php', 'lauthz'); $this->bootObserver(); + + $this->registerLocalizer(); } /** @@ -55,5 +58,19 @@ public function register() $this->app->singleton(ModelLoaderManager::class, function ($app) { return new ModelLoaderManager($app); }); + + $this->app->singleton(EnforcerLocalizer::class, function ($app) { + return new EnforcerLocalizer($app); + }); + } + + /** + * Register a gate that allows users to use Laravel's built-in Gate to call Enforcer. + * + * @return void + */ + protected function registerLocalizer() + { + $this->app->make(EnforcerLocalizer::class)->register(); } } diff --git a/src/Middlewares/EnforcerMiddleware.php b/src/Middlewares/EnforcerMiddleware.php index c4bcb0f..d651ca1 100644 --- a/src/Middlewares/EnforcerMiddleware.php +++ b/src/Middlewares/EnforcerMiddleware.php @@ -31,6 +31,7 @@ public function handle($request, Closure $next, ...$args) $user = Auth::user(); $identifier = $user->getAuthIdentifier(); if (method_exists($user, 'getAuthzIdentifier')) { + /** @var \Lauthz\Tests\Models\User $user */ $identifier = $user->getAuthzIdentifier(); } $identifier = strval($identifier); diff --git a/tests/DatabaseAdapterTest.php b/tests/DatabaseAdapterTest.php index 21b13c7..7b1fc8a 100644 --- a/tests/DatabaseAdapterTest.php +++ b/tests/DatabaseAdapterTest.php @@ -2,10 +2,10 @@ namespace Lauthz\Tests; -use Enforcer; use Illuminate\Foundation\Testing\DatabaseMigrations; use Casbin\Persist\Adapters\Filter; use Casbin\Exceptions\InvalidFilterTypeException; +use Lauthz\Facades\Enforcer; class DatabaseAdapterTest extends TestCase { diff --git a/tests/EnforcerCustomLocalizerTest.php b/tests/EnforcerCustomLocalizerTest.php new file mode 100644 index 0000000..bbaf9ee --- /dev/null +++ b/tests/EnforcerCustomLocalizerTest.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/tests/EnforcerLocalizerTest.php b/tests/EnforcerLocalizerTest.php new file mode 100644 index 0000000..5de3bcf --- /dev/null +++ b/tests/EnforcerLocalizerTest.php @@ -0,0 +1,69 @@ +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 <<app->make(Kernel::class)->bootstrap(); + $this->initConfig(); $this->app->register(\Lauthz\LauthzServiceProvider::class); - $this->initConfig(); $this->artisan('vendor:publish', ['--provider' => 'Lauthz\LauthzServiceProvider']); $this->artisan('migrate', ['--force' => true]);