TenantAtlas/apps/platform/tests/Feature/Database/JsonbDataLayerHardeningTest.php
ahmido 686947d26c feat: harden json to jsonb data layer for trust payloads (#476)
Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #476
2026-06-23 21:36:35 +00:00

592 lines
26 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AlertDelivery;
use App\Models\AlertRule;
use App\Models\AuditLog;
use App\Models\BackupItem;
use App\Models\BackupSchedule;
use App\Models\BackupSet;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentOnboardingSession;
use App\Models\ManagedEnvironmentPermission;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\RestoreRun;
use App\Models\TenantSetting;
use App\Models\WorkspaceSetting;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
beforeEach(function (): void {
if (DB::getDriverName() !== 'pgsql') {
$this->markTestSkipped('Spec 405 JSONB hardening is PostgreSQL-only.');
}
});
it('converts every reviewed legacy json column to jsonb', function (): void {
$columnTypes = spec405ColumnTypes();
foreach (spec405ConvertedJsonColumns() as [$table, $column]) {
$qualifiedColumn = "{$table}.{$column}";
expect($columnTypes)
->toHaveKey($qualifiedColumn)
->and($columnTypes[$qualifiedColumn])->toBe('jsonb');
}
$remainingJsonColumns = DB::table('information_schema.columns')
->where('table_schema', 'public')
->where('data_type', 'json')
->orderBy('table_name')
->orderBy('column_name')
->get(['table_name', 'column_name'])
->map(fn (object $column): string => "{$column->table_name}.{$column->column_name}")
->all();
expect($remainingJsonColumns)->toBeEmpty();
});
it('does not introduce speculative gin indexes for converted payload columns', function (): void {
$convertedTables = collect(spec405ConvertedJsonColumns())
->pluck(0)
->unique()
->values()
->all();
$convertedGinIndexes = DB::table('pg_indexes')
->where('schemaname', 'public')
->whereIn('tablename', $convertedTables)
->where('indexdef', 'ilike', '%USING gin%')
->orderBy('tablename')
->orderBy('indexname')
->get(['tablename', 'indexname'])
->map(fn (object $index): string => "{$index->tablename}.{$index->indexname}")
->all();
expect($convertedGinIndexes)->toBeEmpty();
});
it('preserves existing json rows attributes constraints indexes and rollback query compatibility', function (): void {
$migration = spec405Migration();
$columnContractsBefore = spec405ColumnContracts();
$indexDefinitionsBefore = spec405IndexDefinitionsForConvertedTables();
$constraintDefinitionsBefore = spec405ConstraintDefinitionsForConvertedTables();
$migration->down();
try {
$jsonColumnContracts = spec405ColumnContracts();
foreach (spec405ConvertedJsonColumns() as [$table, $column]) {
$qualifiedColumn = "{$table}.{$column}";
expect($jsonColumnContracts[$qualifiedColumn]['data_type'])->toBe('json')
->and($jsonColumnContracts[$qualifiedColumn]['nullable'])->toBe($columnContractsBefore[$qualifiedColumn]['nullable'])
->and($jsonColumnContracts[$qualifiedColumn]['default'])->toBe($columnContractsBefore[$qualifiedColumn]['default']);
}
$ids = spec405CreateRepresentativePayloadRows();
$payloadsBefore = spec405RepresentativePayloads($ids);
expect(BackupItem::withAssignments()->pluck('id')->map(fn (int $id): int => $id)->all())
->toContain($ids['backup_item_id']);
$migration->up();
expect(spec405ColumnContracts())->toBe($columnContractsBefore)
->and(spec405IndexDefinitionsForConvertedTables())->toBe($indexDefinitionsBefore)
->and(spec405ConstraintDefinitionsForConvertedTables())->toBe($constraintDefinitionsBefore)
->and(spec405RepresentativePayloads($ids))->toMatchArray($payloadsBefore);
expect(BackupItem::withAssignments()->pluck('id')->map(fn (int $id): int => $id)->all())
->toContain($ids['backup_item_id']);
$migration->down();
expect(spec405ColumnTypes()['backup_items.assignments'])->toBe('json')
->and(BackupItem::withAssignments()->pluck('id')->map(fn (int $id): int => $id)->all())
->toContain($ids['backup_item_id']);
} finally {
if ((spec405ColumnTypes()['backup_items.assignments'] ?? null) !== 'jsonb') {
$migration->up();
}
}
});
it('preserves representative jsonb read write and cast behavior', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'metadata' => ['source' => 'spec405', 'nested' => ['enabled' => true]],
]);
DB::table('managed_environments')
->where('id', (int) $tenant->getKey())
->update([
'rbac_canary_results' => json_encode(['status' => 'pass', 'checks' => ['read' => true]], JSON_THROW_ON_ERROR),
'rbac_last_warnings' => json_encode(['warnings' => ['scope review required']], JSON_THROW_ON_ERROR),
]);
$tenant->refresh();
expect($tenant->metadata)->toMatchArray(['nested' => ['enabled' => true], 'source' => 'spec405'])
->and($tenant->rbac_canary_results)->toMatchArray(['checks' => ['read' => true], 'status' => 'pass'])
->and($tenant->rbac_last_warnings)->toMatchArray(['warnings' => ['scope review required']]);
$backupSet = BackupSet::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'metadata' => ['source' => 'spec405'],
]);
$policy = Policy::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'metadata' => ['platform_family' => 'windows'],
]);
$backupItem = BackupItem::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'backup_set_id' => (int) $backupSet->getKey(),
'policy_id' => (int) $policy->getKey(),
'payload' => ['id' => 'policy-jsonb', 'settings' => [['name' => 'mode', 'value' => 'audit']]],
'metadata' => ['scope_tag_names' => ['Default', 'Security']],
'assignments' => [['id' => 'assignment-1', 'target' => ['groupId' => 'group-jsonb']]],
]);
expect($backupItem->refresh()->payload)->toMatchArray(['id' => 'policy-jsonb', 'settings' => [['name' => 'mode', 'value' => 'audit']]])
->and($backupItem->metadata)->toMatchArray(['scope_tag_names' => ['Default', 'Security']])
->and($backupItem->assignments)->toBe([['id' => 'assignment-1', 'target' => ['groupId' => 'group-jsonb']]]);
$policyVersion = PolicyVersion::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'policy_id' => (int) $policy->getKey(),
'snapshot' => ['displayName' => 'Spec 405 policy', 'settings' => [['id' => 'setting-1']]],
'metadata' => ['source' => 'unit'],
'assignments' => [['target' => ['groupId' => 'group-jsonb']]],
'scope_tags' => ['0', 'security'],
'secret_fingerprints' => ['snapshot' => [], 'assignments' => ['hash']],
]);
expect($policyVersion->refresh()->snapshot)->toMatchArray(['displayName' => 'Spec 405 policy', 'settings' => [['id' => 'setting-1']]])
->and($policyVersion->metadata)->toMatchArray(['source' => 'unit'])
->and($policyVersion->assignments)->toBe([['target' => ['groupId' => 'group-jsonb']]])
->and($policyVersion->scope_tags)->toBe(['0', 'security'])
->and($policyVersion->secret_fingerprints)->toMatchArray(['assignments' => ['hash'], 'snapshot' => []]);
$restoreRun = RestoreRun::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'backup_set_id' => (int) $backupSet->getKey(),
'requested_items' => [['policy_identifier' => 'policy-jsonb']],
'preview' => [['action' => 'update', 'policy_identifier' => 'policy-jsonb']],
'results' => ['items' => [['status' => 'applied']]],
'metadata' => ['dry_run' => false],
'group_mapping' => ['old-group' => 'new-group'],
]);
expect($restoreRun->refresh()->requested_items)->toBe([['policy_identifier' => 'policy-jsonb']])
->and($restoreRun->preview)->toBe([['action' => 'update', 'policy_identifier' => 'policy-jsonb']])
->and($restoreRun->results)->toMatchArray(['items' => [['status' => 'applied']]])
->and($restoreRun->metadata)->toMatchArray(['dry_run' => false])
->and($restoreRun->group_mapping)->toMatchArray(['old-group' => 'new-group']);
$auditLog = AuditLog::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'action' => 'spec405.jsonb.regression',
'resource_type' => 'policy',
'resource_id' => 'policy-jsonb',
'status' => 'success',
'metadata' => ['_dedupe_key' => 'spec405-jsonb', 'reason' => 'schema regression'],
'recorded_at' => now(),
]);
expect($auditLog->refresh()->metadata)->toHaveKey('_dedupe_key', 'spec405-jsonb');
$metadataLookupExists = AuditLog::query()
->whereRaw("metadata ->> '_dedupe_key' = ?", ['spec405-jsonb'])
->exists();
expect($metadataLookupExists)->toBeTrue();
$schedule = BackupSchedule::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'name' => 'Spec 405 weekly backup',
'frequency' => 'weekly',
'time_of_day' => '03:00:00',
'days_of_week' => [1, 3, 5],
'policy_types' => ['settingsCatalogPolicy', 'deviceConfiguration'],
]);
expect($schedule->refresh()->days_of_week)->toBe([1, 3, 5])
->and($schedule->policy_types)->toBe(['settingsCatalogPolicy', 'deviceConfiguration']);
$alertRule = AlertRule::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_allowlist' => [(int) $tenant->getKey()],
]);
$alertDelivery = AlertDelivery::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'alert_rule_id' => (int) $alertRule->getKey(),
'payload' => ['title' => 'Spec 405', 'context' => ['severity' => 'high']],
]);
$permission = ManagedEnvironmentPermission::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'permission_key' => 'DeviceManagementConfiguration.Read.All',
'status' => 'granted',
'details' => ['source' => 'graph', 'roles' => ['reader']],
'last_checked_at' => now(),
]);
$onboardingSession = ManagedEnvironmentOnboardingSession::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'state' => ['entra_tenant_id' => 'tenant-jsonb', 'step' => 'verify'],
]);
$tenantSetting = TenantSetting::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'value' => ['keep_last' => 45],
]);
$workspaceSetting = WorkspaceSetting::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'value' => ['keep_last' => 30],
]);
expect($backupSet->refresh()->metadata)->toMatchArray(['source' => 'spec405'])
->and($policy->refresh()->metadata)->toMatchArray(['platform_family' => 'windows'])
->and($alertRule->refresh()->tenant_allowlist)->toBe([(int) $tenant->getKey()])
->and($alertDelivery->refresh()->payload)->toMatchArray(['context' => ['severity' => 'high'], 'title' => 'Spec 405'])
->and($permission->refresh()->details)->toMatchArray(['roles' => ['reader'], 'source' => 'graph'])
->and($onboardingSession->refresh()->state)->toMatchArray(['entra_tenant_id' => 'tenant-jsonb'])
->and($tenantSetting->refresh()->value)->toMatchArray(['keep_last' => 45])
->and($workspaceSetting->refresh()->value)->toMatchArray(['keep_last' => 30]);
});
it('keeps backup item assignment filtering compatible with jsonb columns', function (): void {
BackupItem::factory()->create(['assignments' => null]);
BackupItem::factory()->create(['assignments' => []]);
$withAssignments = BackupItem::factory()->create([
'assignments' => [
['id' => 'assignment-1', 'target' => ['groupId' => 'group-1']],
],
]);
$resultIds = BackupItem::withAssignments()
->pluck('id')
->map(fn (int $id): int => (int) $id)
->all();
expect($resultIds)->toBe([(int) $withAssignments->getKey()]);
});
/**
* @return list<array{0: string, 1: string}>
*/
function spec405ConvertedJsonColumns(): array
{
return [
['alert_deliveries', 'payload'],
['alert_rules', 'tenant_allowlist'],
['audit_logs', 'metadata'],
['backup_items', 'assignments'],
['backup_items', 'metadata'],
['backup_items', 'payload'],
['backup_schedules', 'days_of_week'],
['backup_schedules', 'policy_types'],
['backup_sets', 'metadata'],
['managed_environment_onboarding_sessions', 'state'],
['managed_environment_permissions', 'details'],
['managed_environments', 'metadata'],
['managed_environments', 'rbac_canary_results'],
['managed_environments', 'rbac_last_warnings'],
['policies', 'metadata'],
['policy_versions', 'assignments'],
['policy_versions', 'metadata'],
['policy_versions', 'scope_tags'],
['policy_versions', 'secret_fingerprints'],
['policy_versions', 'snapshot'],
['restore_runs', 'group_mapping'],
['restore_runs', 'metadata'],
['restore_runs', 'preview'],
['restore_runs', 'requested_items'],
['restore_runs', 'results'],
['tenant_settings', 'value'],
['workspace_settings', 'value'],
];
}
function spec405Migration(): Migration
{
return require database_path('migrations/2026_06_23_000405_convert_trust_payload_json_columns_to_jsonb.php');
}
/**
* @return list<string>
*/
function spec405ConvertedTables(): array
{
return collect(spec405ConvertedJsonColumns())
->pluck(0)
->unique()
->values()
->all();
}
/**
* @return array<string, array{data_type: string, nullable: string, default: ?string}>
*/
function spec405ColumnContracts(): array
{
return DB::table('information_schema.columns')
->where('table_schema', 'public')
->whereIn('table_name', spec405ConvertedTables())
->orderBy('table_name')
->orderBy('column_name')
->get(['table_name', 'column_name', 'data_type', 'is_nullable', 'column_default'])
->mapWithKeys(fn (object $column): array => [
"{$column->table_name}.{$column->column_name}" => [
'data_type' => (string) $column->data_type,
'nullable' => (string) $column->is_nullable,
'default' => $column->column_default === null ? null : (string) $column->column_default,
],
])
->only(collect(spec405ConvertedJsonColumns())->map(fn (array $column): string => "{$column[0]}.{$column[1]}")->all())
->all();
}
/**
* @return array<string, string>
*/
function spec405IndexDefinitionsForConvertedTables(): array
{
return DB::table('pg_indexes')
->where('schemaname', 'public')
->whereIn('tablename', spec405ConvertedTables())
->orderBy('tablename')
->orderBy('indexname')
->get(['tablename', 'indexname', 'indexdef'])
->mapWithKeys(fn (object $index): array => [
"{$index->tablename}.{$index->indexname}" => (string) $index->indexdef,
])
->all();
}
/**
* @return array<string, string>
*/
function spec405ConstraintDefinitionsForConvertedTables(): array
{
return DB::table('pg_constraint')
->selectRaw('conrelid::regclass::text as table_name, conname, pg_get_constraintdef(oid) as definition')
->whereRaw("connamespace = 'public'::regnamespace")
->whereIn(DB::raw('conrelid::regclass::text'), spec405ConvertedTables())
->orderBy('table_name')
->orderBy('conname')
->get()
->mapWithKeys(fn (object $constraint): array => [
"{$constraint->table_name}.{$constraint->conname}" => (string) $constraint->definition,
])
->all();
}
/**
* @return array<string, int>
*/
function spec405CreateRepresentativePayloadRows(): array
{
$tenant = ManagedEnvironment::factory()->create([
'metadata' => ['source' => 'spec405', 'nested' => ['enabled' => true]],
]);
DB::table('managed_environments')
->where('id', (int) $tenant->getKey())
->update([
'rbac_canary_results' => json_encode(['status' => 'pass', 'checks' => ['read' => true]], JSON_THROW_ON_ERROR),
'rbac_last_warnings' => json_encode(['warnings' => ['scope review required']], JSON_THROW_ON_ERROR),
]);
$backupSet = BackupSet::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'metadata' => ['source' => 'spec405'],
]);
$policy = Policy::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'metadata' => ['platform_family' => 'windows'],
]);
$backupItem = BackupItem::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'backup_set_id' => (int) $backupSet->getKey(),
'policy_id' => (int) $policy->getKey(),
'payload' => ['id' => 'policy-jsonb', 'settings' => [['name' => 'mode', 'value' => 'audit']]],
'metadata' => ['scope_tag_names' => ['Default', 'Security']],
'assignments' => [['id' => 'assignment-1', 'target' => ['groupId' => 'group-jsonb']]],
]);
$policyVersion = PolicyVersion::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'policy_id' => (int) $policy->getKey(),
'snapshot' => ['displayName' => 'Spec 405 policy', 'settings' => [['id' => 'setting-1']]],
'metadata' => ['source' => 'unit'],
'assignments' => [['target' => ['groupId' => 'group-jsonb']]],
'scope_tags' => ['0', 'security'],
'secret_fingerprints' => ['snapshot' => [], 'assignments' => ['hash']],
]);
$restoreRun = RestoreRun::factory()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'backup_set_id' => (int) $backupSet->getKey(),
'requested_items' => [['policy_identifier' => 'policy-jsonb']],
'preview' => [['action' => 'update', 'policy_identifier' => 'policy-jsonb']],
'results' => ['items' => [['status' => 'applied']]],
'metadata' => ['dry_run' => false],
'group_mapping' => ['old-group' => 'new-group'],
]);
$auditLog = AuditLog::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'action' => 'spec405.jsonb.regression',
'resource_type' => 'policy',
'resource_id' => 'policy-jsonb',
'status' => 'success',
'metadata' => ['_dedupe_key' => 'spec405-jsonb', 'reason' => 'schema regression'],
'recorded_at' => now(),
]);
$schedule = BackupSchedule::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'name' => 'Spec 405 weekly backup',
'frequency' => 'weekly',
'time_of_day' => '03:00:00',
'days_of_week' => [1, 3, 5],
'policy_types' => ['settingsCatalogPolicy', 'deviceConfiguration'],
]);
$alertRule = AlertRule::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_allowlist' => [(int) $tenant->getKey()],
]);
$alertDelivery = AlertDelivery::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'alert_rule_id' => (int) $alertRule->getKey(),
'payload' => ['title' => 'Spec 405', 'context' => ['severity' => 'high']],
]);
$permission = ManagedEnvironmentPermission::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'permission_key' => 'DeviceManagementConfiguration.Read.All',
'status' => 'granted',
'details' => ['source' => 'graph', 'roles' => ['reader']],
'last_checked_at' => now(),
]);
$onboardingSession = ManagedEnvironmentOnboardingSession::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'state' => ['entra_tenant_id' => 'tenant-jsonb', 'step' => 'verify'],
]);
$tenantSetting = TenantSetting::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'value' => ['keep_last' => 45],
]);
$workspaceSetting = WorkspaceSetting::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'value' => ['keep_last' => 30],
]);
return [
'alert_delivery_id' => (int) $alertDelivery->getKey(),
'alert_rule_id' => (int) $alertRule->getKey(),
'audit_log_id' => (int) $auditLog->getKey(),
'backup_item_id' => (int) $backupItem->getKey(),
'backup_schedule_id' => (int) $schedule->getKey(),
'backup_set_id' => (int) $backupSet->getKey(),
'managed_environment_id' => (int) $tenant->getKey(),
'managed_environment_onboarding_session_id' => (int) $onboardingSession->getKey(),
'managed_environment_permission_id' => (int) $permission->getKey(),
'policy_id' => (int) $policy->getKey(),
'policy_version_id' => (int) $policyVersion->getKey(),
'restore_run_id' => (int) $restoreRun->getKey(),
'tenant_setting_id' => (int) $tenantSetting->getKey(),
'workspace_setting_id' => (int) $workspaceSetting->getKey(),
];
}
/**
* @param array<string, int> $ids
* @return array<string, mixed>
*/
function spec405RepresentativePayloads(array $ids): array
{
$alertDelivery = AlertDelivery::query()->findOrFail($ids['alert_delivery_id']);
$alertRule = AlertRule::query()->findOrFail($ids['alert_rule_id']);
$auditLog = AuditLog::query()->findOrFail($ids['audit_log_id']);
$backupItem = BackupItem::query()->findOrFail($ids['backup_item_id']);
$backupSchedule = BackupSchedule::query()->findOrFail($ids['backup_schedule_id']);
$backupSet = BackupSet::query()->findOrFail($ids['backup_set_id']);
$tenant = ManagedEnvironment::query()->findOrFail($ids['managed_environment_id']);
$onboardingSession = ManagedEnvironmentOnboardingSession::query()->findOrFail($ids['managed_environment_onboarding_session_id']);
$permission = ManagedEnvironmentPermission::query()->findOrFail($ids['managed_environment_permission_id']);
$policy = Policy::query()->findOrFail($ids['policy_id']);
$policyVersion = PolicyVersion::query()->findOrFail($ids['policy_version_id']);
$restoreRun = RestoreRun::query()->findOrFail($ids['restore_run_id']);
$tenantSetting = TenantSetting::query()->findOrFail($ids['tenant_setting_id']);
$workspaceSetting = WorkspaceSetting::query()->findOrFail($ids['workspace_setting_id']);
return [
'alert_deliveries.payload' => $alertDelivery->payload,
'alert_rules.tenant_allowlist' => $alertRule->tenant_allowlist,
'audit_logs.metadata' => $auditLog->metadata,
'backup_items.assignments' => $backupItem->assignments,
'backup_items.metadata' => $backupItem->metadata,
'backup_items.payload' => $backupItem->payload,
'backup_schedules.days_of_week' => $backupSchedule->days_of_week,
'backup_schedules.policy_types' => $backupSchedule->policy_types,
'backup_sets.metadata' => $backupSet->metadata,
'managed_environment_onboarding_sessions.state' => $onboardingSession->state,
'managed_environment_permissions.details' => $permission->details,
'managed_environments.metadata' => $tenant->metadata,
'managed_environments.rbac_canary_results' => $tenant->rbac_canary_results,
'managed_environments.rbac_last_warnings' => $tenant->rbac_last_warnings,
'policies.metadata' => $policy->metadata,
'policy_versions.assignments' => $policyVersion->assignments,
'policy_versions.metadata' => $policyVersion->metadata,
'policy_versions.scope_tags' => $policyVersion->scope_tags,
'policy_versions.secret_fingerprints' => $policyVersion->secret_fingerprints,
'policy_versions.snapshot' => $policyVersion->snapshot,
'restore_runs.group_mapping' => $restoreRun->group_mapping,
'restore_runs.metadata' => $restoreRun->metadata,
'restore_runs.preview' => $restoreRun->preview,
'restore_runs.requested_items' => $restoreRun->requested_items,
'restore_runs.results' => $restoreRun->results,
'tenant_settings.value' => $tenantSetting->value,
'workspace_settings.value' => $workspaceSetting->value,
];
}
/**
* @return array<string, string>
*/
function spec405ColumnTypes(): array
{
$tables = collect(spec405ConvertedJsonColumns())
->pluck(0)
->unique()
->values()
->all();
return DB::table('information_schema.columns')
->where('table_schema', 'public')
->whereIn('table_name', $tables)
->get(['table_name', 'column_name', 'data_type'])
->mapWithKeys(fn (object $column): array => [
"{$column->table_name}.{$column->column_name}" => (string) $column->data_type,
])
->all();
}