Access control
Per-option authorization is opt-in and off by default, so existing options are never gated. Turn it on:
OPTIONS_AUTHORIZATION=true
OPTIONS_AUTHORIZATION_GATE=true # optional: also consult option.read / option.writeThen declare rules on an option. authorizeRead() and authorizeWrite() both default to true:
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Options\BaseOption;
final class MaintenanceModeOption extends BaseOption
{
public function authorizeWrite(?Authenticatable $user, ?Model $owner): bool
{
return (bool) $user?->is_admin;
}
}Options::set(MaintenanceModeOption::class, true); // throws UnauthorizedOption for non-admins
// Act on behalf of a user, or bypass entirely (e.g. system jobs):
Options::actingAs($user, fn () => Options::set(MaintenanceModeOption::class, true));
Options::withoutAuthorization(fn () => Options::set(MaintenanceModeOption::class, true));What is enforced
- Reads — get(), has(), many(), the fluent API, helpers, HasOptions, groups and the @option directive — check authorizeRead().
- Writes — set(), setMany(), group set(), remember() when it stores, forget() and reset() — check authorizeWrite().
- A denial throws UnauthorizedOption naming the option key.
- Options::all() doesn’t throw: it leaves out every option the current user may not read — and, since it can only check an option it knows, every stored key that isn’t in the string-key registry.
- Options::export() and exportJson() read the table directly and aren’t gated — don’t expose their output to users you would deny.
- The testing fake enforces the same rules when authorization is enabled.
The current user
Hooks receive the authenticated user (Auth::user(), null for guests) and the owner of the scope being accessed. actingAs() swaps the user for the duration of a callback, and withoutAuthorization() skips enforcement entirely — both restore the previous state afterwards, even when the callback throws.
Gate abilities
With authorization.use_gate on, the Gate abilities option.read and option.write are consulted too — but only when you define them. They receive the option instance and the owner, and both the hook and the ability must allow:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
use RoundlyConsulting\Options\BaseOption;
Gate::define('option.write', function (?User $user, BaseOption $option, ?Model $owner): bool {
// a user may change their own settings; only admins change global ones
return $owner !== null
? $owner->is($user)
: (bool) $user?->is_admin;
});System code and the console
System code bypasses authorization: the config bridge reads its options unguarded (it runs at boot, before any user exists), and so do the console commands — options:list, options:get and options:set. Pass --as=<userKey> to options:get or options:set to run under that user’s rules instead; the user is loaded through the default guard’s user provider:
# maintenance-mode is a registered key
php artisan options:set maintenance-mode 1 --as=1 # authorize as the user with key 1
php artisan options:get maintenance-mode # default: authorization bypassedShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.