28 lines
7.0 KiB
PowerShell
28 lines
7.0 KiB
PowerShell
$ErrorActionPreference='Stop'
|
|||
|
|
$PSNativeCommandUseErrorActionPreference=$true
|
||
|
|
$root=Split-Path -Parent $PSScriptRoot;Set-Location -LiteralPath $root
|
||
|
|
$api='http://localhost:58080';$tokenUrl='http://localhost:58180/realms/voice-local/protocol/openid-connect/token';$tenant='10000000-0000-0000-0000-000000000010';$device='10000000-0000-0000-0000-000000000511';$deviceSecret=[Text.Encoding]::UTF8.GetBytes('local-simulated-device-secret-32-bytes-minimum')
|
||
|
|
$conversation=$null;$accessID=$null;$createKey='ops-access-conversation-'+[Guid]::NewGuid().ToString('N');$requestID='ops-access-smoke-'+[Guid]::NewGuid().ToString('N')
|
||
|
|
function Token([string]$name){(Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{grant_type='password';client_id='voice-platform-web';username=$name;password='LocalOnly123!';scope='openid'}).access_token}
|
||
|
|
function SignedPost([string]$path,[string]$body,[string]$key){$bytes=[Text.Encoding]::UTF8.GetBytes($body);$digest=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant();$timestamp=[DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ');$nonce=[Guid]::NewGuid().ToString('N');$canonical="POST`n$path`n$timestamp`n$nonce`n$digest";$hmac=[Security.Cryptography.HMACSHA256]::new($deviceSecret);try{$signature=[Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))).TrimEnd('=').Replace('+','-').Replace('/','_')}finally{$hmac.Dispose()};Invoke-WebRequest -UseBasicParsing -Method Post -Uri ($api+$path) -ContentType 'application/json' -Body $body -Headers @{'X-Device-Id'=$device;'X-Device-Timestamp'=$timestamp;'X-Device-Nonce'=$nonce;'X-Device-Content-SHA256'=$digest;'X-Device-Signature'=$signature;'Idempotency-Key'=$key}}
|
||
|
|
function OpsPost([string]$path,[object]$body,[string]$key,[string]$token){Invoke-WebRequest -UseBasicParsing -Method Post -Uri ($api+$path) -Headers @{Authorization="Bearer $token";'X-Tenant-Id'=$tenant;'Idempotency-Key'=$key;'X-Request-Id'=$requestID} -ContentType 'application/json' -Body ($body|ConvertTo-Json -Compress -Depth 5)}
|
||
|
|
try{
|
||
|
|
$owner=Token 'owner';$approver=Token 'editor'
|
||
|
|
$createBody=@{device_time=[DateTime]::UtcNow.ToString('o');monotonic_ms=35791;capabilities=@{rtc=$true;audio_tracks=@('near','far');upload_resume=$true};network=@{type='wifi'};requested_goal='open_conversation'}|ConvertTo-Json -Compress -Depth 8
|
||
|
|
$conversation=[string](((SignedPost '/device/v2/conversations' $createBody $createKey).Content|ConvertFrom-Json).data.conversation_id)
|
||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "INSERT INTO conversation_turn(tenant_id,subject_id,conversation_id,turn_key,sequence_no,speaker,start_at,end_at,final_text,source_type) SELECT c.tenant_id,c.subject_id,c.id,'ops-access-smoke-turn',1,'subject',now()-interval '2 seconds',now()-interval '1 second','仅在双人批准后返回的烟测字幕','provider' FROM conversation c WHERE c.public_id='$conversation'::uuid;"|Out-Null
|
||
|
|
$expires=[DateTime]::UtcNow.AddHours(1).ToString('o');$create=OpsPost '/api/v1/ops/sensitive-access-requests' @{conversation_id=$conversation;capabilities=@('TRANSCRIPT_READ');reason='核对指定通话的最终转写异常';expires_at=$expires} ('ops-access-request-'+[Guid]::NewGuid().ToString('N')) $owner
|
||
|
|
$accessID=[string](($create.Content|ConvertFrom-Json).data.id);if($create.StatusCode-ne201-or$accessID-notmatch'^[0-9a-fA-F-]{36}$'){throw '临时访问申请未创建'}
|
||
|
|
try{OpsPost "/api/v1/ops/sensitive-access-requests/$accessID/review" @{decision='APPROVE';reason='申请人不得自批'} ('ops-access-self-'+[Guid]::NewGuid().ToString('N')) $owner|Out-Null;throw '申请人自批不应成功'}catch{if($null-eq$_.Exception.Response-or[int]$_.Exception.Response.StatusCode-ne409){throw}}
|
||
|
|
$review=OpsPost "/api/v1/ops/sensitive-access-requests/$accessID/review" @{decision='APPROVE';reason='审批人复核问题范围与最小能力后批准'} ('ops-access-review-'+[Guid]::NewGuid().ToString('N')) $approver;if((($review.Content|ConvertFrom-Json).data.status)-ne'APPROVED'){throw '双人审批未生效'}
|
||
|
|
try{Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/ops/sensitive-conversations/$conversation" -Headers @{Authorization="Bearer $approver";'X-Tenant-Id'=$tenant;'X-Ops-Access-Grant'=$accessID;'X-Request-Id'=$requestID}|Out-Null;throw '审批人不应使用申请人的临时权限'}catch{if($null-eq$_.Exception.Response-or[int]$_.Exception.Response.StatusCode-ne403){throw}}
|
||
|
|
$sensitive=Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/ops/sensitive-conversations/$conversation" -Headers @{Authorization="Bearer $owner";'X-Tenant-Id'=$tenant;'X-Ops-Access-Grant'=$accessID;'X-Request-Id'=$requestID};$data=($sensitive.Content|ConvertFrom-Json).data
|
||
|
|
if(@($data.turns).Count-ne1-or$data.turns[0].final_text-ne'仅在双人批准后返回的烟测字幕'){throw '批准后的字幕读取不正确'}
|
||
|
|
$counts=(docker compose exec -T postgres psql -U voice -d voice_platform -t -A -F '|' -c "SELECT count(*) FILTER(WHERE result='SUCCESS')||'|'||count(*) FILTER(WHERE result='DENIED') FROM ops_sensitive_access_log WHERE access_request_id=(SELECT id FROM ops_sensitive_access_request WHERE public_id='$accessID'::uuid)"|Out-String).Trim();if($counts-ne'1|1'){throw "逐次访问日志不正确:$counts"}
|
||
|
|
Write-Host '临时敏感数据访问烟测通过:申请、自批拒绝、双人审批、申请人绑定、字幕读取及成功/拒绝逐次审计均正确。'
|
||
|
|
}finally{
|
||
|
|
if($conversation-match'^[0-9a-fA-F-]{36}$'){docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "BEGIN; CREATE TEMP TABLE ops_access_conv AS SELECT id,context_snapshot_id,consent_snapshot_id FROM conversation WHERE public_id='$conversation'::uuid; DELETE FROM audit_event WHERE request_id='$requestID' OR resource_id=NULLIF('$accessID','')::uuid; DELETE FROM ops_sensitive_access_log WHERE conversation_id IN(SELECT id FROM ops_access_conv); DELETE FROM ops_sensitive_access_request WHERE conversation_id IN(SELECT id FROM ops_access_conv); DELETE FROM conversation_turn WHERE conversation_id IN(SELECT id FROM ops_access_conv); DELETE FROM conversation_state_history WHERE conversation_id IN(SELECT id FROM ops_access_conv); DELETE FROM rtc_task WHERE conversation_id IN(SELECT id FROM ops_access_conv); DELETE FROM outbox_event WHERE aggregate_id IN(SELECT id FROM ops_access_conv); DELETE FROM conversation WHERE id IN(SELECT id FROM ops_access_conv); DELETE FROM context_snapshot WHERE id IN(SELECT context_snapshot_id FROM ops_access_conv); DELETE FROM consent_snapshot WHERE id IN(SELECT consent_snapshot_id FROM ops_access_conv); COMMIT;"|Out-Null}
|
||
|
|
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "DELETE FROM idempotency_record WHERE (actor_key LIKE 'ops:%' AND scope LIKE 'POST /api/v1/ops/sensitive-access-requests%') OR (actor_key='device:$device' AND idempotency_key='$createKey');"|Out-Null
|
||
|
|
Write-Host '已清理临时提权烟测的通话、申请、访问日志、审计和幂等记录。'
|
||
|
|
}
|