๐Ÿ‘ฅ Authorization โ€” Groups & Permissions๏ƒ

Daycry Auth includes a full Role-Based Access Control (RBAC) system built on two concepts:

  • Groups โ€” Named roles (e.g., admin, editor, user)

  • Permissions โ€” Specific actions (e.g., posts.create, users.delete)

Both are stored in the database and can be assigned freely to any user.

๐Ÿ“‹ Table of Contents๏ƒ


Quick Reference๏ƒ

// Groups
$user->inGroup('admin');                    // bool
$user->addGroup('editor');                  // void
$user->removeGroup('editor');               // void
$user->getGroups();                         // array of group names

// Permissions
$user->can('posts.create');                 // bool
$user->addPermission('posts.edit');         // void
$user->removePermission('posts.edit');      // void
$user->getPermissions();                    // array of permission names

// Shortcuts (throws AuthorizationException on failure)
$user->authorize('posts.delete');

// Multiple groups / permissions
$user->inGroup('admin', 'moderator');       // true if in ANY of them
$user->hasAnyPermission('posts.edit', 'posts.delete'); // true if has ANY

Groups๏ƒ

What Is a Group?๏ƒ

A group is a named role. Users can belong to multiple groups. Example groups: admin, editor, subscriber, premium.

Groups are stored in the auth_groups table. You create them through the Admin Panel or directly via SQL/migrations.

Checking Group Membership๏ƒ

$user = auth()->user();

// Single group check
if ($user->inGroup('admin')) {
    echo "Welcome, administrator!";
}

// Check multiple groups (true if the user is in ANY of them)
if ($user->inGroup('admin', 'moderator')) {
    echo "Welcome, elevated user!";
}

// Get all groups the user belongs to
$groups = $user->getGroups();
// Returns: ['admin', 'editor']

Assigning and Removing Groups๏ƒ

$user = auth()->user();

// Add to a group
$user->addGroup('editor');
$user->addGroup('premium', 'beta-tester'); // Multiple groups at once

// Remove from a group
$user->removeGroup('editor');

// Check and conditionally assign
if (! $user->inGroup('subscriber')) {
    $user->addGroup('subscriber');
}

Getting All Users in a Group๏ƒ

use Daycry\Auth\Models\GroupModel;

$groupModel = model(GroupModel::class);
$adminGroup = $groupModel->where('name', 'admin')->first();

// Get all users in the admin group
$adminUsers = model(\Daycry\Auth\Models\UserModel::class)
    ->join('auth_groups_users', 'auth_groups_users.user_id = users.id')
    ->join('auth_groups', 'auth_groups.id = auth_groups_users.group_id')
    ->where('auth_groups.name', 'admin')
    ->findAll();

Permissions๏ƒ

Permission Format๏ƒ

Permissions follow a resource.action convention:

posts.create
posts.edit
posts.delete
users.view
users.edit
admin.panel

Using dots keeps permissions organized and makes wildcard patterns intuitive.

Checking Permissions๏ƒ

$user = auth()->user();

// Direct check
if ($user->can('posts.create')) {
    // User can create posts
}

// Negation
if (! $user->can('users.delete')) {
    return redirect()->back()->with('error', 'You cannot delete users.');
}

// Check multiple permissions (true if the user has ALL of them)
if ($user->can('posts.create') && $user->can('posts.publish')) {
    // User can create AND publish
}

// Check if user has ANY of several permissions
if ($user->hasAnyPermission('posts.edit', 'posts.delete')) {
    // Can edit or delete
}

Assigning and Removing Permissions๏ƒ

Permissions can be assigned directly to a user (user-level) or to a group (group-level, then inherited by all group members).

$user = auth()->user();

// Direct user permissions
$user->addPermission('posts.edit');
$user->addPermission('posts.publish', 'posts.feature'); // Multiple at once

// Remove a permission
$user->removePermission('posts.edit');

// Get all permissions the user has (direct + inherited from groups)
$allPermissions = $user->getPermissions();

Assigning Permissions to a Group๏ƒ

use Daycry\Auth\Models\GroupModel;

$groupModel  = model(GroupModel::class);
$editorGroup = $groupModel->where('name', 'editor')->first();

// Add permission to the group โ€” all editors inherit this
$editorGroup->addPermission('posts.create');
$editorGroup->addPermission('posts.edit');

