# 👥 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](#quick-reference) - [Groups](#groups) - [Permissions](#permissions) - [Permission Inheritance](#permission-inheritance) - [Gates & Policies](#gates--policies) - [Authorization in Controllers](#authorization-in-controllers) - [Authorization in Views](#authorization-in-views) - [Route Filters](#route-filters) - [Permission Cache](#permission-cache) - [Default Group for New Users](#default-group-for-new-users) - [Admin Panel](#admin-panel) - [Best Practices](#best-practices) --- ## Quick Reference ```php // 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 ```php $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 ```php $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 ```php 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 ```php $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). ```php $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 ```php 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: ```php // 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) ``` ```php // 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: ```php // 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: ```php 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()`: ```php $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: ```php 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`: ```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: ```php 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: ```php $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: ```php 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 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 ```php 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: ```php // 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 ```php 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: ```html user()?->can('posts.edit')): ?> Edit user()?->inGroup('admin')): ?> user()?->inGroup('admin', 'moderator')): ?>