whilesmart/eloquent-owner-access

Pluggable polymorphic owner authorization for Laravel packages with owner_type/owner_id columns. Lets host apps decide who can see and modify owner-scoped records without coupling the underlying packages to a specific tenancy model.

Maintainers

Package info

github.com/whilesmartphp/eloquent-owner-access

pkg:composer/whilesmart/eloquent-owner-access

Statistics

Installs: 35

Dependents: 5

Suggesters: 0

Stars: 0

Open Issues: 0

dev-dev 2026-04-25 11:40 UTC

This package is auto-updated.

Last update: 2026-04-25 11:53:46 UTC


README

Pluggable polymorphic owner authorization for Laravel packages with owner_type / owner_id columns.

Domain packages (accounts, expenses, payments, invoices, products, customers, ...) expose CRUD endpoints scoped by a polymorphic owner. They should not have to know how the host app decides who owns what. This package gives them a single contract to consult and a default that preserves existing "trust the client" behavior, so adoption is incremental and safe.

Concept

One contract:

interface OwnerAuthorizer
{
    public function authorize(?Authenticatable $user, string $ownerType, mixed $ownerId): bool;

    public function scope(
        Builder $query,
        ?Authenticatable $user,
        string $ownerTypeColumn = 'owner_type',
        string $ownerIdColumn = 'owner_id',
    ): Builder;
}

authorize() is per-record (called from FormRequest::authorize() for store/update and from controllers for show/destroy). scope() constrains index queries.

The default binding is AllowAllAuthorizer (everything goes through). Hosts that want strict tenancy bind their own implementation.

Installation

composer require whilesmart/eloquent-owner-access

The service provider auto-registers via Laravel package discovery and bindIfs the default authorizer.

Usage in a package

In a StoreXRequest:

use Whilesmart\OwnerAccess\Concerns\AuthorizesOwnerRequest;

class StoreAccountRequest extends FormRequest
{
    use AuthorizesOwnerRequest;

    public function authorize(): bool
    {
        return $this->authorizeOwnerInRequest();
    }

    public function rules(): array { /* ... */ }
}

In an UpdateXRequest:

public function authorize(): bool
{
    return $this->authorizeOwnerOfBoundModel('account');
}

In the controller:

use Whilesmart\OwnerAccess\Concerns\AuthorizesOwnerController;

class AccountController extends Controller
{
    use AuthorizesOwnerController;

    public function index(Request $request)
    {
        $query = Account::query();
        $query = $this->scopeAccessibleOwners($query, $request->user());
        // ...
    }

    public function show(Account $account, Request $request)
    {
        $this->authorizeAccessTo($account, $request->user());
        // ...
    }

    public function destroy(Account $account, Request $request)
    {
        $this->authorizeAccessTo($account, $request->user());
        $account->delete();
        // ...
    }
}

Update is already covered by UpdateXRequest::authorize(), so no controller change is needed there.

Usage in a host app

By default everything is allowed. To enforce tenancy, write an authorizer that consults your tenancy model and bind it:

namespace App\Authorization;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Whilesmart\OwnerAccess\Contracts\OwnerAuthorizer;

class WorkspaceMemberAuthorizer implements OwnerAuthorizer
{
    public function authorize(?Authenticatable $user, string $ownerType, mixed $ownerId): bool
    {
        if ($user === null || $ownerType !== \App\Models\Workspace::class) {
            return false;
        }

        return $user->workspaces()->whereKey($ownerId)->exists();
    }

    public function scope(Builder $query, ?Authenticatable $user, string $ownerTypeColumn = 'owner_type', string $ownerIdColumn = 'owner_id'): Builder
    {
        if ($user === null) {
            return $query->whereRaw('0 = 1');
        }

        return $query->where($ownerTypeColumn, \App\Models\Workspace::class)
            ->whereIn($ownerIdColumn, $user->workspaces()->pluck('workspaces.id'));
    }
}

Then in AppServiceProvider::register():

$this->app->bind(
    \Whilesmart\OwnerAccess\Contracts\OwnerAuthorizer::class,
    \App\Authorization\WorkspaceMemberAuthorizer::class,
);

That single binding switches every consuming package over to strict mode at once.

Backwards compatibility

The provider uses bindIf, so any explicit binding the host registers (before or after) wins. Without an explicit binding, the default AllowAllAuthorizer keeps current behavior.

Testing

composer test

License

MIT.