Skip to content

Authorization & Access Control ​

Last updated: 2026-03-22

Table of Contents ​

  1. Overview
  2. Security Warning
  3. Quick Start
  4. Default Behavior
  5. Enabling Authorization
  6. Configuration Options
  7. Adding Ownership Tracking
  8. Custom Policy
  9. Spatie Permission Integration
  10. Common Scenarios
  11. Best Practices
  12. Troubleshooting

Overview ​

The Filament Address Pro package includes a comprehensive authorization system that controls who can:

  • View addresses (list and individual records)
  • Create new addresses
  • Edit existing addresses
  • Delete addresses
  • Perform bulk operations (verify, import, export)

The system uses Laravel's native Policy system, making it flexible, extensible, and compatible with your existing authorization infrastructure.


Security Warning ​

⚠️ CRITICAL: Default Configuration is NOT Secure for Production ​

By default, authorization is DISABLED for backward compatibility. This means:

ALL authenticated users have FULL ACCESS to:

  • View all addresses (globally)
  • Create/edit/delete any address
  • Run bulk operations (import, export, verify)
  • Trigger API calls (incurring costs)
  • Access cache and audit logs

When is this acceptable? ​

✅ Safe for:

  • Demo/development environments
  • Single-user applications
  • Solo entrepreneur/freelancer use cases
  • Prototyping and testing

❌ NOT SAFE for:

  • Multi-user applications (teams, organizations)
  • Multi-tenant SaaS platforms
  • Production applications with sensitive data
  • Applications requiring compliance (GDPR, SOC2, HIPAA)

Risks of Unrestricted Access ​

  1. Data Privacy Risk 🔴 CRITICAL

    • User A can view/edit User B's addresses
    • In multi-tenant apps: Customer A sees Customer B's data
    • No GDPR compliance possible without access controls
    • Example: SaaS with 10 companies → all can see each other's addresses
  2. Cost Control Risk 🔴 HIGH

    • Any user can trigger unlimited API calls
    • Bulk verify without approval → unexpected charges
    • No spending limits or admin approval required
    • Example: Junior staff runs "Verify All" on 10K addresses = $50-100 API cost
  3. Data Integrity Risk 🟡 MEDIUM

    • Any user can delete any address
    • Bulk operations without safeguards
    • Import can overwrite data
    • No audit trail of who did what
  4. Compliance Risk 🔴 HIGH

    • Cannot meet GDPR requirements (no data access restrictions)
    • No SOC2 audit trail capabilities
    • No HIPAA field-level restrictions
    • No segregation of duties

Quick Start ​

For Production Multi-User Applications ​

Minimum required configuration for production:

env
# .env file
ADDRESS_AUTHORIZATION_ENABLED=true

This enables authorization with sensible defaults. Then configure specific permissions:

php
// config/addresses.php
'authorization' => [
    'enabled' => true,

    // Restrict bulk operations to admins
    'bulk_verify' => fn($user) => $user->hasRole('admin'),
    'import' => fn($user) => $user->hasRole('admin'),
    'export' => fn($user) => $user->hasRole('admin'),
],

Default Behavior ​

When authorization is disabled (default):

php
// config/addresses.php
'authorization' => [
    'enabled' => false,  // ← Default
],

Result: All authenticated users can perform ALL operations.

The AddressPolicy checks return true for all authenticated users:

  • ✅ View all addresses
  • ✅ Create addresses
  • ✅ Edit any address
  • ✅ Delete any address
  • ✅ Bulk verify, import, export

UI Behavior:

  • "New Address" button: Visible to all
  • "Edit" action: Visible for all records
  • "Delete" action: Visible for all records
  • Bulk actions: All visible

Enabling Authorization ​

env
# .env
ADDRESS_AUTHORIZATION_ENABLED=true

This enables authorization checks while using the default permissions from config.

Method 2: Config File ​

php
// config/addresses.php
'authorization' => [
    'enabled' => true,
],

