๐ฅ 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()andcantDo()are deliberately distinct fromcan()/cant()so the existing RBAC methodcan('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 |
RBAC โ |
Does this user belong to the admin group? |
RBAC โ |
Can this user update this specific post? |
Gate โ |
Block a route based on the user alone |
|
Block a route based on a resource |
|
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:
GroupFilterredirects to the URL defined inconfig('Auth')->redirects['group_denied']PermissionFilterredirects toconfig('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