Skip to content

Authorized Integration

mozrin edited this page Nov 9, 2026 · 1 revision

Authorized Integration

$target->assignRole('admin') identifies the target user. It neither identifies nor authorizes the actor. These public PHP methods are trusted server-side operations; arbitrary trusted application code can call them. Every HTTP route, command or job exposing management must authorize its actor using host-owned rules before mutating access. Never infer the acting user from the target model.

Normal document policy

This example assumes a host-owned Document with a team_id UUID. Teams do not filter queries automatically. Register this policy through normal Laravel policy discovery or Gate::policy(Document::class, DocumentPolicy::class) in the host provider.

<?php

namespace App\Policies;

use App\Models\Document;
use App\Models\User;
use Moztopia\Access\Models\AccessTeam;

class DocumentPolicy
{
    /**
     * Authorize editing only in the document's explicit team context.
     */

    public function update(User $actor, Document $document, AccessTeam $team): bool
    {
        return $document->team_id === $team->getKey()
            && $actor->hasPermission('documents.edit', team: $team);
    }
}

In the host controller, authenticate using the host's existing route middleware, load the resource in the explicit team context, then authorize through Laravel:

$document = Document::query()
    ->where('team_id', $team->getKey())
    ->findOrFail($documentId);

Gate::authorize('update', [$document, $team]);

The team travels with the policy arguments. No static current-team setter, middleware state or hidden tenancy scope is needed. Do not substitute a global permission check for the team check when membership is required. A global document policy can deliberately call hasPermission('documents.edit') without a team for genuinely application-wide resources.

Authorize management separately

Define access.manage through an owner-controlled provisioning path, not an unprotected request. The following host-owned team policy allows explicitly scoped managers to grant the fixed editor role to an existing member. It does not allow a client to submit arbitrary roles or a global scope. Register it normally:

Gate::policy(AccessTeam::class, AccessTeamPolicy::class);
<?php

namespace App\Policies;

use App\Models\User;
use Moztopia\Access\Models\AccessTeam;

class AccessTeamPolicy
{
    /**
     * Let the host decide who may grant the fixed editor role.
     */

    public function grantEditor(User $actor, AccessTeam $team, User $target): bool
    {
        return $actor->hasPermission('access.manage', team: $team)
            && $target->hasTeam($team);
    }
}
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Gate;
use Moztopia\Access\Models\AccessTeam;

class TeamEditorController extends Controller
{
    /**
     * Authorize the current actor before changing a different target user.
     */

    public function store(AccessTeam $team, User $target): Response
    {
        Gate::authorize('grantEditor', [$team, $target]);
        $target->assignRole('editor', team: $team);

        return response()->noContent();
    }
}

The host defines an authenticated, CSRF-protected route appropriate to its application. The package defines no route. Gate::authorize resolves the host's authenticated actor, while $target stays the mutation target. If membership changes after policy evaluation, the mutation checks it again under package locks. If the host needs the actor's management authority to remain fixed across the entire change, it must coordinate actor authorization and mutation within its business transaction as described in the concurrency boundary.

Global role assignment deserves a separate host policy for global management; do not accidentally omit team: in a team-management controller. Likewise, constrain and authorize any user-supplied role/permission selection before mutation. Never implement a blanket Gate::before bypass for a role named admin; define the exact permitted abilities instead. Existing host policies and explicit denials continue to govern native Laravel authorization.

For queued management, persist the initiating actor's identity and the exact scope in host-owned job data, then re-authorize according to the host's job policy when executing. Do not assume the target is the initiator or rely on ambient request authentication in a worker.

Clone this wiki locally