Files
isaaccladandClaude Opus 4.8 107013bd04
Deploy Ladill Hosting / deploy (push) Successful in 23s
Hosting dev-access: resolve via central identity API (shadow mode)
Consolidate the inline HostingAccountMember lookups (Overview/Search/Hosting
Product/Afia controllers) into HostingAccessResolver, which can source hosting
developer-access from the monolith's central /identity/hosting/developer-access
(correlating cPanel usernames to local account ids). Shadow mode by default
(HOSTING_CENTRAL_DEV_ACCESS=false): local hosting_account_members stays
authoritative and divergence from central is logged; flip the flag to cut over.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 23:42:08 +00:00

89 lines
3.0 KiB
PHP

<?php
namespace App\Services\Hosting;
use App\Models\HostingAccount;
use App\Models\HostingAccountMember;
use App\Models\User;
use App\Services\Identity\IdentityClient;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
/**
* Resolves which hosting accounts a user can access as a developer (shared with
* them). The monolith is the source of truth; this consolidates the previously
* inline HostingAccountMember lookups into one place so authz can move to the
* central identity API.
*
* Shadow mode (config hosting.central_dev_access = false, default): local
* hosting_account_members stays authoritative; central is fetched and any
* divergence is logged. When flipped to true, central becomes authoritative
* with a fail-open fallback to local if the API is unreachable.
*/
class HostingAccessResolver
{
public function __construct(private IdentityClient $identity) {}
/** @return Collection<int, int> hosting account ids shared with the user */
public function sharedAccountIds(User $user): Collection
{
$local = HostingAccountMember::query()
->where('user_id', $user->id)
->pluck('hosting_account_id');
$central = $this->centralAccountIds($user);
if ($central !== null && $central->sort()->values()->all() !== $local->sort()->values()->all()) {
Log::warning('Hosting dev-access parity mismatch', [
'user_id' => $user->id,
'local' => $local->values()->all(),
'central' => $central->values()->all(),
]);
}
if (config('hosting.central_dev_access', false)) {
return $central ?? $local; // fail-open to local if central unreachable
}
return $local;
}
/** @return Collection<int, int>|null null = central unreachable (fall back to local) */
private function centralAccountIds(User $user): ?Collection
{
if (! $user->public_id) {
return collect();
}
return Cache::remember(
"hosting_dev_access:{$user->id}",
now()->addSeconds(60),
function () use ($user): ?Collection {
try {
$grants = $this->identity->hostingDeveloperAccess((string) $user->public_id);
$usernames = array_values(array_filter(array_map(
fn ($g) => $g['username'] ?? null,
$grants,
)));
if ($usernames === []) {
return collect();
}
return HostingAccount::query()
->whereIn('username', $usernames)
->pluck('id');
} catch (\Throwable $e) {
Log::warning('Central hosting dev-access fetch failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
return null;
}
},
);
}
}