Preserva base local aceite de identidade e acessos

This commit is contained in:
Codex
2026-10-25 21:29:22 +02:00
commit 26b80df545
112 changed files with 13259 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
param([ValidateSet('start','stop')][string]$Action = 'start')
$ErrorActionPreference = 'Stop'
$sgfProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if ($sgfProject -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
$sgfPhp = 'C:\laragon\tools\sgf-tempo\php-8.4.27\php.exe'
$sgfRun = 'C:\laragon\data\sgf-tempo\run'
$recordFile = Join-Path $sgfRun 'browser-proof.json'
Set-Location -LiteralPath $sgfProject
if ($Action -eq 'stop') {
if (Test-Path -LiteralPath $recordFile) {
$record = Get-Content -LiteralPath $recordFile -Raw | ConvertFrom-Json
$process = Get-Process -Id $record.pid -ErrorAction SilentlyContinue
if ($process) {
if ($process.Path -ne $sgfPhp -or $process.StartTime.ToUniversalTime().ToString('o') -ne $record.started) { throw 'Process identity mismatch; no process stopped.' }
Stop-Process -Id $process.Id
}
Remove-Item -LiteralPath $recordFile
}
$fixtureFile = 'C:\laragon\data\sgf-tempo\secrets\browser-fixture.json'
if (Test-Path -LiteralPath $fixtureFile) { Remove-Item -LiteralPath $fixtureFile }
Write-Output 'Browser proof server stopped. Synthetic data remains only in sgf_tempo_test until the next test run.'
exit 0
}
if (Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue) { throw 'Port 8001 occupied; no process was stopped.' }
if (Test-Path -LiteralPath $recordFile) { throw 'Browser proof record exists; stop that proof first.' }
& $sgfPhp scripts/prepare-browser-fixture.php --env=testing
if ($LASTEXITCODE) { throw 'Could not prepare synthetic fixtures.' }
$utf8 = New-Object Text.UTF8Encoding $false
$lines = Get-Content -LiteralPath '.env.testing' | Where-Object { $_ -notmatch '^(APP_ENV|APP_URL|SESSION_DRIVER|SESSION_COOKIE|SESSION_FILES|CACHE_STORE|CACHE_FILE_PATH|CACHE_LOCK_PATH)=' }
$lines += @('APP_ENV=browser-testing','APP_URL=http://127.0.0.1:8001','SESSION_DRIVER=file','SESSION_COOKIE=sgf_browser_proof',
'SESSION_FILES=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/sessions', 'CACHE_STORE=file',
'CACHE_FILE_PATH=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/cache', 'CACHE_LOCK_PATH=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/locks')
[IO.File]::WriteAllLines((Join-Path $sgfProject '.env.browser-testing'), $lines, $utf8)
$account = [Security.Principal.WindowsIdentity]::GetCurrent().Name
& icacls.exe '.env.browser-testing' /inheritance:r /grant:r "${account}:F" 'SYSTEM:F' | Out-Null
New-Item -ItemType Directory -Path 'storage\framework\browser-proof\sessions','storage\framework\browser-proof\cache','storage\framework\browser-proof\locks' -Force | Out-Null
$env:APP_ENV = 'browser-testing'
try {
if (Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue) { throw 'Port 8001 became occupied; no process stopped.' }
$process = Start-Process -FilePath $sgfPhp -ArgumentList @('-S','127.0.0.1:8001','-t','public','scripts/router.php') -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\browser-proof.stdout.log" -RedirectStandardError "$sgfRun\browser-proof.stderr.log"
@{pid=$process.Id;started=$process.StartTime.ToUniversalTime().ToString('o')} | ConvertTo-Json | Set-Content -LiteralPath $recordFile -Encoding UTF8
} finally { Remove-Item Env:APP_ENV -ErrorAction SilentlyContinue }
Write-Output 'Browser proof: http://127.0.0.1:8001 — synthetic data only, isolated session/cache, CSRF enabled.'
+9
View File
@@ -0,0 +1,9 @@
<?php
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
if (! \App\Modules\Identity\Services\LocalDatabaseGuard::allows()) { fwrite(STDERR, "Invalid local destination.\n"); exit(1); }
$tables = \Illuminate\Support\Facades\DB::select("SELECT relname, has_table_privilege(current_user, c.oid, 'SELECT') AS can_read, has_table_privilege(current_user, c.oid, 'INSERT') AS can_insert, has_table_privilege(current_user, c.oid, 'UPDATE') AS can_update FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND relname IN ('users','tenants','companies','company_memberships','permission_grants','identity_audit') ORDER BY relname");
$sequences = \Illuminate\Support\Facades\DB::select("SELECT relname, has_sequence_privilege(current_user, c.oid, 'USAGE') AS can_use FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND relkind = 'S' ORDER BY relname");
echo json_encode(['guard' => 'passed', 'tables' => $tables, 'sequences' => $sequences], JSON_PRETTY_PRINT).PHP_EOL;
+59
View File
@@ -0,0 +1,59 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const base = 'http://127.0.0.1:8001';
const fixture = JSON.parse(await fs.readFile('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json', 'utf8'));
const results = [];
function client() {
const cookies = new Map();
let csrf = ''; let context = '';
return {
cookies,
async request(path, method = 'GET', body, {token = context, csrfHeader = true} = {}) {
const headers = {Accept:'application/json', 'Content-Type':'application/json', Cookie:[...cookies].map(([k,v]) => `${k}=${v}`).join('; '), 'X-SGF-Context':token};
if (csrfHeader) headers['X-CSRF-TOKEN'] = csrf;
const response = await fetch(base+path, {method, headers, ...(body === undefined ? {} : {body:JSON.stringify(body)})});
for (const cookie of response.headers.getSetCookie()) { const pair=cookie.split(';',1)[0]; const split=pair.indexOf('='); cookies.set(pair.slice(0,split),pair.slice(split+1)); }
const data = await response.json().catch(() => ({}));
if (data.csrf) csrf=data.csrf;
if (data.context?.token) context=data.context.token;
return {status:response.status,data};
},
async login(email) {
assert.equal((await this.request('/api/session')).status,200);
assert.equal((await this.request('/api/login','POST',{email,password:fixture.password})).status,200);
},
async select(company=fixture.company,unit=fixture.unit) {
const result=await this.request('/api/context','POST',{tenant_id:fixture.tenant,company_id:company,unit_id:unit});
assert.equal(result.status,200); return result.data.context.token;
}
};
}
const manager=client(); await manager.login('gestor@example.test'); const token=await manager.select();
assert.equal((await manager.request(`/api/companies/${fixture.company}`,'PATCH',{name:'CSRF must refuse'},{csrfHeader:false})).status,419);
results.push('CSRF: pedido de alteração sem token/origem válida recusado (419).');
const reader=client(); await reader.login('consulta@example.test'); const readerToken=await reader.select();
assert.equal((await reader.request(`/api/companies/${fixture.sibling}`)).status,403);
assert.equal((await reader.request(`/api/units/${fixture.hidden}`)).status,403);
assert.equal((await reader.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Not allowed'})).status,403);
results.push('URLs/API: empresa do mesmo tenant e unidade sem acesso recusadas; consulta não permite edição (403).');
const grant={email:'consulta@example.test',permission:'company.edit',reason:'Prova HTTP sintética'};
assert.equal((await manager.request('/api/accesses','POST',grant)).status,200);
assert.equal((await reader.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Empresa Horizonte — Prova HTTP'})).status,200);
assert.equal((await manager.request('/api/accesses/revoke','POST',grant)).status,200);
assert.equal((await reader.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Revoked'})).status,403);
results.push('Concessão e revogação: efeito no pedido seguinte em sessão independente (200 → 403).');
await manager.select(fixture.sibling,null);
assert.equal((await manager.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Stale'},{token})).status,409);
results.push('Formulário anterior à mudança de contexto recusado (409).');
const limited=client(); await limited.login('limitado@example.test'); await limited.select();
assert.equal((await limited.request('/api/accesses','POST',grant)).status,403);
results.push('Gestor limitado não consegue conceder edição que não possui (403).');
const oldCookies=new Map(reader.cookies);
assert.equal((await reader.request('/api/logout','POST')).status,200);
reader.cookies.clear(); for(const [key,value] of oldCookies) reader.cookies.set(key,value);
assert.equal((await reader.request('/api/context')).status,401);
results.push('Replay do cookie da sessão terminada recusado (401).');
await manager.request('/api/logout','POST'); await limited.request('/api/logout','POST');
await fs.writeFile(new URL('../docs/proofs/http-results.json',import.meta.url),JSON.stringify({environment:'sgf_tempo_test, browser-testing, localhost:8001',results},null,2));
console.log(results.join('\n'));
+11
View File
@@ -0,0 +1,11 @@
<?php
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$db = \Illuminate\Support\Facades\DB::selectOne('SELECT current_database() AS db, current_user AS role');
$counts = [];
foreach (['users', 'tenants', 'companies', 'units', 'company_memberships', 'permission_grants', 'identity_audit'] as $table) {
$counts[$table] = \Illuminate\Support\Facades\Schema::hasTable($table) ? \Illuminate\Support\Facades\DB::table($table)->count() : 'pending migration';
}
echo json_encode(['database' => $db->db, 'role' => $db->role, 'counts' => $counts], JSON_PRETTY_PRINT).PHP_EOL;
+15
View File
@@ -0,0 +1,15 @@
<?php
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
foreach (['users', 'tenants', 'companies', 'units'] as $table) {
if (\Illuminate\Support\Facades\DB::table($table)->exists()) {
fwrite(STDERR, "Test database is not empty. No data was deleted. Stop other test workflows and prepare the test database explicitly.\n"); exit(1);
}
}
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\IdentityFixture::create());
$safe = array_intersect_key($fixture, array_flip(['tenant', 'otherTenant', 'company', 'sibling', 'foreign', 'unit', 'hidden', 'siblingUnit', 'password']));
file_put_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json', json_encode($safe, JSON_THROW_ON_ERROR));
echo "Synthetic browser fixtures prepared only in sgf_tempo_test. Credentials stored locally with restricted ACL; not displayed.\n";
+44
View File
@@ -0,0 +1,44 @@
param(
[Parameter(Mandatory=$true)][string]$Email,
[Parameter(Mandatory=$true)][string]$Name,
[Parameter(Mandatory=$true)][string]$Operator,
[Parameter(Mandatory=$true)][string]$Reason
)
$ErrorActionPreference = 'Stop'
$project = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if ($project -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
Set-Location -LiteralPath $project
$OutputEncoding = New-Object Text.UTF8Encoding $false
[Console]::OutputEncoding = $OutputEncoding
$sgfPhp = 'C:\laragon\tools\sgf-tempo\php-8.4.27\php.exe'
$arguments = @('artisan','identity:provision',"--email=$Email","--name=$Name","--operator=$Operator","--reason=$Reason")
& $sgfPhp @arguments
if ($LASTEXITCODE -ne 0) { throw 'Preview refused. No account created.' }
$answer = Read-Host 'Confirma criar apenas esta conta, sem permissoes automaticas? [sim/nao]'
if ($answer -notin @('sim','s','yes','y')) { Write-Host 'Operacao cancelada. Nada criado.'; exit 1 }
Write-Host 'Defina a palavra-passe apenas neste terminal. Minimo 12 caracteres; maximo 72 bytes UTF-8.'
while ($true) {
$first = Read-Host 'Nova palavra-passe' -AsSecureString
$repeat = Read-Host 'Repita a palavra-passe' -AsSecureString
$firstPtr = [IntPtr]::Zero; $repeatPtr = [IntPtr]::Zero
$plainFirst = $null; $plainRepeat = $null; $payload = $null
try {
$firstPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($first)
$repeatPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($repeat)
$plainFirst = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($firstPtr)
$plainRepeat = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($repeatPtr)
if ($plainFirst.Length -lt 12) { Write-Host 'Use pelo menos 12 caracteres. Tente novamente.' -ForegroundColor Yellow; continue }
if ([Text.Encoding]::UTF8.GetByteCount($plainFirst) -gt 72) { Write-Host 'Excede 72 bytes. Tente novamente.' -ForegroundColor Yellow; continue }
if ($plainFirst -cne $plainRepeat) { Write-Host 'As entradas nao coincidem. Tente novamente.' -ForegroundColor Yellow; continue }
$payload = @{confirmed=$true;email=$Email.Trim().ToLowerInvariant();password=$plainFirst;confirmation=$plainRepeat} | ConvertTo-Json -Compress
$payload | & $sgfPhp @arguments --apply --protected-stdin
if ($LASTEXITCODE -ne 0) { throw 'Account creation refused. Copy only the error message, never the password.' }
Write-Host 'Conta criada sem acessos automaticos. Um gestor autorizado pode agora conceder os direitos na aplicacao.' -ForegroundColor Green
break
} finally {
if ($firstPtr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($firstPtr) }
if ($repeatPtr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($repeatPtr) }
$plainFirst=$null; $plainRepeat=$null; $payload=$null
$first.Dispose(); $repeat.Dispose()
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
$public = realpath(__DIR__.'/../public');
$uri = rawurldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/');
$file = realpath($public.$uri);
if ($uri !== '/' && $file && str_starts_with($file, $public.DIRECTORY_SEPARATOR) && is_file($file)) {
return false;
}
require $public.'/index.php';
+130
View File
@@ -0,0 +1,130 @@
param(
[ValidateSet('start', 'stop', 'status', 'frontend', 'php', 'composer', 'npm', 'test', 'migrate')]
[string]$Action = 'status',
[Parameter(ValueFromRemainingArguments = $true)] [string[]]$ToolArguments
)
$ErrorActionPreference = 'Stop'
$sgfProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if ($sgfProject -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
$sgfTools = 'C:\laragon\tools\sgf-tempo'
$sgfData = 'C:\laragon\data\sgf-tempo'
$sgfCluster = Join-Path $sgfData 'postgres-17'
$sgfPg = 'C:\Program Files\PostgreSQL\17\bin'
$sgfPhp = "$sgfTools\php-8.4.27\php.exe"
$sgfNode = "$sgfTools\node-v24.22.1-win-arm64\node.exe"
$sgfComposer = "$sgfTools\composer-2.10.3\composer.phar"
$sgfNpmCli = "$sgfTools\node-v24.22.1-win-arm64\node_modules\npm\bin\npm-cli.js"
$sgfRun = Join-Path $sgfData 'run'
$env:PATH = "$sgfTools\php-8.4.27;$sgfTools\node-v24.22.1-win-arm64;" + $env:PATH
$env:COMPOSER_HOME = "$sgfTools\composer-home"
$env:COMPOSER_CACHE_DIR = "$sgfTools\composer-cache"
$env:npm_config_cache = "$sgfTools\npm-cache"
$env:npm_config_userconfig = "$sgfTools\npmrc"
Remove-Item Env:COMPOSER_AUTH -ErrorAction SilentlyContinue
Set-Location -LiteralPath $sgfProject
New-Item -ItemType Directory -Path $sgfRun -Force | Out-Null
function Assert-FreePort([int]$Port) {
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
throw "Port $Port is occupied. No process was stopped."
}
}
function Start-SgfDatabase {
$listeners = @(Get-NetTCPConnection -LocalPort 55432 -State Listen -ErrorAction SilentlyContinue)
if ($listeners.Count) {
$pidFile = Join-Path $sgfCluster 'postmaster.pid'
if (!(Test-Path -LiteralPath $pidFile)) { throw 'Port 55432 belongs to another process.' }
$clusterPid = [int](Get-Content -LiteralPath $pidFile -TotalCount 1)
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $clusterPid"
$normalizedCommand = if ($process) { $process.CommandLine.Replace('/', '\') } else { '' }
if (($listeners.OwningProcess -notcontains $clusterPid) -or !$process -or $process.ExecutablePath -ne "$sgfPg\postgres.exe" -or $normalizedCommand -notlike "*$sgfCluster*") {
throw 'Port 55432 is not owned by the SGF cluster. No process was stopped.'
}
return
}
Assert-FreePort 55432
& "$sgfPg\pg_ctl.exe" -D $sgfCluster -l (Join-Path $sgfData 'postgres.log') -w start
if ($LASTEXITCODE) { throw 'SGF database start failed.' }
}
function Start-SgfProcess([string]$Name, [string]$Executable, [string[]]$Arguments, [int]$Port) {
Assert-FreePort $Port
$process = Start-Process -FilePath $Executable -ArgumentList $Arguments -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\$Name.stdout.log" -RedirectStandardError "$sgfRun\$Name.stderr.log"
$record = @{pid=$process.Id; executable=$Executable; project=$sgfProject; port=$Port; started=$process.StartTime.ToUniversalTime().ToString('o')}
$record | ConvertTo-Json | Set-Content -LiteralPath "$sgfRun\$Name.json" -Encoding UTF8
$ready = $false
for ($attempt = 0; $attempt -lt 40; $attempt++) {
$process.Refresh()
if ($process.HasExited) { throw "$Name exited. See SGF run logs." }
$listeners = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
if ($listeners.OwningProcess -contains $process.Id) { $ready = $true; break }
Start-Sleep -Milliseconds 250
}
if (!$ready) { throw "$Name did not become ready. No other process was stopped." }
Write-Output "$Name ready at http://127.0.0.1:$Port"
}
switch ($Action) {
'start' {
Assert-FreePort 8000
Start-SgfDatabase
Start-SgfProcess 'web' $sgfPhp @('-S', '127.0.0.1:8000', '-t', 'public', 'scripts/router.php') 8000
}
'frontend' {
Start-SgfProcess 'vite' $sgfNode @('node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', '5173', '--strictPort') 5173
}
'status' {
& "$sgfPg\pg_ctl.exe" -D $sgfCluster status
Get-NetTCPConnection -LocalPort 55432,8000,5173 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,OwningProcess
}
'stop' {
foreach ($name in @('vite', 'web')) {
$recordFile = "$sgfRun\$name.json"
if (Test-Path -LiteralPath $recordFile) {
$record = Get-Content -LiteralPath $recordFile -Raw | ConvertFrom-Json
$process = Get-Process -Id $record.pid -ErrorAction SilentlyContinue
if ($process) {
if ($process.Path -ne $record.executable -or $record.project -ne $sgfProject -or $process.StartTime.ToUniversalTime().ToString('o') -ne $record.started) {
throw "Process identity mismatch for $name; refusing to stop it."
}
Stop-Process -Id $process.Id
}
Remove-Item -LiteralPath $recordFile
if ($name -eq 'vite') {
$hotFile = Join-Path $sgfProject 'public\hot'
if ((Test-Path -LiteralPath $hotFile) -and (Get-Content -LiteralPath $hotFile -Raw).Trim() -eq 'http://127.0.0.1:5173') {
Remove-Item -LiteralPath $hotFile
}
}
}
}
& "$sgfPg\pg_ctl.exe" -D $sgfCluster -m fast -w stop
if ($LASTEXITCODE -ne 0 -and (Test-Path -LiteralPath "$sgfCluster\postmaster.pid")) { throw 'SGF cluster stop failed.' }
}
'php' { & $sgfPhp @ToolArguments; if ($LASTEXITCODE) { throw 'PHP command failed.' } }
'composer' { & $sgfPhp $sgfComposer @ToolArguments; if ($LASTEXITCODE) { throw 'Composer command failed.' } }
'npm' { & $sgfNode $sgfNpmCli @ToolArguments; if ($LASTEXITCODE) { throw 'npm command failed.' } }
'test' {
if (Test-Path -LiteralPath "$sgfRun\browser-proof.json") { throw 'Stop the browser proof before tests reset sgf_tempo_test.' }
Start-SgfDatabase
& $sgfPhp 'vendor/phpunit/phpunit/phpunit' @ToolArguments
if ($LASTEXITCODE) { throw 'Tests failed.' }
}
'migrate' {
Start-SgfDatabase
$credentials = Get-Content -LiteralPath "$sgfData\secrets\database.json" -Raw | ConvertFrom-Json
$env:APP_ENV = 'local'
$env:DB_CONNECTION = 'pgsql'
$env:DB_HOST = '127.0.0.1'
$env:DB_PORT = '55432'
$env:DB_DATABASE = 'sgf_tempo_dev'
$env:DB_USERNAME = 'sgf_dev_owner'
$env:DB_PASSWORD = $credentials.dev_owner
$env:DB_URL = ''
try {
& $sgfPhp artisan migrate --no-interaction
if ($LASTEXITCODE) { throw 'Development migration failed.' }
} finally {
foreach ($name in @('APP_ENV','DB_CONNECTION','DB_HOST','DB_PORT','DB_DATABASE','DB_USERNAME','DB_PASSWORD','DB_URL')) { Remove-Item "Env:$name" -ErrorAction SilentlyContinue }
}
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$identity = DB::selectOne('SELECT current_database() AS db, current_user AS role');
$privileges = DB::selectOne("SELECT rolsuper, rolcreatedb, rolcreaterole, rolbypassrls FROM pg_roles WHERE rolname = current_user");
$ddl = DB::selectOne("SELECT has_schema_privilege(current_user, 'public', 'CREATE') AS allowed");
$crossDatabase = DB::selectOne("SELECT has_database_privilege(current_user, 'sgf_tempo_test', 'CONNECT') AS allowed");
if ($identity->db !== 'sgf_tempo_dev' || $identity->role !== 'sgf_dev_app'
|| $privileges->rolsuper || $privileges->rolcreatedb || $privileges->rolcreaterole
|| $privileges->rolbypassrls || $ddl->allowed || $crossDatabase->allowed) {
fwrite(STDERR, "Development database identity/privilege check failed.\n");
exit(1);
}
if (DB::table('users')->where('email', 'like', '%@example.test')->exists()) {
fwrite(STDERR, "Synthetic test accounts must not be present in development.\n");
exit(1);
}
echo "Development connection: sgf_dev_app; no administration, schema creation or test database access. No test fixtures in development.\n";
+9
View File
@@ -0,0 +1,9 @@
<?php
require __DIR__.'/../vendor/autoload.php';
try {
[$password, $confirmation] = \App\Modules\Identity\Services\ProtectedProvisioningInput::read(STDIN, 'synthetic@example.test');
$expected = getenv('SGF_SYNTHETIC_HASH');
if (! is_string($expected) || $expected === '' || ! hash_equals($expected, hash('sha256', $password)) || $password !== $confirmation) { exit(1); }
echo "Protected PowerShell-to-PHP transport: OK (synthetic input, no database writes).\n";
} catch (\Throwable) { fwrite(STDERR, "Protected transport verification failed; payload not displayed.\n"); exit(1); }