Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Authorized Attributes Laravel Package

vantage/authorized-attributes

A Laravel package for attribute-level authorization: control which model attributes users can view or update using policies and PHP attributes, keeping sensitive fields protected across requests and resources.

View on GitHub
Deep Wiki
Context7

Getting Started

The package adds attribute-level authorization to Eloquent models: instead of only deciding whether a user can update a User, you decide which fields of that User they can read or write. That is the whole value proposition, and it maps to two recurring problems in Laravel apps: privilege escalation through mass assignment (role, is_admin, tenant_id, balance), and over-sharing in API responses.

Minimal setup:

composer require vantage/authorized-attributes

Then:

  1. Read the README end to end first. The public surface is small — a trait, a config file, and an authorization contract. Spend five minutes confirming the exact method/attribute names before you design around them; the snippets below are illustrative of the workflow, not a substitute for the README.
  2. Publish and read the config (php artisan vendor:publish --tag=...). The important question to answer immediately is the default posture: is an attribute that has no explicit authorization rule allowed or denied? Set this to fail-closed before you write any rules.
  3. Apply the trait to one model — pick your User model, since it's the one with the dangerous columns.
  4. First use case: the profile settings form. A user edits their own name/email/timezone but must never be able to submit role, is_admin, or email_verified_at. That single screen is the fastest way to validate whether the package's ergonomics fit your codebase.

If step 4 doesn't feel natural, the package probably isn't worth the dependency — the alternative is a $fillable + $guarded discipline plus a Form Request, which is what most teams do.

Implementation Patterns

Trait + declarative rules on the model. The primary pattern is defining which attributes are authorized in one place on the model, next to the other model concerns, rather than scattering $request->only([...]) across controllers. This is the biggest day-to-day win: a new sensitive column added six months from now has one obvious place to be guarded.

class User extends Authenticatable
{
    use AuthorizedAttributes;

    // illustrative — check the README for the real shape
    protected function authorizedAttributes(): array
    {
        return [
            'name'       => ['view' => true, 'update' => true],
            'email'      => ['view' => true, 'update' => true],
            'role'       => ['view' => false, 'update' => $this->can('assignRole')],
            'is_admin'   => ['view' => false, 'update' => false],
            'tenant_id'  => ['view' => false, 'update' => false],
        ];
    }
}

Filter input at the controller boundary. The most valuable integration is a single call that takes validated input and strips unauthorized keys before fill()/update(). Do this before mass assignment, not after — filtering after the write is meaningless.

$user->update($user->authorizedInput($request->validated()));

Derive Form Request rules from the same source. If your rules array (['role' => 'sometimes|in:admin,user']) and your authorization rules live in two different files, they will drift. Where the package exposes the authorized attribute list, use it to intersect with $request->validate() output. If it doesn't expose one, wrap the package call in a small helper on a base model so you do have a single accessor.

Use it in API Resources for read-side filtering. The view/read half of the package is the underrated half. role and internal notes leaking into a UserResource payload is a far more common bug than a mass-assignment hole. Call the authorized-attribute check inside toArray() so a field appears in the JSON only when the requester is allowed to see it.

Context matters — create vs. update. A field like email may be creatable by an admin but not updatable by the user. Design your rule definitions with an operation/context argument from day one, because retrofitting a "context" into the rule array later touches every model.

Policy delegation for anything non-trivial. For simple boolean columns, a closure in the attribute list is fine. Once a rule needs the target user, tenant, or subscription, move the logic into a Policy and have the attribute rule call the gate. Keeps the model file readable and makes the rules unit-testable in isolation.

Roll it out model by model. Don't add the trait to all 40 models in one PR. Order: User → anything with tenant_id/account_id → anything with money → the long tail. Each model added is one place you can now stop writing defensive $request->only().

Gotchas and Tips

  • $fillable is not replaced. The package filters what a user is authorized to touch; it does not stop Model::create($request->all()) elsewhere in the app. Keep $fillable tight as a second line of defense. If the package ever silently lets an unauthorized key through, $fillable is what saves you from a CVE in your own app.
  • Default-open is the classic footgun. If the config defaults to allowing unlisted attributes, adding a password_reset_token column tomorrow is instantly world-writable and nothing warns you. Flip the default to deny, and expect the upgrade cost to be "list every attribute once per model." Pay it.
  • hidden / visible are unrelated. $hidden is static and user-agnostic; this package is per-requester. Having both means a field can be hidden and still authorized, or visible and unauthorized depending on which layer you bypass (e.g. ->toArray() vs ->only()). Decide which layer owns serialization and be consistent.
  • Watch the per-attribute cost. Naive rule implementations run a Gate::check() (and possibly DB queries) once per attribute per model. On an index page with 50 models × 8 attributes that's 400 authorization calls. Cache the permission result per request, and prefer resolving the ability once per request-scoped "current user context" rather than inside each attribute closure.
  • Lazy-loading interaction. Attribute rules that inspect a relation ($this->tenant->owner_id) will trigger lazy loads and break under Model::preventLazyLoading(). Eager-load in authorizedAttributes() or restrict rules to the model's own columns.
  • Beware update() returning success while silently dropping fields. This is the intended behavior, but it makes UX confusing: a user edits a field, sees a 200, and nothing changed. Validate first, then filter, then write — and surface a 403 on the form for genuinely unauthorized fields rather than silently discarding them. Silent filtering hides bugs during development; a strict/debug mode that throws on unauthorized input is worth turning on locally.
  • Debugging: dump the resolved decision. When a field won't save and you can't see why, the fastest path is a one-off dd() of the authorized-attribute resolution for that model and user, not stepping through the service provider. Wrap the package call in a tiny authorizedAttributesFor($user) accessor early — you'll use it constantly, and it gives you a seam to mock in tests.
  • Test the negative cases explicitly. Feature tests asserting "admin can set role" are cheap and everyone writes them. The test that matters is "non-admin posting role=admin results in role unchanged." Write that for every sensitive column, and assert on the database, not the response.
  • Extension points to reach for before forking: the config file (defaults, strictness), the authorization contract (swap in your own resolver if you need tenant-aware logic), and the trait's rule method (override per model). Given the package's small size and low dependent count, a hard fork for anything beyond this is a maintenance liability — prefer wrapping the package in your own base model / service so an eventual replacement is a one-file change.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource