---
title: "Access control — Options for Laravel | Roundly"
description: "Opt-in per-option read and write rules, Gate abilities, acting as a user, bypassing for system jobs and CLI authorization."
url: https://roundly-consulting.com/open-source/docs/options-for-laravel/access-control
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Options for Laravel](https://roundly-consulting.com/open-source/docs/options-for-laravel.md)

# Access control

Per-option authorization is opt-in and off by default, so existing options are never gated. Turn it on:

```bash
OPTIONS_AUTHORIZATION=true
OPTIONS_AUTHORIZATION_GATE=true   # optional: also consult option.read / option.write
```

Then declare rules on an option. authorizeRead() and authorizeWrite() both default to true:

```php
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;
    }
}
```

```php
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.
- export(), exportJson() and for($owner)->export() filter the same way: a row the current user may not read — or whose key no registered option answers for, or whose owner no longer exists — is left out. Each owned row is checked against its own owner.
- import() checks every row before the first write and throws UnauthorizedOption for a row the user may not write, an unregistered key or an owner that doesn’t exist — nothing is imported.
- 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:

```php
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, options:set, options:export and options:import. 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:

```bash
# 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 bypassed
```

## Show 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/options-for-laravel.md)
