Compare commits

...

4 Commits

Author SHA1 Message Date
Dobando a050a9c222
BREAKING CHANGE: upgrade to PHP 8.0 and PHP-Casbin 4.0 (#75) 2024-10-28 23:22:19 +08:00
Dobando 434ee8003f
docs: fix CI badge and update TOC in README.md (#74)
* feat: Integrate Laravel's built-in authorization Gates

- Integrate Laravel's built-in authorization Gates (#70)
- Added guidance for Gates in README.md

* docs: fix CI badge and update TOC in README.md
2024-08-08 10:23:35 +08:00
Dobando 11ffe28750
feat: Integrate Laravel's built-in authorization Gates (#73)
- Integrate Laravel's built-in authorization Gates (#70)
- Added guidance for Gates in README.md
2024-08-07 23:18:59 +08:00
Dobando 4d49aef1ab
feat: use Laravel's built-in Manager class (#72)
- Use Laravel's built-in abstract Manager class instead of ModelLoaderFactory (#71)
2024-07-05 19:16:47 +08:00
17 changed files with 375 additions and 146 deletions

View File

@ -27,73 +27,7 @@ jobs:
# laravel: [ ]
# stability: [ prefer-lowest, prefer-stable ]
include:
# Laravel 5.5
- php: 7.1
laravel: 5.5.*
phpunit: ~6.0
- php: 7.2
laravel: 5.5.*
phpunit: ~6.0
- php: 7.3
laravel: 5.5.*
phpunit: ~6.0
# Laravel 5.6
- php: 7.1
laravel: 5.6.*
phpunit: ~7.0
- php: 7.2
laravel: 5.6.*
phpunit: ~7.0
- php: 7.3
laravel: 5.6.*
phpunit: ~7.0
# Laravel 5.7
- php: 7.1
laravel: 5.7.*
phpunit: ~7.5
- php: 7.2
laravel: 5.7.*
phpunit: ~7.5
- php: 7.3
laravel: 5.7.*
phpunit: ~7.5
# Laravel 5.8
- php: 7.1
laravel: 5.8.*
phpunit: ~7.5
- php: 7.2
laravel: 5.8.*
phpunit: ~8.0
- php: 7.3
laravel: 5.8.*
phpunit: ~8.0
# Laravel 6.x
- php: 7.2
laravel: 6.*
phpunit: ~8.0
- php: 7.3
laravel: 6.*
phpunit: ~8.0
# Laravel 7.x
- php: 7.3
laravel: 7.*
phpunit: ~9.0
- php: 7.4
laravel: 7.*
phpunit: ~9.0
# Laravel 8.x
- php: 7.3
laravel: 8.*
phpunit: ~9.0
- php: 7.4
laravel: 8.*
phpunit: ~9.0
- php: 8.0
laravel: 8.*
phpunit: ~9.0

View File

@ -8,7 +8,7 @@
<p align="center">
<a href="https://github.com/php-casbin/laravel-authz/actions">
<img src="https://github.com/php-casbin/laravel-authz/workflows/build/badge.svg?branch=master" alt="Build Status">
<img src="https://github.com/php-casbin/laravel-authz/actions/workflows/build.yml/badge.svg?branch=master" alt="Build Status">
</a>
<a href="https://coveralls.io/github/php-casbin/laravel-authz">
<img src="https://coveralls.io/repos/github/php-casbin/laravel-authz/badge.svg" alt="Coverage Status">
@ -35,6 +35,7 @@ All you need to learn to use `Casbin` first.
* [Using a middleware](#using-a-middleware)
* [basic Enforcer Middleware](#basic-enforcer-middleware)
* [HTTP Request Middleware ( RESTful is also supported )](#http-request-middleware--restful-is-also-supported-)
* [Using Gates](#using-gates)
* [Multiple enforcers](#multiple-enforcers)
* [Using artisan commands](#using-artisan-commands)
* [Cache](#using-cache)
@ -277,6 +278,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.

View File

@ -20,18 +20,17 @@
],
"license": "Apache-2.0",
"require": {
"php": ">=7.1.0",
"illuminate/support": "~5.5|~6.0|~7.0|~8.0|~9.0|~10.0|~11.0",
"illuminate/database": "~5.5|~6.0|~7.0|~8.0|~9.0|~10.0|~11.0",
"illuminate/console": "~5.5|~6.0|~7.0|~8.0|~9.0|~10.0|~11.0",
"casbin/casbin": "~3.1",
"casbin/psr3-bridge": "^1.1"
"php": ">=8.0",
"illuminate/support": "~8.0|~9.0|~10.0|~11.0",
"illuminate/database": "~8.0|~9.0|~10.0|~11.0",
"illuminate/console": "~8.0|~9.0|~10.0|~11.0",
"casbin/casbin": "~4.0"
},
"require-dev": {
"phpunit/phpunit": "~7.0|~8.0|~9.0|~10.5",
"php-coveralls/php-coveralls": "^2.4",
"phpunit/phpunit": "~9.0|~10.5",
"php-coveralls/php-coveralls": "^2.7",
"mockery/mockery": "^1.0",
"laravel/laravel": "~5.5|~6.0|~7.0|~8.0|~9.0|~10.0|~11.0"
"laravel/laravel": "~9.0|~10.0|~11.0"
},
"autoload": {
"psr-4": {

View File

@ -6,6 +6,14 @@ return [
*/
'default' => 'basic',
/*
* Lauthz Localizer
*/
'localizer' => [
// changes whether enforcer will register at gates.
'enabled_register_at_gates' => true
],
'basic' => [
/*
* 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

@ -2,15 +2,15 @@
namespace Lauthz;
use Casbin\Bridge\Logger\LoggerBridge;
use Casbin\Enforcer;
use Casbin\Model\Model;
use Casbin\Log\Log;
use Casbin\Log\Logger\DefaultLogger;
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
@ -80,14 +80,15 @@ class EnforcerManager implements Factory
if ($logger = Arr::get($config, 'log.logger')) {
if (is_string($logger)) {
$logger = $this->app->make($logger);
$logger = new DefaultLogger($this->app->make($logger));
}
Log::setLogger(new LoggerBridge($logger));
Log::setLogger($logger);
}
$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');
@ -97,7 +98,7 @@ class EnforcerManager implements Factory
]);
}
return new Enforcer($model, $adapter, Arr::get($config, 'log.enabled', false));
return new Enforcer($model, $adapter, $logger, Arr::get($config, 'log.enabled', false));
}
/**

View File

@ -3,8 +3,8 @@
namespace Lauthz;
use Illuminate\Support\ServiceProvider;
use Lauthz\Contracts\ModelLoader;
use Lauthz\Loaders\ModelLoaderFactory;
use Lauthz\EnforcerLocalizer;
use Lauthz\Loaders\ModelLoaderManager;
use Lauthz\Models\Rule;
use Lauthz\Observers\RuleObserver;
@ -32,6 +32,8 @@ class LauthzServiceProvider extends ServiceProvider
$this->mergeConfigFrom(__DIR__ . '/../config/lauthz.php', 'lauthz');
$this->bootObserver();
$this->registerLocalizer();
}
/**
@ -53,8 +55,22 @@ class LauthzServiceProvider extends ServiceProvider
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->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();
}
}

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();
$identifier = $user->getAuthIdentifier();
if (method_exists($user, 'getAuthzIdentifier')) {
/** @var \Lauthz\Tests\Models\User $user */
$identifier = $user->getAuthzIdentifier();
}
$identifier = strval($identifier);

View File

@ -2,11 +2,9 @@
namespace Lauthz\Tests;
use Enforcer;
use Lauthz\Models\Rule;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Casbin\Persist\Adapters\Filter;
use Casbin\Exceptions\InvalidFilterTypeException;
use Lauthz\Facades\Enforcer;
class DatabaseAdapterForCacheTest extends TestCase
{

View File

@ -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 @@ class DatabaseAdapterTest extends TestCase
$this->assertEquals([
['bob', 'data2', 'write']
], Enforcer::getPolicy());
// Filter
$filter = new Filter(['v2'], ['read']);
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

@ -3,6 +3,7 @@
namespace Lauthz\Tests;
use Lauthz\Facades\Enforcer;
use Lauthz\Loaders\ModelLoaderManager;
use InvalidArgumentException;
use RuntimeException;
@ -67,7 +68,15 @@ class ModelLoaderTest extends TestCase
$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 @@ class ModelLoaderTest extends TestCase
);
}
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 @@ e = some(where (p.eft == allow))
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
EOT;
}
}
}

View File

@ -5,6 +5,7 @@ namespace Lauthz\Tests;
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 @@ class RequestMiddlewareTest extends TestCase
$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 @@ 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)
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()

View File

@ -27,9 +27,9 @@ abstract class TestCase extends BaseTestCase
});
$this->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]);