Skip to content

Commit

Permalink
Add group and permission filters.
Browse files Browse the repository at this point in the history
  • Loading branch information
lonnieezell committed Nov 30, 2022
1 parent f7fbfae commit 714bdd1
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 23 deletions.
26 changes: 25 additions & 1 deletion docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
- [can()](#can)
- [inGroup()](#ingroup)
- [hasPermission()](#haspermission)
- [Authorizing via Filters](#authorizing-via-filters)
- [Authorizing via Routes](#authorizing-via-routes)
- [Managing User Permissions](#managing-user-permissions)
- [addPermission()](#addpermission)
- [removePermission()](#removepermission)
Expand Down Expand Up @@ -128,6 +130,28 @@ if (! $user->hasPermission('users.create')) {
}
```

#### Authorizing via Filters

You can restrict access to multiple routes through a [Controller Filter](https://codeigniter.com/user_guide/incoming/filters.html). One is provided for both restricting via groups the user belongs to, as well as which permission they need. The filters are automatically registered with the system undeer the `group` and `permission` aliases, respectively. You can define the protectections within `app/Config/Filters.php`:

```php
public $filters = [
'group:admin,superadmin' => ['before' => ['admin/*']],
'permission:users.manage' => ['before' => ['admin/users/*']],
];
```

#### Authorizing via Routes

The filters can also be used on a route or route group level:

```php
$routes->group('admin', ['filter' => 'group:admin,superadmin'], static function ($routes) {
$routes->resource('users');
});

```

## Managing User Permissions

Permissions can be granted on a user level as well as on a group level. Any user-level permissions granted will
Expand Down Expand Up @@ -199,7 +223,7 @@ $user->syncGroups('admin', 'beta');

#### getGroups()

Returns all groups this user is a part of.
Returns all groups this user is a part of.

```php
$user->getGroups();
Expand Down
21 changes: 11 additions & 10 deletions src/Filters/GroupFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,8 @@
class GroupFilter implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
* Ensures the user is logged in and a member of one or
* more groups as specified in the filter.
*
* @param array|null $arguments
*
Expand All @@ -38,14 +32,21 @@ public function before(RequestInterface $request, $arguments = null)
}

if (! auth()->loggedIn()) {
return redirect()->to('login');
return redirect()->route('login');
}

if (auth()->user()->inGroup(...$arguments)) {
return;
}

throw GroupException::forUnauthorized();
// If the previous_url is from this site, then
// we can redirect back to it.
if (strpos(previous_url(), site_url()) === 0) {
return redirect()->back()->with('error', lang('Auth.notEnoughPrivilege'));
}

// Otherwise, we'll just send them to the home page.
return redirect()->to('/')->with('error', lang('Auth.notEnoughPrivilege'));
}

/**
Expand Down
25 changes: 14 additions & 11 deletions src/Filters/PermissionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,8 @@
class PermissionFilter implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
* Ensures the user is logged in and has one or
* more permissions as specified in the filter.
*
* @param array|null $arguments
*
Expand All @@ -38,14 +32,23 @@ public function before(RequestInterface $request, $arguments = null)
}

if (! auth()->loggedIn()) {
return redirect()->to('login');
return redirect()->route('login');
}

foreach ($arguments as $permission) {
if (! auth()->user()->can($permission)) {
throw PermissionException::forUnauthorized();
if (auth()->user()->can($permission)) {
return;
}
}

// If the previous_url is from this site, then
// we can redirect back to it.
if (strpos(previous_url(), site_url()) === 0) {
return redirect()->back()->with('error', lang('Auth.notEnoughPrivilege'));
}

// Otherwise, we'll just send them to the home page.
return redirect()->to('/')->with('error', lang('Auth.notEnoughPrivilege'));
}

/**
Expand Down
9 changes: 8 additions & 1 deletion tests/Authentication/Filters/AbstractFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
use CodeIgniter\Test\FeatureTestTrait;
use Config\Services;
use Tests\Support\TestCase;
use CodeIgniter\Shield\Test\AuthenticationTesting;

/**
* @internal
*/
abstract class AbstractFilterTest extends TestCase
{
use FeatureTestTrait;
use AuthenticationTesting;

protected $namespace;
protected string $alias;
Expand All @@ -25,6 +27,7 @@ protected function setUp(): void
$_SESSION = [];

Services::reset(true);
helper('test');

parent::setUp();

Expand All @@ -48,9 +51,13 @@ private function addRoutes(): void
{
$routes = service('routes');

$filterString = ! empty($this->routeFilter)
? $this->routeFilter
: $this->alias;

$routes->group(
'/',
['filter' => $this->alias],
['filter' => $filterString],
static function ($routes): void {
$routes->get('protected-route', static function (): void {
echo 'Protected';
Expand Down
81 changes: 81 additions & 0 deletions tests/Authentication/Filters/GroupFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Tests\Authentication\Filters;

use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Filters\GroupFilter;
use CodeIgniter\Shield\Models\UserModel;
use CodeIgniter\Test\DatabaseTestTrait;

final class GroupFilterTest extends AbstractFilterTest
{
use DatabaseTestTrait;

protected string $alias = 'group';
protected string $classname = GroupFilter::class;
protected string $routeFilter = 'group:admin';

public function testFilterNotAuthorized(): void
{
$result = $this->call('get', 'protected-route');

$result->assertRedirectTo('/login');

$result = $this->get('open-route');
$result->assertStatus(200);
$result->assertSee('Open');
}

public function testFilterSuccess()
{
/** @var User */
$user = fake(UserModel::class);
$user->addGroup('admin');

$result = $this
->actingAs($user)
->get('protected-route');

$result->assertStatus(200);
$result->assertSee('Protected');

$this->assertSame($user->id, auth('session')->id());
$this->assertSame($user->id, auth('session')->user()->id);
}

public function testFilterIncorrectGroupNoPrevious()
{
/** @var User */
$user = fake(UserModel::class);
$user->addGroup('beta');

$result = $this
->actingAs($user)
->get('protected-route');

// Should redirect to home page since previous_url is not set
$result->assertRedirectTo(site_url('/'));
// Should have error message
$result->assertSessionHas('error');
}

public function testFilterIncorrectGroupWithPrevious()
{
/** @var User */
$user = fake(UserModel::class);
$user->addGroup('beta');

$result = $this
->actingAs($user)
->withSession(['_ci_previous_url' => site_url('open-route')])
->get('protected-route');

// Should redirect to home page since previous_url is not set
$result->assertRedirectTo(site_url('open-route'));

$result->assertSessionHas('error');
}

}
81 changes: 81 additions & 0 deletions tests/Authentication/Filters/PermissionFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Tests\Authentication\Filters;

use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Filters\PermissionFilter;
use CodeIgniter\Shield\Models\UserModel;
use CodeIgniter\Test\DatabaseTestTrait;

final class PermissionFilterTest extends AbstractFilterTest
{
use DatabaseTestTrait;

protected string $alias = 'permission';
protected string $classname = PermissionFilter::class;
protected string $routeFilter = 'permission:admin.access';

public function testFilterNotAuthorized(): void
{
$result = $this->call('get', 'protected-route');

$result->assertRedirectTo('/login');

$result = $this->get('open-route');
$result->assertStatus(200);
$result->assertSee('Open');
}

public function testFilterSuccess()
{
/** @var User */
$user = fake(UserModel::class);
$user->addPermission('admin.access');

$result = $this
->actingAs($user)
->get('protected-route');

$result->assertStatus(200);
$result->assertSee('Protected');

$this->assertSame($user->id, auth('session')->id());
$this->assertSame($user->id, auth('session')->user()->id);
}

public function testFilterIncorrectGroupNoPrevious()
{
/** @var User */
$user = fake(UserModel::class);
$user->addPermission('beta.access');

$result = $this
->actingAs($user)
->get('protected-route');

// Should redirect to home page since previous_url is not set
$result->assertRedirectTo(site_url('/'));
// Should have error message
$result->assertSessionHas('error');
}

public function testFilterIncorrectGroupWithPrevious()
{
/** @var User */
$user = fake(UserModel::class);
$user->addPermission('beta.access');

$result = $this
->actingAs($user)
->withSession(['_ci_previous_url' => site_url('open-route')])
->get('protected-route');

// Should redirect to home page since previous_url is not set
$result->assertRedirectTo(site_url('open-route'));

$result->assertSessionHas('error');
}

}

0 comments on commit 714bdd1

Please sign in to comment.