TenantAtlas/apps/platform/app/Services/Auth/WorkspaceCapabilityResolver.php
ahmido ce0615a9c1 Spec 182: relocate Laravel platform to apps/platform (#213)
## Summary
- move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling
- update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location
- add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation`
- integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404`

## Remaining Rollout Checks
- validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout
- confirm web, queue, and scheduler processes all start from the expected working directory in staging/production
- verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #213
2026-04-08 08:40:47 +00:00

101 lines
2.7 KiB
PHP

<?php
namespace App\Services\Auth;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Auth\Capabilities;
use App\Support\Auth\WorkspaceRole;
use Illuminate\Support\Facades\Log;
/**
* Workspace Capability Resolver
*
* Resolves user memberships and capabilities for a given workspace.
* Caches results per request to avoid N+1 queries.
*/
class WorkspaceCapabilityResolver
{
private array $resolvedMemberships = [];
private array $loggedDenials = [];
public function getRole(User $user, Workspace $workspace): ?WorkspaceRole
{
$membership = $this->getMembership($user, $workspace);
if ($membership === null) {
return null;
}
return WorkspaceRole::tryFrom($membership['role']);
}
public function can(User $user, Workspace $workspace, string $capability): bool
{
if (! Capabilities::isKnown($capability)) {
throw new \InvalidArgumentException("Unknown capability: {$capability}");
}
$role = $this->getRole($user, $workspace);
if ($role === null) {
$this->logDenial($user, $workspace, $capability);
return false;
}
$allowed = WorkspaceRoleCapabilityMap::hasCapability($role, $capability);
if (! $allowed) {
$this->logDenial($user, $workspace, $capability);
}
return $allowed;
}
public function isMember(User $user, Workspace $workspace): bool
{
return $this->getMembership($user, $workspace) !== null;
}
public function clearCache(): void
{
$this->resolvedMemberships = [];
}
private function logDenial(User $user, Workspace $workspace, string $capability): void
{
$key = implode(':', [(string) $user->getKey(), (string) $workspace->getKey(), $capability]);
if (isset($this->loggedDenials[$key])) {
return;
}
$this->loggedDenials[$key] = true;
Log::warning('rbac.workspace.denied', [
'capability' => $capability,
'workspace_id' => (int) $workspace->getKey(),
'actor_user_id' => (int) $user->getKey(),
]);
}
private function getMembership(User $user, Workspace $workspace): ?array
{
$cacheKey = "workspace_membership_{$user->id}_{$workspace->id}";
if (! isset($this->resolvedMemberships[$cacheKey])) {
$membership = WorkspaceMembership::query()
->where('user_id', $user->id)
->where('workspace_id', $workspace->id)
->first(['role']);
$this->resolvedMemberships[$cacheKey] = $membership?->toArray();
}
return $this->resolvedMemberships[$cacheKey];
}
}