Method 3: Dynamic (Runtime) ​

php
// In a service provider or middleware
config(['addresses.authorization.enabled' => true]);

Configuration Options ​

All authorization options are in config/addresses.php under the authorization section.

Available Permissions ​

Each permission can be:

  • Boolean: true (allow all) or false (deny all)
  • Closure: fn($user) => $user->hasRole('admin')
  • Null: Inherit from parent permission

View Permissions ​

php
'authorization' => [
    // Can user access the address list?
    'view_any' => true,  // Default: allow all

    // Can user view a specific address?
    // null = inherit from view_any
    'view' => null,

    // Example: Only view own addresses
    // 'view' => fn($user, $address) => $address->created_by === $user->id,
],

Note: The created_by field is not included in the default migration. Row-level ownership tracking requires custom schema modifications. See Adding Ownership Tracking below for implementation details.

Modify Permissions ​

php
'authorization' => [
    // Can user create new addresses?
    'create' => true,

    // Can user edit addresses?
    'update' => true,

    // Example: Only edit own addresses
    // 'update' => fn($user, $address) => $address->created_by === $user->id,

    // Can user delete addresses?
    'delete' => true,

    // Example: Admin only
    // 'delete' => fn($user) => $user->hasRole('admin'),
],

Bulk Operation Permissions ​

⚠️ RECOMMENDATION: Restrict these to admin roles for production.

php
'authorization' => [
    // Bulk verification (triggers API calls, incurs costs)
    'bulk_verify' => true,  // ⚠️ Default allows all

    // Import addresses (can overwrite data)
    'import' => true,  // ⚠️ Default allows all

    // Export addresses (contains sensitive data)
    'export' => true,  // ⚠️ Default allows all
],

Production Example:

php
'authorization' => [
    'bulk_verify' => fn($user) => $user->hasRole('admin'),
    'import' => fn($user) => $user->hasAnyRole(['admin', 'data-manager']),
    'export' => fn($user) => $user->hasAnyRole(['admin', 'analyst']),
],

Environment Variables ​

All permissions support environment variable overrides:

env
# View permissions
ADDRESS_AUTH_VIEW_ANY=true
ADDRESS_AUTH_VIEW=true

# Modify permissions
ADDRESS_AUTH_CREATE=true
ADDRESS_AUTH_UPDATE=true
ADDRESS_AUTH_DELETE=false  # Disable delete for all users

# Bulk operations
ADDRESS_AUTH_BULK_VERIFY=false  # Disable bulk verify
ADDRESS_AUTH_IMPORT=false       # Disable import
ADDRESS_AUTH_EXPORT=false       # Disable export

Note: Environment variables only support boolean values. For role-based or complex logic, use closures in the config file.


Adding Ownership Tracking ​

The addresses table does not include created_by or updated_by columns by default. This keeps the package flexible and avoids assumptions about your authentication system (integer IDs, UUIDs, ULIDs, etc.).

When Do You Need This? ​

Add ownership tracking if you need:

  • Row-level authorization (users can only edit their own addresses)
  • Audit trails (who created/modified each address)
  • Multi-tenant data segregation
  • Compliance requirements (GDPR, SOC2, HIPAA)

Implementation Steps ​

1. Publish the Migration ​

bash
php artisan vendor:publish --tag=filament-address-migrations

2. Customize the Migration ​

