grantScope( tenant: $tenant, actor: $actor, member: $member, source: $source, sourceRef: $sourceRef, ); } public function grantScope( ManagedEnvironment $tenant, User $actor, User $member, string $source = 'manual', ?string $sourceRef = null, ): ManagedEnvironmentMembership { $workspace = $this->workspaceForTenant($tenant); $memberWorkspaceRole = $this->memberWorkspaceRole($workspace, $member); $this->assertActorCanManageScope($actor, $workspace); $membership = DB::transaction(function () use ($tenant, $actor, $member, $memberWorkspaceRole, $source, $sourceRef): ManagedEnvironmentMembership { $existing = ManagedEnvironmentMembership::query() ->where('managed_environment_id', $tenant->getKey()) ->where('user_id', $member->getKey()) ->first(); if ($existing) { $existing->forceFill([ 'role' => $memberWorkspaceRole, 'source' => $source, 'source_ref' => $sourceRef, 'created_by_user_id' => (int) $actor->getKey(), ])->save(); return $existing->refresh(); } $membership = ManagedEnvironmentMembership::query()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'user_id' => (int) $member->getKey(), 'role' => $memberWorkspaceRole, 'source' => $source, 'source_ref' => $sourceRef, 'created_by_user_id' => (int) $actor->getKey(), ]); $this->auditLogger->log( tenant: $tenant, action: AuditActionId::ManagedEnvironmentAccessScopeGrant->value, context: [ 'metadata' => [ 'member_user_id' => (int) $member->getKey(), 'workspace_role' => $memberWorkspaceRole, 'source' => $source, ], ], actorId: (int) $actor->getKey(), actorEmail: $actor->email, actorName: $actor->name, status: 'success', resourceType: 'tenant', resourceId: (string) $tenant->getKey(), ); return $membership; }); $this->managedEnvironmentAccessScopeResolver->clearCache(); return $membership; } public function changeRole(ManagedEnvironment $tenant, User $actor, ManagedEnvironmentMembership $membership, string $newRole): ManagedEnvironmentMembership { throw new DomainException('Managed-environment access scopes do not manage roles. Change the workspace role instead.'); } public function removeMember(ManagedEnvironment $tenant, User $actor, ManagedEnvironmentMembership $membership): void { $workspace = $this->workspaceForTenant($tenant); $this->assertActorCanManageScope($actor, $workspace); DB::transaction(function () use ($tenant, $actor, $membership): void { $membership->refresh(); if ($membership->managed_environment_id !== (int) $tenant->getKey()) { throw new DomainException('Membership belongs to a different tenant.'); } $memberUserId = (int) $membership->user_id; $membership->delete(); $this->auditLogger->log( tenant: $tenant, action: AuditActionId::ManagedEnvironmentAccessScopeRemove->value, context: [ 'metadata' => [ 'member_user_id' => $memberUserId, ], ], actorId: (int) $actor->getKey(), actorEmail: $actor->email, actorName: $actor->name, status: 'success', resourceType: 'tenant', resourceId: (string) $tenant->getKey(), ); }); $this->managedEnvironmentAccessScopeResolver->clearCache(); } public function bootstrapRecover(ManagedEnvironment $tenant, User $actor, User $member): ManagedEnvironmentMembership { $membership = $this->addMember( tenant: $tenant, actor: $actor, member: $member, role: 'owner', source: 'break_glass', ); $this->auditLogger->log( tenant: $tenant, action: AuditActionId::TenantMembershipBootstrapRecover->value, context: [ 'metadata' => [ 'member_user_id' => (int) $member->getKey(), ], ], actorId: (int) $actor->getKey(), actorEmail: $actor->email, actorName: $actor->name, status: 'success', resourceType: 'tenant', resourceId: (string) $tenant->getKey(), ); return $membership; } private function workspaceForTenant(ManagedEnvironment $tenant): Workspace { $workspace = $tenant->workspace; if (! $workspace instanceof Workspace) { $workspace = Workspace::query()->whereKey((int) $tenant->workspace_id)->first(); } if (! $workspace instanceof Workspace) { throw new DomainException('Managed environment does not belong to a workspace.'); } return $workspace; } private function assertActorCanManageScope(User $actor, Workspace $workspace): void { if (! $this->workspaceCapabilityResolver->can($actor, $workspace, Capabilities::WORKSPACE_MEMBERSHIP_MANAGE)) { throw new DomainException('Forbidden.'); } } private function memberWorkspaceRole(Workspace $workspace, User $member): string { $membership = WorkspaceMembership::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('user_id', (int) $member->getKey()) ->first(['role']); if (! $membership instanceof WorkspaceMembership) { throw new DomainException('Environment access scope can only be granted to workspace members.'); } return (string) $membership->role; } }