Permission Inheritance๏ƒ

User
 โ”œโ”€โ”€ Direct permissions: [posts.delete]
 โ””โ”€โ”€ Groups:
      โ”œโ”€โ”€ editor  โ†’ permissions: [posts.create, posts.edit]
      โ””โ”€โ”€ premium โ†’ permissions: [posts.feature]

Effective permissions: [posts.delete, posts.create, posts.edit, posts.feature]

$user->can('posts.create') returns true because editor group has that permission, and the user is in the editor group.

Wildcard Permissions๏ƒ

Use * as a wildcard:

// Grant access to all "posts" permissions
$user->addPermission('posts.*');

$user->can('posts.create'); // true
$user->can('posts.edit');   // true
$user->can('posts.delete'); // true
$user->can('users.view');   // false (different resource)
// Grant ALL permissions (superadmin)
$user->addPermission('*');

$user->can('posts.delete'); // true
$user->can('users.view');   // true

Gates & Policies๏ƒ

The RBAC system above answers questions like โ€œdoes this user have permission posts.update?โ€ โ€” a property of the user only. For โ€œis this user allowed to update this specific post?โ€ โ€” a question that depends on the resource โ€” daycry/auth ships a Gate / Policy layer inspired by Laravelโ€™s Gate facade. Use both: RBAC for static role checks, Gates for context-dependent rules.

Closure-based abilities๏ƒ

Register a closure to define an ability inline. The first parameter is the authenticated user (or null for guests); any further arguments are the resource(s) being checked:

// app/Config/Events.php (or any bootstrap point)
use Daycry\Auth\Authorization\Gate;
use Daycry\Auth\Entities\User;

service('gate')->define('post.update', static function (?User $user, Post $post): bool {
    return $user !== null && $user->id === $post->author_id;
});

Then call from anywhere:

if (service('gate')->allows('post.update', $post)) {
    // ...
}

// Negation:
service('gate')->denies('post.update', $post);

// Fail-fast โ€” throws AuthorizationException on a deny:
service('gate')->authorize('post.update', $post);

The User entity exposes the same checks as canDo() / cantDo():

$user->canDo('post.update', $post);  // bool
$user->cantDo('post.delete', $post); // bool

canDo() and cantDo() are deliberately distinct from can() / cant() so the existing RBAC method can('posts.update') (string-only permission identifier) keeps its signature.

Class-based policies๏ƒ

For resources with multiple actions, group the rules into a Policy subclass:

namespace App\Policies;

use App\Models\Post;
use Daycry\Auth\Authorization\Policy;
use Daycry\Auth\Authorization\PolicyResponse;
use Daycry\Auth\Entities\User;

class PostPolicy extends Policy
{
    public function update(?User $user, Post $post): bool
    {
        return $user !== null && $user->id === $post->author_id;
    }

    public function delete(?User $user, Post $post): PolicyResponse
    {
        if ($user === null) {
            return PolicyResponse::deny('You must be logged in.');
        }

        return $user->id === $post->author_id
            ? PolicyResponse::allow()
            : PolicyResponse::deny('Only the author can delete this post.');
    }

    /**
     * Optional pre-flight hook. Return true / false / a PolicyResponse to
     * short-circuit the action method. Return null to fall through.
     */
    public function before(?User $user, string $ability, array $arguments): bool|PolicyResponse|null
    {
        if ($user !== null && $user->inGroup('admin')) {
            return true; // admins bypass every action on this resource
        }

        return null;
    }
}

Auto-discovery๏ƒ

By default, the Gate looks up App\Policies\{ResourceShortName}Policy for any resource passed to a check. So service('gate')->allows('update', $post) automatically resolves App\Models\Post โ†’ App\Policies\PostPolicy::update(). Override the namespace in app/Config/Auth.php:

public string $policyNamespace = 'App\\Authorization\\Policies\\';
public bool $gateAutoDiscover  = true; // false to require explicit registration

Explicit registration๏ƒ

If your resources donโ€™t follow the convention, map them by hand:

service('gate')->policy(\App\Models\Post::class, \App\Policies\PostPolicy::class);

Action name convention๏ƒ

Ability names ending in .action (e.g. post.update) dispatch to the action method (update). Bare names (e.g. update) also work. This lets you namespace abilities for clarity in routes / configs while keeping policy method names short.

gate: route filter๏ƒ