Edit database/migrations/*_create_addresses_table.php and add ownership fields:

php
Schema::create('addresses', function (Blueprint $table) {
    // ... existing fields ...

    // Add ownership tracking
    // Choose the appropriate type for your auth system:
    $table->unsignedBigInteger('created_by')->nullable();  // Standard Laravel
    // $table->uuid('created_by')->nullable();              // UUID users
    // $table->ulid('created_by')->nullable();              // ULID users

    $table->unsignedBigInteger('updated_by')->nullable();  // Optional: track updates

    // Add foreign keys (adjust table name if needed)
    $table->foreign('created_by')
        ->references('id')
        ->on('users')
        ->nullOnDelete();  // Set to null if user is deleted

    $table->foreign('updated_by')
        ->references('id')
        ->on('users')
        ->nullOnDelete();

    // ... rest of migration ...
});

3. Update the Address Model ​

Add fillable fields and relationships:

php
// app/Models/Address.php (if using custom model)
// or extend the package model

protected $fillable = [
    // ... existing fields ...
    'created_by',
    'updated_by',
];

public function creator()
{
    return $this->belongsTo(User::class, 'created_by');
}

public function updater()
{
    return $this->belongsTo(User::class, 'updated_by');
}

4. Auto-Populate on Create/Update ​

Add an observer or model boot method:

php
// In AddressServiceProvider or AppServiceProvider

Address::creating(function ($address) {
    if (auth()->check() && !$address->created_by) {
        $address->created_by = auth()->id();
    }
});

Address::updating(function ($address) {
    if (auth()->check()) {
        $address->updated_by = auth()->id();
    }
});

5. Use in Authorization ​

Now you can use ownership checks in your config:

php
'authorization' => [
    'enabled' => true,

    // Users can only view their own addresses
    'view' => fn($user, $address) => $address->created_by === $user->id,

    // Admins can edit all, users can edit only their own
    'update' => fn($user, $address) =>
        $user->hasRole('admin') || $address->created_by === $user->id,

    // Only admins can delete
    'delete' => fn($user) => $user->hasRole('admin'),
],

Alternative: Full Audit Trail ​

For comprehensive auditing (what changed, when, why), consider dedicated audit packages:

These packages provide full audit trails including:

  • Old and new values for each field
  • User agent, IP address, timestamps
  • Restoration capabilities
  • Query interfaces for audit logs

Custom Policy ​

For advanced authorization requirements, copy and customize the AddressPolicy:

Step 1: Copy the Policy ​

bash
php artisan vendor:publish --tag=filament-address-policies

This creates: app/Policies/AddressPolicy.php

Step 2: Customize the Policy ​

php
<?php

namespace App\Policies;

use App\Models\User;
use Viewflex\FilamentAddress\Models\Address;
use Viewflex\FilamentAddress\Policies\AddressPolicy as BasePolicy;

class AddressPolicy extends BasePolicy
{
    /**
     * Only allow users to view addresses they created.
     */
    public function view(User $user, Address $address): bool
    {
        return $address->created_by === $user->id;
    }

    /**
     * Only admins can delete addresses.
     */
    public function delete(User $user, Address $address): bool
    {
        return $user->hasRole('admin');
    }

    /**
     * Only allow bulk verify for users with remaining API quota.
     */
    public function bulkVerify(User $user): bool
    {
        if (!$user->hasRole('admin')) {
            return false;
        }

        // Check if user/tenant has remaining quota
        return $user->tenant->api_quota_remaining > 0;
    }
}

Note: The example above uses $address->created_by, which requires adding ownership tracking to your schema. See Adding Ownership Tracking for implementation steps.

Step 3: Register Your Custom Policy ​

php
// config/addresses.php
'authorization' => [
    'enabled' => true,
    'policy' => \App\Policies\AddressPolicy::class,  // ← Your custom policy
],

Spatie Permission Integration ​

The package automatically detects and uses Spatie Laravel Permission if installed. No additional configuration needed!

Installation ​

bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate

Permission Names ​

The policy automatically checks for these permissions:

  • view-addresses - Can view address list
  • create-addresses - Can create addresses; also controls the "Recalculate Quality Scores" header and bulk actions
  • update-addresses - Can edit addresses
  • delete-addresses - Can delete addresses; also controls the "Delete Selection" bulk action
  • verify-addresses - Can bulk verify
  • import-addresses - Can import addresses
  • export-addresses - Can export addresses

