Hosting dev-access: resolve via central identity API (shadow mode)
Deploy Ladill Hosting / deploy (push) Successful in 23s
Deploy Ladill Hosting / deploy (push) Successful in 23s
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
174ead7d3e
commit
107013bd04
@@ -6,10 +6,10 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\CustomerHostingOrder;
|
use App\Models\CustomerHostingOrder;
|
||||||
use App\Models\HostedSite;
|
use App\Models\HostedSite;
|
||||||
use App\Models\HostingAccount;
|
use App\Models\HostingAccount;
|
||||||
use App\Models\HostingAccountMember;
|
|
||||||
use App\Models\HostingProduct;
|
use App\Models\HostingProduct;
|
||||||
use App\Services\Afia\AfiaService;
|
use App\Services\Afia\AfiaService;
|
||||||
use App\Services\Billing\BillingClient;
|
use App\Services\Billing\BillingClient;
|
||||||
|
use App\Services\Hosting\HostingAccessResolver;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -55,9 +55,7 @@ class AfiaController extends Controller
|
|||||||
HostingProduct::TYPE_WORDPRESS,
|
HostingProduct::TYPE_WORDPRESS,
|
||||||
];
|
];
|
||||||
|
|
||||||
$sharedAccountIds = HostingAccountMember::query()
|
$sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user);
|
||||||
->where('user_id', $user->id)
|
|
||||||
->pluck('hosting_account_id');
|
|
||||||
|
|
||||||
$accounts = HostingAccount::query()
|
$accounts = HostingAccount::query()
|
||||||
->where(function ($query) use ($user, $sharedAccountIds) {
|
->where(function ($query) use ($user, $sharedAccountIds) {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ namespace App\Http\Controllers\Hosting;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\CustomerHostingOrder;
|
use App\Models\CustomerHostingOrder;
|
||||||
use App\Models\HostingAccount;
|
use App\Models\HostingAccount;
|
||||||
use App\Models\HostingAccountMember;
|
|
||||||
use App\Models\HostingOrder;
|
use App\Models\HostingOrder;
|
||||||
use App\Models\HostingPlanChange;
|
use App\Models\HostingPlanChange;
|
||||||
use App\Models\HostingProduct;
|
use App\Models\HostingProduct;
|
||||||
use App\Models\RcServiceOrder;
|
use App\Models\RcServiceOrder;
|
||||||
|
use App\Services\Hosting\HostingAccessResolver;
|
||||||
use App\Services\Hosting\HostingOrderFulfillmentService;
|
use App\Services\Hosting\HostingOrderFulfillmentService;
|
||||||
use App\Services\Hosting\HostingPlanChangeService;
|
use App\Services\Hosting\HostingPlanChangeService;
|
||||||
use App\Services\Hosting\HostingRenewalCheckoutService;
|
use App\Services\Hosting\HostingRenewalCheckoutService;
|
||||||
@@ -63,9 +63,7 @@ class HostingProductController extends Controller
|
|||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Admin-assigned Hosting Accounts
|
// Admin-assigned Hosting Accounts
|
||||||
$sharedAccountIds = HostingAccountMember::query()
|
$sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user);
|
||||||
->where('user_id', $user->id)
|
|
||||||
->pluck('hosting_account_id');
|
|
||||||
|
|
||||||
$hostingAccounts = HostingAccount::query()
|
$hostingAccounts = HostingAccount::query()
|
||||||
->where(function ($query) use ($user, $sharedAccountIds) {
|
->where(function ($query) use ($user, $sharedAccountIds) {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\CustomerHostingOrder;
|
use App\Models\CustomerHostingOrder;
|
||||||
use App\Models\HostedSite;
|
use App\Models\HostedSite;
|
||||||
use App\Models\HostingAccount;
|
use App\Models\HostingAccount;
|
||||||
use App\Models\HostingAccountMember;
|
|
||||||
use App\Models\HostingOrder;
|
use App\Models\HostingOrder;
|
||||||
use App\Models\HostingProduct;
|
use App\Models\HostingProduct;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Hosting\HostingAccessResolver;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Collection as SupportCollection;
|
use Illuminate\Support\Collection as SupportCollection;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -100,9 +100,7 @@ class OverviewController extends Controller
|
|||||||
{
|
{
|
||||||
$sharedTypes = $this->sharedTypes();
|
$sharedTypes = $this->sharedTypes();
|
||||||
|
|
||||||
$sharedAccountIds = HostingAccountMember::query()
|
$sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user);
|
||||||
->where('user_id', $user->id)
|
|
||||||
->pluck('hosting_account_id');
|
|
||||||
|
|
||||||
$hostingAccounts = HostingAccount::query()
|
$hostingAccounts = HostingAccount::query()
|
||||||
->where(function ($query) use ($user, $sharedAccountIds) {
|
->where(function ($query) use ($user, $sharedAccountIds) {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace App\Http\Controllers\Hosting;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\CustomerHostingOrder;
|
use App\Models\CustomerHostingOrder;
|
||||||
use App\Models\HostingAccount;
|
use App\Models\HostingAccount;
|
||||||
use App\Models\HostingAccountMember;
|
|
||||||
use App\Models\HostingProduct;
|
use App\Models\HostingProduct;
|
||||||
|
use App\Services\Hosting\HostingAccessResolver;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
@@ -49,9 +49,7 @@ class SearchController extends Controller
|
|||||||
HostingProduct::TYPE_WORDPRESS,
|
HostingProduct::TYPE_WORDPRESS,
|
||||||
];
|
];
|
||||||
|
|
||||||
$sharedAccountIds = HostingAccountMember::query()
|
$sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user);
|
||||||
->where('user_id', $user->id)
|
|
||||||
->pluck('hosting_account_id');
|
|
||||||
|
|
||||||
HostingAccount::query()
|
HostingAccount::query()
|
||||||
->where(function ($query) use ($user, $sharedAccountIds) {
|
->where(function ($query) use ($user, $sharedAccountIds) {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,23 @@ class IdentityClient
|
|||||||
return (array) $response->json('data', []);
|
return (array) $response->json('data', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hosting accounts (by cPanel username) the user can access as a developer,
|
||||||
|
* from the central monolith (source of truth for hosting developer access).
|
||||||
|
*
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function hostingDeveloperAccess(string $publicId): array
|
||||||
|
{
|
||||||
|
$response = $this->request()->get($this->url('/identity/hosting/developer-access'), [
|
||||||
|
'user' => $publicId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->throw();
|
||||||
|
|
||||||
|
return (array) $response->json('data', []);
|
||||||
|
}
|
||||||
|
|
||||||
private function request()
|
private function request()
|
||||||
{
|
{
|
||||||
return Http::withToken((string) config('identity.api_key'))
|
return Http::withToken((string) config('identity.api_key'))
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Central developer access
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When true, hosting developer-access authorization is sourced from the
|
||||||
|
| central monolith (account.ladill.com) via the identity API instead of the
|
||||||
|
| local hosting_account_members table. While false (shadow mode), local data
|
||||||
|
| stays authoritative and any divergence from central is logged.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'central_dev_access' => env('HOSTING_CENTRAL_DEV_ACCESS', false),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Contabo API Configuration
|
| Contabo API Configuration
|
||||||
|
|||||||
Reference in New Issue
Block a user