Implements Spec 089: moves Provider Connections to canonical tenantless route under `/admin/provider-connections`, enforces 404/403 semantics (workspace/tenant membership vs capability), adds tenant transparency (tenant column + filter + deep links), adds legacy redirects for old tenant-scoped URLs without leaking Location for 404 cases, and adds regression test coverage (RBAC semantics, filters, UI enforcement tooltips, Microsoft-only MVP scope, navigation placement). Notes: - Filament v5 / Livewire v4 compatible. - Global search remains disabled for Provider Connections. - Destructive/manage actions require confirmation and are policy-gated. Tests: - `vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #107
250 lines
7.3 KiB
PHP
250 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Policies;
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantMembership;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
|
use Illuminate\Auth\Access\Response;
|
|
use Illuminate\Support\Facades\Gate;
|
|
|
|
class ProviderConnectionPolicy
|
|
{
|
|
use HandlesAuthorization;
|
|
|
|
public function viewAny(User $user): Response|bool
|
|
{
|
|
$workspace = $this->currentWorkspace($user);
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
$entitledTenants = Tenant::query()
|
|
->select('tenants.*')
|
|
->join('tenant_memberships as policy_memberships', function ($join) use ($user): void {
|
|
$join->on('policy_memberships.tenant_id', '=', 'tenants.id')
|
|
->where('policy_memberships.user_id', '=', (int) $user->getKey());
|
|
})
|
|
->where('tenants.workspace_id', (int) $workspace->getKey())
|
|
->get();
|
|
|
|
if ($entitledTenants->isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($entitledTenants as $tenant) {
|
|
if (Gate::forUser($user)->allows(Capabilities::PROVIDER_VIEW, $tenant)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function view(User $user, ProviderConnection $connection): Response|bool
|
|
{
|
|
$workspace = $this->currentWorkspace($user);
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
$tenant = $this->tenantForConnection($connection);
|
|
|
|
if (! $tenant instanceof Tenant || (int) $tenant->workspace_id !== (int) $workspace->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! $this->isTenantMember($user, $tenant)) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! Gate::forUser($user)->allows(Capabilities::PROVIDER_VIEW, $tenant)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $connection->tenant_id !== (int) $tenant->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if ((int) $connection->workspace_id !== (int) $workspace->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function create(User $user): Response|bool
|
|
{
|
|
$workspace = $this->currentWorkspace($user);
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
$tenant = $this->resolveCreateTenant($workspace);
|
|
|
|
if (! $tenant instanceof Tenant || ! $this->isTenantMember($user, $tenant)) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! Gate::forUser($user)->allows(Capabilities::PROVIDER_MANAGE, $tenant)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function update(User $user, ProviderConnection $connection): Response|bool
|
|
{
|
|
$workspace = $this->currentWorkspace($user);
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
$tenant = $this->tenantForConnection($connection);
|
|
|
|
if (! $tenant instanceof Tenant || (int) $tenant->workspace_id !== (int) $workspace->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! $this->isTenantMember($user, $tenant)) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! Gate::forUser($user)->allows(Capabilities::PROVIDER_MANAGE, $tenant)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $connection->tenant_id !== (int) $tenant->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if ((int) $connection->workspace_id !== (int) $workspace->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function delete(User $user, ProviderConnection $connection): Response|bool
|
|
{
|
|
$workspace = $this->currentWorkspace($user);
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
$tenant = $this->tenantForConnection($connection);
|
|
|
|
if (! $tenant instanceof Tenant || (int) $tenant->workspace_id !== (int) $workspace->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! $this->isTenantMember($user, $tenant)) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if (! Gate::forUser($user)->allows(Capabilities::PROVIDER_MANAGE, $tenant)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $connection->tenant_id !== (int) $tenant->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
if ((int) $connection->workspace_id !== (int) $workspace->getKey()) {
|
|
return Response::denyAsNotFound();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function currentWorkspace(User $user): ?Workspace
|
|
{
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if (! is_int($workspaceId)) {
|
|
$filamentTenant = Filament::getTenant();
|
|
|
|
if ($filamentTenant instanceof Tenant) {
|
|
$workspaceId = (int) $filamentTenant->workspace_id;
|
|
}
|
|
}
|
|
|
|
if (! is_int($workspaceId)) {
|
|
return null;
|
|
}
|
|
|
|
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return null;
|
|
}
|
|
|
|
if (! app(WorkspaceContext::class)->isMember($user, $workspace)) {
|
|
return null;
|
|
}
|
|
|
|
return $workspace;
|
|
}
|
|
|
|
private function resolveCreateTenant(Workspace $workspace): ?Tenant
|
|
{
|
|
$tenantExternalId = request()->query('tenant_id');
|
|
|
|
if (! is_string($tenantExternalId) || $tenantExternalId === '') {
|
|
$lastTenantId = app(WorkspaceContext::class)->lastTenantId(request());
|
|
|
|
if (is_int($lastTenantId)) {
|
|
return Tenant::query()
|
|
->whereKey($lastTenantId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->first();
|
|
}
|
|
|
|
$filamentTenant = Filament::getTenant();
|
|
|
|
if ($filamentTenant instanceof Tenant && (int) $filamentTenant->workspace_id === (int) $workspace->getKey()) {
|
|
return $filamentTenant;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return Tenant::query()
|
|
->where('external_id', $tenantExternalId)
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->first();
|
|
}
|
|
|
|
private function tenantForConnection(ProviderConnection $connection): ?Tenant
|
|
{
|
|
if ($connection->relationLoaded('tenant') && $connection->tenant instanceof Tenant) {
|
|
return $connection->tenant;
|
|
}
|
|
|
|
if (is_int($connection->tenant_id) || is_numeric($connection->tenant_id)) {
|
|
return Tenant::query()->whereKey((int) $connection->tenant_id)->first();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isTenantMember(User $user, Tenant $tenant): bool
|
|
{
|
|
return TenantMembership::query()
|
|
->where('user_id', (int) $user->getKey())
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->exists();
|
|
}
|
|
}
|