Note on Recalculate Quality Scores: Both the header button and the bulk action are gated behind create-addresses (not update-addresses). The update policy requires a specific model instance and cannot be used for class-level permission checks. Users with only view-addresses will not see these actions.

Creating Roles and Permissions ​

php
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

// Create permissions
Permission::create(['name' => 'view-addresses']);
Permission::create(['name' => 'create-addresses']);
Permission::create(['name' => 'update-addresses']);
Permission::create(['name' => 'delete-addresses']);
Permission::create(['name' => 'verify-addresses']);
Permission::create(['name' => 'import-addresses']);
Permission::create(['name' => 'export-addresses']);

// Create roles
$admin = Role::create(['name' => 'admin']);
$staff = Role::create(['name' => 'staff']);
$viewer = Role::create(['name' => 'viewer']);

// Assign permissions to roles
$admin->givePermissionTo([
    'view-addresses',
    'create-addresses',
    'update-addresses',
    'delete-addresses',
    'verify-addresses',
    'import-addresses',
    'export-addresses',
]);

$staff->givePermissionTo([
    'view-addresses',
    'create-addresses',
    'update-addresses',
]);

$viewer->givePermissionTo([
    'view-addresses',
]);

// Assign role to user
$user->assignRole('staff');

How It Works ​

The AddressPolicy automatically detects if your User model has the hasPermissionTo() method (from Spatie):

php
public function viewAny(User $user): bool
{
    // If Spatie is installed and user has the method
    if (method_exists($user, 'hasPermissionTo')) {
        return $user->hasPermissionTo('view-addresses');
    }

    // Fallback to config
    return config('addresses.authorization.view_any', true);
}

No configuration needed — it just works.


Common Scenarios ​

Scenario 1: Small Team (Admin + Staff) ​

php
// config/addresses.php
'authorization' => [
    'enabled' => true,

    // Everyone can view and create
    'view_any' => true,
    'create' => true,

    // Only admins can edit/delete
    'update' => fn($user) => $user->hasRole('admin'),
    'delete' => fn($user) => $user->hasRole('admin'),

    // Only admins can do bulk operations
    'bulk_verify' => fn($user) => $user->hasRole('admin'),
    'import' => fn($user) => $user->hasRole('admin'),
    'export' => fn($user) => $user->hasRole('admin'),
],

Scenario 2: Users Can Only Edit Their Own ​

php
'authorization' => [
    'enabled' => true,

    // Everyone can view list (but might filter in query)
    'view_any' => true,

    // Can only view own addresses
    'view' => fn($user, $address) => $address->created_by === $user->id,

    // Can create for themselves
    'create' => true,

    // Can only edit own
    'update' => fn($user, $address) => $address->created_by === $user->id,

    // Can only delete own
    'delete' => fn($user, $address) => $address->created_by === $user->id,

    // No bulk operations for regular users
    'bulk_verify' => false,
    'import' => false,
    'export' => false,
],

Scenario 3: Department-Based Access ​

php
'authorization' => [
    'enabled' => true,

    // Sales: View and create
    // Finance: Export for billing
    // Admin: Full access

    'view_any' => fn($user) => $user->hasAnyRole(['sales', 'finance', 'admin']),
    'create' => fn($user) => $user->hasAnyRole(['sales', 'admin']),
    'update' => fn($user) => $user->hasRole('admin'),
    'delete' => fn($user) => $user->hasRole('admin'),

    'bulk_verify' => fn($user) => $user->hasRole('admin'),
    'import' => fn($user) => $user->hasRole('admin'),
    'export' => fn($user) => $user->hasAnyRole(['finance', 'admin']),
],

Scenario 4: Quota-Based Restrictions ​

php
// Custom policy
public function bulkVerify(User $user): bool
{
    if (!$user->hasRole('admin')) {
        return false;
    }

    // Check daily quota
    $todayUsage = Address::where('verified_at', '>=', today())
        ->where('verification_triggered_by', $user->id)
        ->count();

    return $todayUsage < 100; // Max 100 verifications per day
}