For abilities that depend only on the authenticated user (not on a resource instance) โ€” typical for โ€œsection-levelโ€ gates such as accessing a dashboard or a billing area โ€” apply the gate: filter directly:

$routes->get('admin', 'Admin::index', ['filter' => 'gate:admin.access']);

// Multiple abilities (AND):
$routes->get('billing/cancel', 'Billing::cancel', [
    'filter' => 'gate:billing.access,billing.cancel',
]);

Failure responses follow the same shape as permission: โ€” JSON 403 for API requests, redirect to Auth::permissionDeniedRedirect() for web.

For abilities that need a resource argument (a Post, an Orderโ€ฆ), use the Gate API directly inside the controller method โ€” the filter cannot reach the resource yet:

public function update(int $id)
{
    $post = $this->postModel->find($id);
    service('gate')->authorize('post.update', $post);  // throws on deny

    // ...persist update...
}

When to use what๏ƒ

You want to check

Use

Does this user have permission users.edit?

RBAC โ€” $user->can('users.edit')

Does this user belong to the admin group?

RBAC โ€” $user->inGroup('admin')

Can this user update this specific post?

Gate โ€” $user->canDo('post.update', $post)

Block a route based on the user alone

permission: or group: filter

Block a route based on a resource

gate: filter for ability-only checks; controller Gate::authorize() for resource-aware checks


Authorization in Controllers๏ƒ

Pattern 1: Early Return๏ƒ

<?php

namespace App\Controllers;

use App\Controllers\BaseController;

class PostController extends BaseController
{
    public function delete(int $id)
    {
        if (! auth()->user()->can('posts.delete')) {
            return redirect()->back()->with('error', 'You are not authorized to delete posts.');
        }

        // Delete the post
        model(\App\Models\PostModel::class)->delete($id);

        return redirect()->to('posts')->with('message', 'Post deleted.');
    }
}

Pattern 2: Exception-Based๏ƒ

use Daycry\Auth\Exceptions\AuthorizationException;

public function delete(int $id)
{
    // Throws AuthorizationException if not authorized
    auth()->user()->authorize('posts.delete');

    model(\App\Models\PostModel::class)->delete($id);
    return redirect()->to('posts')->with('message', 'Post deleted.');
}

Set a global exception handler to catch AuthorizationException and redirect to a โ€œ403 Forbiddenโ€ page:

// app/Config/Exceptions.php or a custom ExceptionHandler
use Daycry\Auth\Exceptions\AuthorizationException;

// In your BaseController or exception handler:
if ($e instanceof AuthorizationException) {
    return redirect()->to('/')->with('error', 'You do not have permission to do that.');
}

Pattern 3: Group + Permission Check Together๏ƒ

public function adminPanel()
{
    $user = auth()->user();

    if (! $user->inGroup('admin') && ! $user->can('admin.panel')) {
        return redirect()->to('/')->with('error', 'Access denied.');
    }

    return view('admin/index');
}

Authorization in Views๏ƒ

Show or hide UI elements based on the current userโ€™s permissions:

<!-- Show the edit button only to users who can edit posts -->
<?php if (auth()->user()?->can('posts.edit')): ?>
    <a href="<?= site_url('posts/edit/' . $post->id) ?>" class="btn btn-sm btn-secondary">
        Edit
    </a>
<?php endif ?>

<!-- Show the delete button only to admins -->
<?php if (auth()->user()?->inGroup('admin')): ?>
    <button type="button" class="btn btn-sm btn-danger"
            onclick="confirmDelete(<?= $post->id ?>)">
        Delete
    </button>
<?php endif ?>

<!-- Admin navigation link -->
<?php if (auth()->user()?->inGroup('admin', 'moderator')): ?>
    <li class="nav-item">
        <a class="nav-link" href="<?= site_url('admin') ?>">Admin Panel</a>
    </li>
<?php endif ?>

Note: Use auth()->user()?->can(...) (null-safe operator) when the user might not be logged in.


Route Filters๏ƒ

Protect entire route groups using filters โ€” no controller code needed:

// app/Config/Routes.php

// Require 'admin' group
$routes->group('admin', ['filter' => 'session,group:admin'], static function ($routes) {
    $routes->get('/', 'Admin\DashboardController::index');
    $routes->get('users', 'Admin\UsersController::index');
});

