Skip to content

Commit c14dcb3

Browse files
committed
Add group and permission filters.
1 parent 810dfb7 commit c14dcb3

File tree

6 files changed

+220
-23
lines changed

6 files changed

+220
-23
lines changed

docs/authorization.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- [can()](#can)
1010
- [inGroup()](#ingroup)
1111
- [hasPermission()](#haspermission)
12+
- [Authorizing via Filters](#authorizing-via-filters)
13+
- [Authorizing via Routes](#authorizing-via-routes)
1214
- [Managing User Permissions](#managing-user-permissions)
1315
- [addPermission()](#addpermission)
1416
- [removePermission()](#removepermission)
@@ -128,6 +130,28 @@ if (! $user->hasPermission('users.create')) {
128130
}
129131
```
130132

133+
#### Authorizing via Filters
134+
135+
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`:
136+
137+
```php
138+
public $filters = [
139+
'group:admin,superadmin' => ['before' => ['admin/*']],
140+
'permission:users.manage' => ['before' => ['admin/users/*']],
141+
];
142+
```
143+
144+
#### Authorizing via Routes
145+
146+
The filters can also be used on a route or route group level:
147+
148+
```php
149+
$routes->group('admin', ['filter' => 'group:admin,superadmin'], static function ($routes) {
150+
$routes->resource('users');
151+
});
152+
153+
```
154+
131155
## Managing User Permissions
132156

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

200224
#### getGroups()
201225

202-
Returns all groups this user is a part of.
226+
Returns all groups this user is a part of.
203227

204228
```php
205229
$user->getGroups();

src/Filters/GroupFilter.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,8 @@
1818
class GroupFilter implements FilterInterface
1919
{
2020
/**
21-
* Do whatever processing this filter needs to do.
22-
* By default it should not return anything during
23-
* normal execution. However, when an abnormal state
24-
* is found, it should return an instance of
25-
* CodeIgniter\HTTP\Response. If it does, script
26-
* execution will end and that Response will be
27-
* sent back to the client, allowing for error pages,
28-
* redirects, etc.
21+
* Ensures the user is logged in and a member of one or
22+
* more groups as specified in the filter.
2923
*
3024
* @param array|null $arguments
3125
*
@@ -38,14 +32,21 @@ public function before(RequestInterface $request, $arguments = null)
3832
}
3933

4034
if (! auth()->loggedIn()) {
41-
return redirect()->to('login');
35+
return redirect()->route('login');
4236
}
4337

4438
if (auth()->user()->inGroup(...$arguments)) {
4539
return;
4640
}
4741

48-
throw GroupException::forUnauthorized();
42+
// If the previous_url is from this site, then
43+
// we can redirect back to it.
44+
if (strpos(previous_url(), site_url()) === 0) {
45+
return redirect()->back()->with('error', lang('Auth.notEnoughPrivilege'));
46+
}
47+
48+
// Otherwise, we'll just send them to the home page.
49+
return redirect()->to('/')->with('error', lang('Auth.notEnoughPrivilege'));
4950
}
5051

5152
/**

src/Filters/PermissionFilter.php

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,8 @@
1818
class PermissionFilter implements FilterInterface
1919
{
2020
/**
21-
* Do whatever processing this filter needs to do.
22-
* By default it should not return anything during
23-
* normal execution. However, when an abnormal state
24-
* is found, it should return an instance of
25-
* CodeIgniter\HTTP\Response. If it does, script
26-
* execution will end and that Response will be
27-
* sent back to the client, allowing for error pages,
28-
* redirects, etc.
21+
* Ensures the user is logged in and has one or
22+
* more permissions as specified in the filter.
2923
*
3024
* @param array|null $arguments
3125
*
@@ -38,14 +32,23 @@ public function before(RequestInterface $request, $arguments = null)
3832
}
3933

4034
if (! auth()->loggedIn()) {
41-
return redirect()->to('login');
35+
return redirect()->route('login');
4236
}
4337

4438
foreach ($arguments as $permission) {
45-
if (! auth()->user()->can($permission)) {
46-
throw PermissionException::forUnauthorized();
39+
if (auth()->user()->can($permission)) {
40+
return;
4741
}
4842
}
43+
44+
// If the previous_url is from this site, then
45+
// we can redirect back to it.
46+
if (strpos(previous_url(), site_url()) === 0) {
47+
return redirect()->back()->with('error', lang('Auth.notEnoughPrivilege'));
48+
}
49+
50+
// Otherwise, we'll just send them to the home page.
51+
return redirect()->to('/')->with('error', lang('Auth.notEnoughPrivilege'));
4952
}
5053

5154
/**

tests/Authentication/Filters/AbstractFilterTest.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
use CodeIgniter\Test\FeatureTestTrait;
99
use Config\Services;
1010
use Tests\Support\TestCase;
11+
use CodeIgniter\Shield\Test\AuthenticationTesting;
1112

1213
/**
1314
* @internal
1415
*/
1516
abstract class AbstractFilterTest extends TestCase
1617
{
1718
use FeatureTestTrait;
19+
use AuthenticationTesting;
1820

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

2729
Services::reset(true);
30+
helper('test');
2831

2932
parent::setUp();
3033

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

54+
$filterString = ! empty($this->routeFilter)
55+
? $this->routeFilter
56+
: $this->alias;
57+
5158
$routes->group(
5259
'/',
53-
['filter' => $this->alias],
60+
['filter' => $filterString],
5461
static function ($routes): void {
5562
$routes->get('protected-route', static function (): void {
5663
echo 'Protected';
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Authentication\Filters;
6+
7+
use CodeIgniter\Shield\Entities\User;
8+
use CodeIgniter\Shield\Filters\GroupFilter;
9+
use CodeIgniter\Shield\Models\UserModel;
10+
use CodeIgniter\Test\DatabaseTestTrait;
11+
12+
final class GroupFilterTest extends AbstractFilterTest
13+
{
14+
use DatabaseTestTrait;
15+
16+
protected string $alias = 'group';
17+
protected string $classname = GroupFilter::class;
18+
protected string $routeFilter = 'group:admin';
19+
20+
public function testFilterNotAuthorized(): void
21+
{
22+
$result = $this->call('get', 'protected-route');
23+
24+
$result->assertRedirectTo('/login');
25+
26+
$result = $this->get('open-route');
27+
$result->assertStatus(200);
28+
$result->assertSee('Open');
29+
}
30+
31+
public function testFilterSuccess()
32+
{
33+
/** @var User */
34+
$user = fake(UserModel::class);
35+
$user->addGroup('admin');
36+
37+
$result = $this
38+
->actingAs($user)
39+
->get('protected-route');
40+
41+
$result->assertStatus(200);
42+
$result->assertSee('Protected');
43+
44+
$this->assertSame($user->id, auth('session')->id());
45+
$this->assertSame($user->id, auth('session')->user()->id);
46+
}
47+
48+
public function testFilterIncorrectGroupNoPrevious()
49+
{
50+
/** @var User */
51+
$user = fake(UserModel::class);
52+
$user->addGroup('beta');
53+
54+
$result = $this
55+
->actingAs($user)
56+
->get('protected-route');
57+
58+
// Should redirect to home page since previous_url is not set
59+
$result->assertRedirectTo(site_url('/'));
60+
// Should have error message
61+
$result->assertSessionHas('error');
62+
}
63+
64+
public function testFilterIncorrectGroupWithPrevious()
65+
{
66+
/** @var User */
67+
$user = fake(UserModel::class);
68+
$user->addGroup('beta');
69+
70+
$result = $this
71+
->actingAs($user)
72+
->withSession(['_ci_previous_url' => site_url('open-route')])
73+
->get('protected-route');
74+
75+
// Should redirect to home page since previous_url is not set
76+
$result->assertRedirectTo(site_url('open-route'));
77+
78+
$result->assertSessionHas('error');
79+
}
80+
81+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Authentication\Filters;
6+
7+
use CodeIgniter\Shield\Entities\User;
8+
use CodeIgniter\Shield\Filters\PermissionFilter;
9+
use CodeIgniter\Shield\Models\UserModel;
10+
use CodeIgniter\Test\DatabaseTestTrait;
11+
12+
final class PermissionFilterTest extends AbstractFilterTest
13+
{
14+
use DatabaseTestTrait;
15+
16+
protected string $alias = 'permission';
17+
protected string $classname = PermissionFilter::class;
18+
protected string $routeFilter = 'permission:admin.access';
19+
20+
public function testFilterNotAuthorized(): void
21+
{
22+
$result = $this->call('get', 'protected-route');
23+
24+
$result->assertRedirectTo('/login');
25+
26+
$result = $this->get('open-route');
27+
$result->assertStatus(200);
28+
$result->assertSee('Open');
29+
}
30+
31+
public function testFilterSuccess()
32+
{
33+
/** @var User */
34+
$user = fake(UserModel::class);
35+
$user->addPermission('admin.access');
36+
37+
$result = $this
38+
->actingAs($user)
39+
->get('protected-route');
40+
41+
$result->assertStatus(200);
42+
$result->assertSee('Protected');
43+
44+
$this->assertSame($user->id, auth('session')->id());
45+
$this->assertSame($user->id, auth('session')->user()->id);
46+
}
47+
48+
public function testFilterIncorrectGroupNoPrevious()
49+
{
50+
/** @var User */
51+
$user = fake(UserModel::class);
52+
$user->addPermission('beta.access');
53+
54+
$result = $this
55+
->actingAs($user)
56+
->get('protected-route');
57+
58+
// Should redirect to home page since previous_url is not set
59+
$result->assertRedirectTo(site_url('/'));
60+
// Should have error message
61+
$result->assertSessionHas('error');
62+
}
63+
64+
public function testFilterIncorrectGroupWithPrevious()
65+
{
66+
/** @var User */
67+
$user = fake(UserModel::class);
68+
$user->addPermission('beta.access');
69+
70+
$result = $this
71+
->actingAs($user)
72+
->withSession(['_ci_previous_url' => site_url('open-route')])
73+
->get('protected-route');
74+
75+
// Should redirect to home page since previous_url is not set
76+
$result->assertRedirectTo(site_url('open-route'));
77+
78+
$result->assertSessionHas('error');
79+
}
80+
81+
}

0 commit comments

Comments
 (0)