Best Practices ​

1. Enable Authorization Before Launch ​

Don't wait until after launch. Enable authorization during development to catch issues early.

env
ADDRESS_AUTHORIZATION_ENABLED=true

2. Restrict Bulk Operations ​

Bulk operations can incur significant costs. Always restrict to trusted users:

php
'bulk_verify' => fn($user) => $user->hasRole('admin'),
'import' => fn($user) => $user->hasRole('admin'),
'export' => fn($user) => $user->hasRole('admin'),

3. Implement Row-Level Security Early ​

Even if all users can view the list, implement per-record checks:

php
'view' => fn($user, $address) => $address->tenant_id === $user->tenant_id,

4. Use Spatie Permission for Complex Roles ​

For 3+ roles with varying permissions, use Spatie Permission instead of closures:

bash
composer require spatie/laravel-permission

Then assign permissions via database instead of hard-coded logic.

5. Log Authorization Failures ​

For compliance, log when users are denied access:

php
// In custom policy
public function delete(User $user, Address $address): bool
{
    $canDelete = $user->hasRole('admin');

    if (!$canDelete) {
        \Log::warning('Authorization denied', [
            'user' => $user->id,
            'action' => 'delete',
            'address' => $address->id,
        ]);
    }

    return $canDelete;
}

6. Test Authorization Logic ​

Include authorization tests in your test suite:

php
test('staff cannot delete addresses', function () {
    $staff = User::factory()->create();
    $staff->assignRole('staff');

    $address = Address::factory()->create();

    expect(Gate::allows('delete', $address))->toBeFalse();
});

7. Document Your Authorization Rules ​

Create a PERMISSIONS.md file documenting which roles can do what:

markdown
# Address Permissions

## Roles

### Admin
- Full access to all addresses
- Can verify, import, export

### Staff
- View all addresses
- Create new addresses
- Cannot edit/delete

### Viewer
- View addresses only
- No modifications allowed

Troubleshooting ​

Problem: "New Address" Button Not Appearing ​

Cause: User doesn't have create permission.

Solution:

php
// Check in tinker
Gate::allows('create', \Viewflex\FilamentAddress\Models\Address::class);

// Enable create permission
config(['addresses.authorization.create' => true]);

Problem: All Actions Hidden After Enabling Authorization ​

Cause: Config not loaded or policy not registered.

Solution:

bash
# Clear config cache
php artisan config:clear

# Check if policy is registered
php artisan tinker
>>> Gate::getPolicyFor(\Viewflex\FilamentAddress\Models\Address::class)

Problem: Spatie Permissions Not Working ​

Cause: User model doesn't have the HasRoles trait.

Solution:

php
// app/Models/User.php
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}

Problem: Authorization Works in Tinker But Not in UI ​

Cause: Different user context or cache issue.

Solution:

bash
# Clear all caches
php artisan cache:clear
php artisan config:clear
php artisan view:clear

# Check which user is authenticated in the panel
dd(auth()->user(), auth()->id());

Problem: Custom Policy Not Being Used ​

Cause: Config still points to default policy.

Solution:

php
// config/addresses.php
'authorization' => [
    'policy' => \App\Policies\AddressPolicy::class,  // ← Make sure this is correct
],

// Then clear config
php artisan config:clear

Multi-Tenancy Support ​

Status: Deferred to a future release.

Multi-tenancy is not built in, but you can implement tenant scoping today with a custom policy:

php
// Custom policy
public function view(User $user, Address $address): bool
{
    // Check if address belongs to user's tenant
    return $address->tenant_id === $user->tenant_id;
}

// Apply global scope to all queries
// app/Providers/AppServiceProvider.php
Address::addGlobalScope('tenant', function ($query) {
    if (auth()->check() && auth()->user()->tenant_id) {
        $query->where('tenant_id', auth()->user()->tenant_id);
    }
});

Additional Resources ​

Released under a commercial license.