// Require a specific permission
$routes->group('posts', ['filter' => 'session,permission:posts.create'], static function ($routes) {
    $routes->get('new', 'PostController::new');
    $routes->post('create', 'PostController::create');
});

// Multiple groups (user must be in AT LEAST ONE)
$routes->get('moderation', 'ModerationController::index', ['filter' => 'session,group:admin,moderator']);

// Multiple permissions (user must have ALL)
$routes->post('posts/publish/(:num)', 'PostController::publish/$1',
    ['filter' => 'session,permission:posts.publish,posts.edit']);

Register Filter Aliases๏ƒ

// app/Config/Filters.php
public array $aliases = [
    'session'    => \Daycry\Auth\Filters\AuthSessionFilter::class,
    'group'      => \Daycry\Auth\Filters\GroupFilter::class,
    'permission' => \Daycry\Auth\Filters\PermissionFilter::class,
];

What Happens on Denial?๏ƒ

When a user fails a group or permission check:

  • GroupFilter redirects to the URL defined in config('Auth')->redirects['group_denied']

  • PermissionFilter redirects to config('Auth')->redirects['permission_denied']

Configure these in app/Config/Auth.php:

public array $redirects = [
    'group_denied'      => '/',   // Redirect when not in required group
    'permission_denied' => '/',   // Redirect when missing required permission
];

Permission Cache๏ƒ

In production, repeated permission checks can generate many database queries. Enable the permission cache to store each userโ€™s groups and permissions in the CI4 cache:

// app/Config/Auth.php
public bool $permissionCacheEnabled = true;
public int  $permissionCacheTTL     = 300; // Seconds (5 minutes)

Cache Invalidation๏ƒ

The cache is automatically invalidated whenever you call:

$user->addGroup('admin');
$user->removeGroup('editor');
$user->addPermission('posts.create');
$user->removePermission('posts.delete');

For manual invalidation:

$user->clearPermissionCache();

When to Enable the Cache๏ƒ

Environment

Recommendation

Development

Disable (easier debugging)

Staging

Enable (realistic performance testing)

Production

Always enable


Default Group for New Users๏ƒ

Automatically assign all new registrations to a group:

// app/Config/Auth.php
public string $defaultGroup = 'user';

During registration, UserModel::addToDefaultGroup() is called automatically. The group must exist in the auth_groups table โ€” create it via the Admin Panel or a migration.


Admin Panel๏ƒ

Daycry Auth ships with a Bootstrap 5 admin panel for managing groups and permissions without writing any code:

// app/Config/Routes.php
$routes->group('admin/auth', ['filter' => 'session,group:admin', 'namespace' => 'Daycry\Auth\Controllers\Admin'], static function ($routes) {
    $routes->get('/',           'DashboardController::index', ['as' => 'auth-admin']);
    $routes->resource('users',       ['controller' => 'UsersController']);
    $routes->resource('groups',      ['controller' => 'GroupsController']);
    $routes->resource('permissions', ['controller' => 'PermissionsController']);
});

From the admin panel you can:

  • Create and edit groups

  • Assign permissions to groups

  • Assign users to groups

  • View and revoke individual user permissions


Best Practices๏ƒ

1. Use Groups for Roles, Permissions for Actions๏ƒ

// Good: Groups for broad roles
$user->addGroup('editor');

// Good: Permissions for specific capabilities
$user->addPermission('posts.publish');

// Avoid: Over-granular groups
// $user->addGroup('post-editor'); // Use a group + permission instead

2. Check Permissions, Not Groups, in Business Logic๏ƒ

// Preferred: permission check (flexible)
if ($user->can('posts.delete')) { ... }

// Avoid: group check in business logic (brittle)
if ($user->inGroup('admin')) { ... } // What if a 'moderator' should also delete?

3. Enable Cache in Production๏ƒ

// app/Config/Auth.php
public bool $permissionCacheEnabled = ENVIRONMENT === 'production';

4. Keep Permission Names Consistent๏ƒ

Use resource.action format consistently:

users.view    users.create    users.edit    users.delete
posts.view    posts.create    posts.edit    posts.delete    posts.publish
admin.panel   admin.settings  admin.logs

๐Ÿ”— See also:

  • Filters โ€” Route-level authorization filters

  • Controllers โ€” Controller-level authorization patterns

  • Configuration โ€” Permission cache and redirect configuration