diff --git a/README.md b/README.md index 1a40335..cd8f53a 100755 --- a/README.md +++ b/README.md @@ -279,14 +279,17 @@ 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 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 -if(Gate::allows('enforcer', ['articles', 'read'])) { - // The user can read articles -}; +$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/EnforcerManager.php b/src/EnforcerManager.php index dc5da35..0c08164 100755 --- a/src/EnforcerManager.php +++ b/src/EnforcerManager.php @@ -7,10 +7,10 @@ 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; +use Lauthz\Loaders\ModelLoaderManager; /** * @mixin \Casbin\Enforcer @@ -87,7 +87,8 @@ protected function resolve($name) } $model = new Model(); - $loader = $this->app->make(ModelLoader::class, $config); + $loader = $this->app->make(ModelLoaderManager::class); + $loader->initFromConfig($config); $loader->loadModel($model); $adapter = Arr::get($config, 'adapter'); diff --git a/src/LauthzServiceProvider.php b/src/LauthzServiceProvider.php index 7221d2f..7d60cfe 100644 --- a/src/LauthzServiceProvider.php +++ b/src/LauthzServiceProvider.php @@ -2,11 +2,9 @@ namespace Lauthz; -use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; -use Lauthz\Contracts\ModelLoader; -use Lauthz\Facades\Enforcer; -use Lauthz\Loaders\ModelLoaderFactory; +use Lauthz\EnforcerLocalizer; +use Lauthz\Loaders\ModelLoaderManager; use Lauthz\Models\Rule; use Lauthz\Observers\RuleObserver; @@ -34,6 +32,8 @@ public function boot() $this->mergeConfigFrom(__DIR__ . '/../config/lauthz.php', 'lauthz'); $this->bootObserver(); + + $this->registerLocalizer(); } /** @@ -55,11 +55,13 @@ public function register() return new EnforcerManager($app); }); - $this->app->bind(ModelLoader::class, function($app, $config) { - return ModelLoaderFactory::createFromConfig($config); + $this->app->singleton(ModelLoaderManager::class, function ($app) { + return new ModelLoaderManager($app); }); - $this->registerGates(); + $this->app->singleton(EnforcerLocalizer::class, function ($app) { + return new EnforcerLocalizer($app); + }); } /** @@ -67,16 +69,8 @@ public function register() * * @return void */ - protected function registerGates() + protected function registerLocalizer() { - Gate::define('enforcer', function ($user, ...$args) { - $identifier = $user->getAuthIdentifier(); - if (method_exists($user, 'getAuthzIdentifier')) { - $identifier = $user->getAuthzIdentifier(); - } - $identifier = strval($identifier); - - return Enforcer::enforce($identifier, ...$args); - }); + $this->app->make(EnforcerLocalizer::class)->register(); } } diff --git a/src/Loaders/ModelLoaderFactory.php b/src/Loaders/ModelLoaderFactory.php deleted file mode 100644 index 8a7ef9f..0000000 --- a/src/Loaders/ModelLoaderFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -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}."); + } +} 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 195c35b..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 { @@ -309,7 +309,7 @@ public function testLoadFilteredPolicy() $this->assertEquals([ ['bob', 'data2', 'write'] ], Enforcer::getPolicy()); - + // Filter $filter = new Filter(['v2'], ['read']); Enforcer::loadFilteredPolicy($filter); 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 <<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'])); - } -} diff --git a/tests/ModelLoaderTest.php b/tests/ModelLoaderTest.php index 75a93f3..4d673d5 100644 --- a/tests/ModelLoaderTest.php +++ b/tests/ModelLoaderTest.php @@ -3,6 +3,7 @@ namespace Lauthz\Tests; use Lauthz\Facades\Enforcer; +use Lauthz\Loaders\ModelLoaderManager; use InvalidArgumentException; use RuntimeException; @@ -67,7 +68,15 @@ public function testEmptyLoaderType(): void $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->app['config']->set('lauthz.basic.model.config_url', 'http://filenoexists'); @@ -94,12 +103,20 @@ protected function initTextConfig(): void ); } - protected function initCustomConfig(): void { - $this->app['config']->set('lauthz.second.model.config_loader_class', '\Lauthz\Loaders\TextLoader'); + protected function initCustomConfig(): void + { + $this->app['config']->set('lauthz.second.model.config_type', 'custom'); $this->app['config']->set( 'lauthz.second.model.config_text', $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 @@ -118,4 +135,4 @@ protected function getModelText(): string m = r.sub == p.sub && r.obj == p.obj && r.act == p.act EOT; } -} \ No newline at end of file +} diff --git a/tests/RequestMiddlewareTest.php b/tests/RequestMiddlewareTest.php index b4e6088..0805952 100644 --- a/tests/RequestMiddlewareTest.php +++ b/tests/RequestMiddlewareTest.php @@ -5,6 +5,7 @@ use Lauthz\Middlewares\RequestMiddleware; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Http\Request; +use Lauthz\Facades\Enforcer; use Lauthz\Models\Rule; class RequestMiddlewareTest extends TestCase @@ -34,11 +35,19 @@ public function testAfterLogin() $this->assertEquals($this->middleware(Request::create('/foo1/123', 'PUT')), '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() @@ -62,6 +71,8 @@ protected function initConfig() m = g(r.sub, p.sub) && r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act) EOT; $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()