Skip to content

Add filter permission and group #535

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 under the `group` and `permission` aliases, respectively. You can define the protections 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
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/AbstractAuthFilter.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;

/**
* Group Authorization Filter.
*/
abstract class AbstractAuthFilter 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 ($this->isAuthorized($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'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this PR in a practical way. I did not see a problem.

What bothers me is displaying the error message to the user.
Bonfire2 uses Tatter/Alerts to display errors, but the following code should be used to display errors on the shield:

    <?php if (session('error') !== null) : ?>
        <div class="alert alert-danger" role="alert"><?= session('error') ?></div>
    <?php elseif (session('errors') !== null) : ?>
        <div class="alert alert-danger" role="alert">
            <?php if (is_array(session('errors'))) : ?>
                <?php foreach (session('errors') as $error) : ?>
                    <?= $error ?>
                    <br>
                <?php endforeach ?>
            <?php else : ?>
                <?= session('errors') ?>
            <?php endif ?>
        </div>
    <?php endif ?>

In fact, I'm not sure how Shield users should know what to do to display error message (Auth.notEnoughPrivilege). Perhaps the need to add an explanation in the documentation or something else.

}

// 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
}

abstract protected function isAuthorized(array $arguments): bool;
}
20 changes: 20 additions & 0 deletions src/Filters/GroupFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Filters;

/**
* Group Authorization Filter.
*/
class GroupFilter extends AbstractAuthFilter
{
/**
* Ensures the user is logged in and a member of one or
* more groups as specified in the filter.
*/
protected function isAuthorized(array $arguments): bool
{
return auth()->user()->inGroup(...$arguments);
}
}
26 changes: 26 additions & 0 deletions src/Filters/PermissionFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace CodeIgniter\Shield\Filters;

/**
* Permission Authorization Filter.
*/
class PermissionFilter extends AbstractAuthFilter
{
/**
* Ensures the user is logged in and has one or more
* of the permissions as specified in the filter.
*/
protected function isAuthorized(array $arguments): bool
{
foreach ($arguments as $permission) {
if (auth()->user()->can($permission)) {
return true;
}
}

return false;
}
}
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/it/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'Impossibile verificare che l\'indirizzo email corrisponda all\'email nel record.',
'unableSendEmailToUser' => 'Spiacente, c\'è stato un problema inviando l\'email. Non possiamo inviare un\'email a "{0}".',
'throttled' => 'Troppe richieste effettuate da questo indirizzo IP. Potrai riprovare tra {0} secondi.',
'notEnoughPrivilege' => 'Non si dispone dell\'autorizzazione necessaria per eseguire l\'operazione desiderata.',

'email' => 'Indirizzo Email',
'username' => 'Nome Utente',
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
1 change: 1 addition & 0 deletions src/Language/tr/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'invalidEmail' => 'E-posta adresinin kayıtlı e-posta ile eşleştiği doğrulanamıyor.',
'unableSendEmailToUser' => 'Üzgünüz, e-posta gönderilirken bir sorun oluştu. "{0}" adresine e-posta gönderemedik.',
'throttled' => 'Bu IP adresinden çok fazla istek yapıldı. {0} saniye sonra tekrar deneyebilirsiniz.',
'notEnoughPrivilege' => 'İstediğiniz işlemi gerçekleştirmek için gerekli izne sahip değilsiniz.',

'email' => 'E-posta Adresi',
'username' => 'Kullanıcı Adı',
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 @@ -5,6 +5,7 @@
namespace Tests\Authentication\Filters;

use CodeIgniter\Config\Factories;
use CodeIgniter\Shield\Test\AuthenticationTesting;
use CodeIgniter\Test\FeatureTestTrait;
use Config\Services;
use Tests\Support\TestCase;
Expand All @@ -15,6 +16,7 @@
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