Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filter permission and group #535

Merged
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`:
lonnieezell marked this conversation as resolved.
Show resolved Hide resolved

kenjis marked this conversation as resolved.
Show resolved Hide resolved
```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
41 changes: 23 additions & 18 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,32 @@ your project.
1. Use InnoDB, not MyISAM.

## Controller Filters
The [Controller Filters](https://codeigniter.com/user_guide/incoming/filters.html) you can use to protect your routes the shield provides are:

```php
public $aliases = [
// ...
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class,
'group' => \CodeIgniter\Shield\Filters\GroupFilter::class,
'permission' => \CodeIgniter\Shield\Filters\PermissionFilter::class,
];
```

Filters | Description
--- | ---
session and tokens | The `Session` and `AccessTokens` authenticators, respectively.
chained | The filter will check both authenticators in sequence to see if the user is logged in through either of authenticators, allowing a single API endpoint to work for both an SPA using session auth, and a mobile app using access tokens.
auth-rates | Provides a good basis for rate limiting of auth-related routes.
group | Checks if the user is in one of the groups passed in.
permission | Checks if the user has the passed permissions.

Shield provides 4 [Controller Filters](https://codeigniter.com/user_guide/incoming/filters.html) you can
use to protect your routes, `session`, `tokens`, and `chained`. The first two cover the `Session` and
`AccessTokens` authenticators, respectively. The `chained` filter will check both authenticators in sequence
to see if the user is logged in through either of authenticators, allowing a single API endpoint to
work for both an SPA using session auth, and a mobile app using access tokens. The fourth, `auth-rates`,
provides a good basis for rate limiting of auth-related routes.
These can be used in any of the [normal filter config settings](https://codeigniter.com/user_guide/incoming/filters.html?highlight=filter#globals), or [within the routes file](https://codeigniter.com/user_guide/incoming/routing.html?highlight=routs#applying-filters).

> **Note** These filters are already loaded for you by the registrar class located at `src/Config/Registrar.php`.

### Protect All Pages

If you want to limit all routes (e.g. `localhost:8080/admin`, `localhost:8080/panel` and ...), you need to add the following code in the `app/Config/Filters.php` file.
Expand All @@ -158,18 +175,6 @@ public $globals = [
];
```

> **Note** These filters are already loaded for you by the registrar class located at `src/Config/Registrar.php`.

```php
public $aliases = [
// ...
'session' => \CodeIgniter\Shield\Filters\SessionAuth::class,
'tokens' => \CodeIgniter\Shield\Filters\TokenAuth::class,
'chain' => \CodeIgniter\Shield\Filters\ChainAuth::class,
'auth-rates' => \CodeIgniter\Shield\Filters\AuthRates::class,
];
```

### Rate Limiting

To help protect your authentication forms from being spammed by bots, it is recommended that you use
Expand Down
5 changes: 5 additions & 0 deletions src/Authorization/AuthorizationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public static function forUnknownPermission(string $permission): self
{
return new self(lang('Auth.unknownPermission', [$permission]));
}

public static function forUnauthorized(): self
{
return new self(lang('Auth.notEnoughPrivilege'));
}
}
4 changes: 4 additions & 0 deletions src/Config/Registrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use CodeIgniter\Shield\Collectors\Auth;
use CodeIgniter\Shield\Filters\AuthRates;
use CodeIgniter\Shield\Filters\ChainAuth;
use CodeIgniter\Shield\Filters\GroupFilter;
use CodeIgniter\Shield\Filters\PermissionFilter;
use CodeIgniter\Shield\Filters\SessionAuth;
use CodeIgniter\Shield\Filters\TokenAuth;

Expand All @@ -24,6 +26,8 @@ public static function Filters(): array
'tokens' => TokenAuth::class,
'chain' => ChainAuth::class,
'auth-rates' => AuthRates::class,
'group' => GroupFilter::class,
'permission' => PermissionFilter::class,
],
];
}
Expand Down
11 changes: 11 additions & 0 deletions src/Exceptions/GroupException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Exceptions;

use CodeIgniter\Shield\Authorization\AuthorizationException;

class GroupException extends AuthorizationException
{
}
11 changes: 11 additions & 0 deletions src/Exceptions/PermissionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Exceptions;

use CodeIgniter\Shield\Authorization\AuthorizationException;

class PermissionException extends AuthorizationException
{
}
62 changes: 62 additions & 0 deletions src/Filters/GroupFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;

use CodeIgniter\Shield\Exceptions\GroupException;

/**
* Group Authorization Filter.
*/
class GroupFilter implements FilterInterface
{
/**
* Ensures the user is logged in and a member of one or
* more groups as specified in the filter.
*
* @param array|null $arguments
*
* @return RedirectResponse|void
*/
public function before(RequestInterface $request, $arguments = null)
{
if (empty($arguments)) {
return;
}

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

if (auth()->user()->inGroup(...$arguments)) {
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'));
}

/**
* We don't have anything to do here.
*
* @param Response|ResponseInterface $response
* @param array|null $arguments
*/
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void
{
// Nothing required
}
}
64 changes: 64 additions & 0 deletions src/Filters/PermissionFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;

use CodeIgniter\Shield\Exceptions\PermissionException;

/**
* Permission Authorization Filter.
*/
class PermissionFilter implements FilterInterface
{
/**
* Ensures the user is logged in and has one or
* more permissions as specified in the filter.
*
* @param array|null $arguments
*
* @return RedirectResponse|void
*/
public function before(RequestInterface $request, $arguments = null)
{
if (empty($arguments)) {
return;
}

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

foreach ($arguments as $permission) {
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'));
}

/**
* We don't have anything to do here.
*
* @param Response|ResponseInterface $response
* @param array|null $arguments
*/
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void
{
// Nothing required
}
}
1 change: 1 addition & 0 deletions src/Language/de/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'Es konnte nicht überprüft werden, ob die E-Mail-Adresse mit der gespeicherten übereinstimmt.',
'unableSendEmailToUser' => 'Leider gab es ein Problem beim Senden der E-Mail. Wir konnten keine E-Mail an "{0}" senden.',
'throttled' => 'Es wurden zu viele Anfragen von dieser IP-Adresse gestellt. Sie können es in {0} Sekunden erneut versuchen.',
'notEnoughPrivilege' => 'Sie haben nicht die erforderliche Berechtigung, um den gewünschten Vorgang auszuführen.',

'email' => 'E-Mail-Adresse',
'username' => 'Benutzername',
Expand Down
1 change: 1 addition & 0 deletions src/Language/en/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'Unable to verify the email address matches the email on record.',
'unableSendEmailToUser' => 'Sorry, there was a problem sending the email. We could not send an email to "{0}".',
'throttled' => 'Too many requests made from this IP address. You may try again in {0} seconds.',
'notEnoughPrivilege' => 'You do not have the necessary permission to perform the desired operation.',

'email' => 'Email Address',
'username' => 'Username',
Expand Down
3 changes: 2 additions & 1 deletion src/Language/es/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
'noUserEntity' => 'Se debe dar una Entidad de Usuario para validar la contraseña.',
'invalidEmail' => 'No podemos verificar que el email coincida con un email registrado.',
'unableSendEmailToUser' => 'Lo sentimaos, ha habido un problema al enviar el email. No podemos enviar un email a "{0}".',
'throttled' => 'demasiadas peticiones hechas desde esta IP. Puedes intentarlo de nuevo en {0} segundos.',
'throttled' => 'Demasiadas peticiones hechas desde esta IP. Puedes intentarlo de nuevo en {0} segundos.',
'notEnoughPrivilege' => 'No tiene los permisos necesarios para realizar la operación deseada.',

'email' => 'Dirección Email',
'username' => 'Usuario',
Expand Down
1 change: 1 addition & 0 deletions src/Language/fa/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'invalidEmail' => 'امکان تایید ایمیلی که با آدرس ایمیل ثبت شده یکسان نیست، وجود ندارد.',
'unableSendEmailToUser' => 'متاسفانه, در ارسال ایمیل مشکلی پیش آمد. ما نتوانستیم ایمیلی را به "{0}" ارسال کنیم.',
'throttled' => 'درخواست های بسیار زیادی از این آدرس IP انجام شده است. می توانید بعد از {0} ثانیه دوباره امتحان کنید.',
'notEnoughPrivilege' => 'شما مجوز لازم برای انجام عملیات مورد نظر را ندارید.',

'email' => 'آدرس ایمیل',
'username' => 'نام کاربری',
Expand Down
1 change: 1 addition & 0 deletions src/Language/fr/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'Impossible de vérifier que l\'adresse email existe.',
'unableSendEmailToUser' => 'Désolé, il y a eu un problème lors de l\'envoi de l\'email. Nous ne pouvons pas envoyer un email à "{0}".',
'throttled' => 'Trop de requêtes faites depuis cette adresse IP. Vous pouvez réessayer dans {0} secondes.',
'notEnoughPrivilege' => 'Vous n\'avez pas l\'autorisation nécessaire pour effectuer l\'opération souhaitée.',

'email' => 'Adresse email',
'username' => 'Identifiant',
Expand Down
1 change: 1 addition & 0 deletions src/Language/id/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'Tidak dapat memverifikasi alamat email yang cocok dengan email yang tercatat.',
'unableSendEmailToUser' => 'Maaf, ada masalah saat mengirim email. Kami tidak dapat mengirim email ke "{0}".',
'throttled' => 'Terlalu banyak permintaan yang dibuat dari alamat IP ini. Anda dapat mencoba lagi dalam {0} detik.',
'notEnoughPrivilege' => 'Anda tidak memiliki izin yang diperlukan untuk melakukan operasi yang diinginkan.',

'email' => 'Alamat Email',
'username' => 'Nama Pengguna',
Expand Down
1 change: 1 addition & 0 deletions src/Language/ja/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'メールアドレスが一致しません。', // 'Unable to verify the email address matches the email on record.',
'unableSendEmailToUser' => '申し訳ありませんが、メールの送信に問題がありました。 "{0}"にメールを送信できませんでした。', // 'Sorry, there was a problem sending the email. We could not send an email to "{0}".',
'throttled' => 'このIPアドレスからのリクエストが多すぎます。 {0}秒後に再試行できます。', // Too many requests made from this IP address. You may try again in {0} seconds.
'notEnoughPrivilege' => '目的の操作を実行するために必要な権限がありません。', // You do not have the necessary permission to perform the desired operation.

'email' => 'メールアドレス', // 'Email Address',
'username' => 'ユーザー名', // 'Username',
Expand Down
1 change: 1 addition & 0 deletions src/Language/sk/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'Nie je možné overiť, či sa e-mailová adresa zhoduje so zaznamenaným e-mailom.',
'unableSendEmailToUser' => 'Ľutujeme, pri odosielaní e-mailu sa vyskytol problém. Nepodarilo sa nám odoslať e-mail na adresu „{0}".',
'throttled' => 'Z tejto adresy IP bolo odoslaných príliš veľa žiadostí. Môžete to skúsiť znova o {0} sekúnd.',
'notEnoughPrivilege' => 'Nemáte potrebné povolenie na vykonanie požadovanej operácie.',

'email' => 'Emailová adresa',
'username' => 'Používateľské meno',
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
Loading