laraveldaily/ai-workflows-for-laravel/laraveldaily-permissions-audit
Claude Code and agent skill by laraveldaily/ai-workflows-for-laravel
- community
Checking that the SKILL.md file is still in the repository.
This skill should be used when the user asks to "audit Laravel permissions", "review roles and permissions", "check authorization code", "find permission issues", "permissions review", "audit access control", or wants to analyze a Laravel project's roles/permissions implementation for improvements. Scans all PHP source files and reports findings with actionable suggestions.
Install
npx vskill@latest install laraveldaily/ai-workflows-for-laravel/laraveldaily-permissions-auditSKILL.md
Laravel Roles & Permissions Audit
Analyze a Laravel project's roles, permissions, and authorization implementation. Scan all PHP source files (excluding vendor/, node_modules/, storage/) and produce a structured report of findings with actionable suggestions.
This is NOT a security penetration test. Focus on authorization architecture, role/permission patterns, and access control implementation quality.
What to Check
1. UI-Only Authorization Without Backend Protection
Scan for cases where authorization is checked in Blade templates but NOT enforced in the corresponding controller methods.
Why it matters: Hiding a button with @can or @if(auth()->user()->isAdmin()) does not prevent a direct HTTP request to the endpoint. Every authorization check in the UI must have a matching backend check. This is a real security gap, not a style issue.
What to flag:
@can,@if(auth()->user()->...), or role/permission checks in Blade templates where the corresponding controller method has noGate::authorize(),$this->authorize(),#[Authorize], Policy check, orcanmiddleware on the route- Form actions or links hidden with
@canin Blade but the POST/PUT/DELETE endpoint has no authorization - Admin-only navigation items hidden with
@ifchecks but the underlying routes have no middleware or Policy protection
Suggestion: Every @can check in Blade should have a matching authorization check on the backend. Use Gate::authorize('ability', $model) in the controller, #[Authorize] attributes on controller methods, or ->can('ability,model') on the route definition. The UI check is cosmetic — the backend check is the actual security.
2. Inline Role/Permission Checks Instead of Policies
Scan controllers and middleware for manual role and permission checks instead of using Laravel's Policy system.
Why it matters: Authorization logic scattered across controllers is easy to forget in new endpoints, hard to audit, and cannot be reused in Blade (@can), route definitions, or Gates. Policies centralize authorization per model and are auto-discovered in Laravel 11+.
What to flag:
if ($user->role === 'admin'),if ($user->is_admin),if ($user->hasRole(...))directly in controller methods to decide accessabort(403)orabort_if/abort_unlesswith role or ownership conditions in controllersif ($user->id !== $model->user_id)ownership checks in controllers- Same authorization condition copy-pasted across multiple controller methods
- Ignore middleware-level role checks (e.g.,
role:adminmiddleware on route groups) — those are appropriate for broad route protection
Suggestion: Create a Policy class using php artisan make:policy PostPolicy --model=Post. Move access decisions to Policy methods (viewAny, view, create, update, delete). Call them via Gate::authorize(), #[Authorize] attributes, @can in Blade, or ->can() on route definitions. In Laravel 11+, Policies are auto-discovered — no manual registration needed.
3. Checking Roles Instead of Permissions
Scan for authorization code that checks role names rather than permission names.
Why it matters: Checking hasRole('admin') ties your logic to a specific role. If you later add a "manager" role that should also access that feature, you must find and update every hasRole('admin') check. Checking hasPermissionTo('edit-post') instead means you only update the role-permission mapping in one place (the seeder or admin UI). This is the fundamental principle of permission-based access control.
What to flag:
$user->hasRole('...')or$user->hasAnyRole([...])used to gate specific actions (not broad area access)- Policy methods that check role names:
$user->role === 'admin'or$user->hasRole('admin')instead of checking a permission - Middleware using
role:adminon individual routes rather than route groups for broad sections - Multiple role checks combined with
||:$user->hasRole('admin') || $user->hasRole('manager')— this is a sign the check should be a permission
Exception: Checking roles is acceptable for broad area access (e.g., role:admin middleware on an entire admin route group) or when the role itself IS the distinction (master admin bypass). Do not flag Gate::before() super-admin bypasses or route-group-level role middleware.
Suggestion: Define granular permissions (create-post, edit-post, delete-post) and assign them to roles in a seeder or admin UI. Check $user->hasPermissionTo('edit-post') or $user->can('edit-post') in application logic. This way, adding a new role that can edit posts requires only a seeder change, not a code change.
4. Boolean is_admin Column When Multiple Roles Exist
Scan for boolean admin flags on the User model when the application clearly has more than two user types.
Why it matters: A boolean is_admin column works for a strict two-role app (admin vs user). But when the application has admin, manager, editor, or any third role, a boolean cannot represent it. This becomes a source of hacks like adding is_manager, is_editor columns.
What to flag:
is_adminboolean column on users table alongside other role-like columns (is_manager,is_editor,is_moderator)- Multiple boolean role columns on the same table
- Code that checks
$user->is_adminalongside other role-like conditions, suggesting more than two user types exist is_adminboolean used in a project that clearly has 3+ distinct user types based on its controllers, routes, or middleware
Do NOT flag: A simple app with genuinely only two user types (admin and regular user) using is_admin — this is a valid and simple approach for small projects.
Suggestion: Replace boolean columns with a proper roles system. For a fixed set of roles: add a role column backed by a PHP Enum and a roles DB table. For flexible/growing roles: use Spatie laravel-permission package. Choose one-to-many (user has one role) or many-to-many (user has multiple roles) based on whether users genuinely need to hold multiple roles simultaneously.
5. Hardcoded Role/Permission Strings Without Enums
Scan for role and permission names used as raw strings scattered across the codebase.
Why it matters: When role names are strings like 'admin' or 'clinic-owner' scattered across Policies, middleware, seeders, and Blade templates, a typo compiles silently and fails at runtime. PHP Enums provide type safety, IDE auto-completion, and a single source of truth for all valid roles and permissions.
What to flag:
hasRole('admin'),hasPermissionTo('edit-post'),assignRole('user')with raw strings instead of Enum references- Spatie's
Role::create(['name' => 'admin'])in seeders using raw strings instead of Enum values @can('edit-post')in Blade orGate::define('edit-post', ...)with hardcoded stringsin:admin,editor,uservalidation rules with hardcoded role lists- Same role/permission string appearing in 3+ different files
Suggestion: Create App\Enums\Role and App\Enums\Permission as string-backed PHP Enums. Use Role::ADMIN->value in seeders, Permission::EDIT_POST->value in Policies, and reference the Enum class in validation rules. When using Spatie, use enum_value() or ->value when passing to Spatie methods that expect strings.
6. Missing Data Scoping (Authorization Without Query Filtering)
Scan for projects that check permissions but do not scope database queries to the authorized data.
Why it matters: A Policy that says "this user can view tasks" does not prevent them from viewing ALL tasks. If users should only see their own data (or their team's data), you need Global Scopes or query-level filtering in addition to Policies. Without scoping, a user who guesses another record's ID can access it even with Policies in place (Policies check ability, not record visibility by default).
What to flag:
- Controllers that query
Model::all()orModel::paginate()without any user/team scoping when different users should see different records Model::findOrFail($id)without a scope limiting to the user's own records or team- Multi-tenant or multi-team apps where queries don't filter by
team_id,organization_id, oruser_id - Policies that authorize the action but the controller still fetches unscoped data
Suggestion: Use Global Scopes to automatically filter queries by the authenticated user's team or ownership. For example, a TeamScope that adds ->where('team_id', auth()->user()->current_team_id) to all queries on team-scoped models. This provides defense in depth: the Policy authorizes the action, and the scope ensures only permitted records are visible. Note: Global Scopes cause 404 (record not found) instead of 403 (forbidden) for cross-team/cross-user access — this is usually desirable as it doesn't leak that the record exists.
7. Missing Authorization Middleware on Routes
Scan route files for unprotected routes that should require authorization.
Why it matters: Every route that serves authenticated content should be behind auth middleware at minimum. Routes that serve role-specific content should additionally have role or permission middleware, or the controller should use Policies. Unprotected routes are the most basic authorization gap.
What to flag:
- Routes inside
web.phporapi.phpthat point to controllers containing authorization checks but the routes themselves are not inside anauthmiddleware group - Admin-prefixed routes (
/admin/*) withoutauthmiddleware or role/permission middleware - Resource routes where some methods need authorization but no middleware or Policy is applied
- Routes that rely solely on controller-level auth checks with no middleware — while this works, combining middleware for broad access (auth, role) with Policies for fine-grained access is more robust
Suggestion: Wrap authenticated routes in Route::middleware('auth') groups. For role-based areas, add role middleware on route groups: Route::middleware(['auth', 'role:admin']). For fine-grained access, use Policies in controllers and ->can() on individual route definitions. The layered approach (middleware for broad access + Policies for specific actions) is the most secure.
8. Overly Complex Role System for Simple Needs
Scan for over-engineered role/permission setups in small applications.
Why it matters: A 5-controller CRUD app with Spatie laravel-permission, 15 granular permissions, a role-permission seeder, and team-based scoping is over-engineered if the app only has "admin" and "user" roles. The right solution depends on the project's actual complexity. A simple is_admin column or a role Enum column is often sufficient.
What to flag:
- Spatie
laravel-permissioninstalled but only 2 roles and no permission-level checks anywhere (everything checks roles) - Permission tables with entries that map 1:1 to roles (every permission assigned to exactly one role) — this means permissions add no value over just checking roles
- Complex role/permission seeders for an app with fewer than 5 controllers
- Team-based permissions (
teamsfeature enabled in Spatie config) when the app has no multi-tenancy concept - Repository pattern + Service layer + Action classes + DTOs all wrapping simple role checks
Do NOT flag: Consistent use of a permissions package in a growing application, even if current complexity is modest — planning ahead is reasonable if the codebase shows signs of growth.
Suggestion: Match the authorization approach to the project's actual complexity:
- 2 roles (admin/user):
is_adminboolean + simple middleware is fine - 3-5 fixed roles:
roleEnum column + Policies, no package needed - Multiple roles per user OR dynamic roles: Spatie
laravel-permission - Multi-tenant with per-team roles: Spatie with teams feature enabled
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/laraveldaily-permissions-audit/SKILL.md
Popularity
- 7-day trend score
- 0
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/laraveldaily/ai-workflows-for-laravel/laraveldaily-permissions-audit)