← CCSP: segurança cloud, dados e operação
24 / 26 · 55 MIN

APIs: autorização, repetição e limites

Autoriza cada operação e objeto, delimita consumo e distingue repetição controlada de garantias distribuídas.

Define identidade, ação e objeto antes do efeito

O laboratório cria trabalhos fictícios de exportação de relatórios por HTTP em loopback. Não faz pagamentos nem consulta serviços externos. Um mapa de credenciais de teste identifica tenant, sujeito e permissão de escrita. Isso não é um fornecedor de identidade nem valida tokens reais. A operação POST /exports exige escrita e um relatório do tenant autorizado. GET /jobs/{id} permite leitura do trabalho dentro do tenant. O tenant vem da identidade de teste, não de um campo livre no corpo. O exercício distingue conhecer um identificador de estar autorizado a usá-lo. IDs difíceis de adivinhar podem reduzir descoberta, mas não substituem a decisão de acesso. A política desta ficha é por tenant; não demonstra direitos mais finos entre utilizadores do mesmo tenant.

Aplica o contrato a entradas e operações

A implementação aceita apenas reportId e format, com csv ou json. Um campo tenant adicional, JSON malformado, um array ou formato não suportado são recusados. O corpo tem limite didático de 512 bytes e não inicia trabalhos quando é recusado. No entanto, o script drena o corpo recebido antes de devolver a recusa e não implementa proteção de produção contra ligações lentas ou fluxos intermináveis. O endpoint de orçamento permite dois pedidos por tenant e janela de 1000 unidades de tempo injetado; não é uma medição de um segundo real. O teste mostra que sujeitos do mesmo tenant partilham orçamento e outro tenant tem saldo separado. Em produção, também é necessário decidir tamanho por operação, concorrência, custo e comportamento dos componentes a jusante.

Dá à repetição um âmbito e um contrato explícitos

Um cliente pode perder a resposta depois de o trabalho ter sido criado. Repetir um POST sem conhecer a semântica pode duplicar o efeito. O RFC9110 distingue métodos idempotentes e condições para repetição; não torna qualquer POST idempotente. Neste contrato autoral, a chave inclui tenant, sujeito, método, rota e Idempotency-Key. O fingerprint usa os campos relevantes do pedido, não a ordem textual das propriedades JSON. A primeira chamada cria um trabalho e devolve 201; uma repetição equivalente devolve o mesmo ID com 200. A mesma chave com formato diferente devolve 409. Uma chave igual noutro sujeito ou tenant tem âmbito distinto. Estes códigos e a retenção em memória pertencem ao laboratório, não a uma norma universal de chaves de idempotência.

Reautoriza antes de devolver resultados antigos

O laboratório verifica a permissão atual e o relatório antes de consultar o resultado guardado. Depois de retirar escrita à identidade que criou o trabalho, repetir a chave devolve 403 e não cria efeitos novos. Devolver a resposta guardada antes dessa verificação poderia ignorar uma mudança de acesso. Oito chamadas simultâneas com a mesma chave produzem um único trabalho neste processo: a consulta e inserção no mapa não têm await entre si. Isso não prova exatamente uma execução num sistema distribuído. Com várias instâncias, falha de processo ou efeito externo, é necessário desenhar persistência, atomicidade, estados intermédios e reconciliação. Expirar a chave também muda o comportamento de uma repetição tardia. Define esses limites antes de anunciar uma garantia aos clientes.

Executa a matriz e interpreta apenas a evidência obtida

Executa o script completo com a versão Node indicada no README. Ele abre uma porta efémera em 127.0.0.1, envia pedidos com dados fictícios, conserva observações HTTP e fecha o servidor. Os testes cobrem identidade ausente, leitura sem escrita, relatório de outro tenant, campos inválidos, tamanho, conflitos de chave, concorrência, mudança de permissão e orçamento. Verifica os resultados e o contador de efeitos, não só o código HTTP. São esperados quatro trabalhos no conjunto de pedidos aceites. Os mapas não são persistentes, não há TLS ou transação com base de dados, e o relógio do orçamento é controlado pelo teste. Resumo: idempotência não concede autorização e um limite numa camada não protege todo o serviço. Usa a evidência para planear o ensaio real, não para declarar prontidão de produção.

// Original teaching fixture. Loopback only; fake credentials and in-memory state.
import http from 'node:http';
import fs from 'node:fs';
import {createHash} from 'node:crypto';
const identities=new Map([['a-reader',{tenant:'a',subject:'reader',write:false}],['a-writer',{tenant:'a',subject:'writer',write:true}],['a-other',{tenant:'a',subject:'other',write:true}],['b-writer',{tenant:'b',subject:'writer',write:true}]]);
const objects=new Map([['report-a',{tenant:'a'}],['report-b',{tenant:'b'}]]),jobs=new Map(),ledger=new Map(),budgets=new Map();
const checks=[],observations=[];let effects=0,now=0;
function check(id,pass){checks.push({id,pass:Boolean(pass)});}
function send(res,status,body){res.writeHead(status,{'Content-Type':'application/json','Cache-Control':'no-store'});res.end(JSON.stringify(body));}
const server=http.createServer(async(req,res)=>{
 const principal=identities.get((req.headers.authorization||'').replace(/^Bearer /,''));
 if(!principal)return send(res,401,{error:'unauthenticated'});
 const path=new URL(req.url,'http://127.0.0.1').pathname;
 if(path==='/limited'&&req.method==='GET'){
  const window=Math.floor(now/1000),key=JSON.stringify([principal.tenant,window]),used=budgets.get(key)||0;
  if(used>=2)return send(res,429,{error:'fixture-rate-limit'});
  budgets.set(key,used+1);return send(res,200,{accepted:true});
 }
 if(path.startsWith('/jobs/')&&req.method==='GET'){
  const job=jobs.get(path.slice(6));return job&&job.tenant===principal.tenant?send(res,200,{id:job.id,reportId:job.reportId}):send(res,404,{error:'not-found'});
 }
 if(path!=='/exports'||req.method!=='POST')return send(res,405,{error:'unsupported-operation'});
 if(!principal.write)return send(res,403,{error:'function-denied'});
 let bytes=0,parts=[];
 for await(const chunk of req){bytes+=chunk.length;if(bytes<=512)parts.push(chunk);}
 if(bytes>512)return send(res,413,{error:'body-limit'});
 let body;try{body=JSON.parse(Buffer.concat(parts).toString('utf8'));}catch{return send(res,400,{error:'invalid-json'});}
 if(!body||Array.isArray(body)||typeof body!=='object'||Object.keys(body).sort().join(',')!=='format,reportId'||typeof body.reportId!=='string'||!['csv','json'].includes(body.format))return send(res,400,{error:'invalid-fields'});
 const object=objects.get(body.reportId);
 if(!object||object.tenant!==principal.tenant)return send(res,404,{error:'not-found'});
 const idempotency=req.headers['idempotency-key'];
 if(typeof idempotency!=='string'||!/^[A-Za-z0-9_-]{1,64}$/.test(idempotency))return send(res,400,{error:'invalid-idempotency-key'});
 const key=JSON.stringify([principal.tenant,principal.subject,req.method,path,idempotency]);
 const fingerprint=JSON.stringify([body.reportId,body.format]);
 // No await between lookup and insertion: atomic only inside this single JS process.
 const previous=ledger.get(key);
 if(previous)return previous.fingerprint===fingerprint?send(res,200,{id:previous.id,replayed:true}):send(res,409,{error:'key-payload-conflict'});
 const id='job-'+(++effects),job={id,tenant:principal.tenant,reportId:body.reportId,format:body.format};
 jobs.set(id,job);ledger.set(key,{id,fingerprint});return send(res,201,{id,replayed:false});
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));const origin='http://127.0.0.1:'+server.address().port;
async function request(path,token='a-writer',body,opts={}){
 const headers={};if(token)headers.Authorization='Bearer '+token;if(body!==undefined)headers['Content-Type']='application/json';if(opts.key!==null)headers['Idempotency-Key']=opts.key||'request-1';
 const method=opts.method||(body===undefined?'GET':'POST'),res=await fetch(origin+path,{method,headers,body:body===undefined?undefined:typeof body==='string'?body:JSON.stringify(body),signal:AbortSignal.timeout(3000)}),data=await res.json();observations.push({path,method,identity:token||'absent',status:res.status,body:data});return{status:res.status,data};
}
const payload={reportId:'report-a',format:'csv'};
try{
 check('missing-identity-denied',(await request('/exports',null,payload)).status===401);
 check('unknown-identity-denied',(await request('/exports','unknown',payload)).status===401);
 check('reader-cannot-create',(await request('/exports','a-reader',payload)).status===403);
 check('cross-tenant-report-denied',(await request('/exports','b-writer',payload)).status===404);
 check('missing-report-denied',(await request('/exports','a-writer',{...payload,reportId:'missing'})).status===404);
 check('body-tenant-injection-denied',(await request('/exports','a-writer',{...payload,tenant:'b'})).status===400);
 check('unsupported-format-denied',(await request('/exports','a-writer',{...payload,format:'xml'})).status===400);
 check('malformed-json-denied',(await request('/exports','a-writer','{')).status===400);
 check('array-body-denied',(await request('/exports','a-writer',[])).status===400);
 check('oversize-body-denied',(await request('/exports','a-writer','x'.repeat(513))).status===413);
 check('missing-idempotency-key-denied',(await request('/exports','a-writer',payload,{key:null})).status===400);
 check('unsafe-idempotency-key-denied',(await request('/exports','a-writer',payload,{key:'bad key'})).status===400);
 check('rejected-inputs-have-no-effects',effects===0);
 const first=await request('/exports','a-writer',payload);check('first-create',first.status===201&&effects===1);
 const repeat=await request('/exports','a-writer',{format:'csv',reportId:'report-a'});check('reordered-fields-replay-same-result',repeat.status===200&&repeat.data.id===first.data.id&&effects===1);
 check('same-key-different-payload-conflict',(await request('/exports','a-writer',{...payload,format:'json'})).status===409&&effects===1);
 const parallel=await Promise.all(Array.from({length:8},()=>request('/exports','a-writer',payload,{key:'parallel'})));
 check('concurrent-same-key-one-effect',new Set(parallel.map(r=>r.data.id)).size===1&&parallel.filter(r=>r.status===201).length===1&&parallel.filter(r=>r.status===200).length===7&&effects===2);
 const other=await request('/exports','a-other',payload);check('same-key-other-principal-is-separate',other.status===201&&other.data.id!==first.data.id&&effects===3);
 const b=await request('/exports','b-writer',{reportId:'report-b',format:'csv'});check('same-key-other-tenant-is-separate',b.status===201&&b.data.id!==first.data.id&&effects===4);
 check('own-job-readable',(await request('/jobs/'+first.data.id,'a-reader')).status===200);
 check('other-tenant-job-not-disclosed',(await request('/jobs/'+first.data.id,'b-writer')).status===404);
 identities.get('a-writer').write=false;check('replay-reauthorizes-current-permission',(await request('/exports','a-writer',payload)).status===403&&effects===4);identities.get('a-writer').write=true;
 check('first-budget-request',(await request('/limited','a-reader')).status===200);
 check('second-budget-request',(await request('/limited','a-writer')).status===200);
 check('same-tenant-budget-shared',(await request('/limited','a-other')).status===429);
 check('other-tenant-budget-independent',(await request('/limited','b-writer')).status===200);
 now=1000;check('injected-clock-next-window',(await request('/limited','a-reader')).status===200);
 check('wrong-method-denied',(await request('/exports','a-writer',undefined,{method:'GET'})).status===405);
 check('no-additional-side-effects',effects===4);
}finally{await new Promise(r=>server.close(r));check('owned-loopback-server-closed',!server.listening);}
const result={runtime:process.version,scriptSHA256:createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex'),passed:checks.filter(x=>x.pass).length,failed:checks.filter(x=>!x.pass).length,checks,requestCount:observations.length,observations,effects,limits:['Fictional credentials, tenants and reports; no actual identity provider, payment or cloud system.','HTTP loopback teaching server; no TLS, database durability, distributed lock or process-failure recovery.','Rate window uses an injected clock; not a wall-clock performance measurement.','Input is counted while drained; no production connection, timeout or streaming-abuse protection.','In-memory job/idempotency/budget maps are finite in this run, not a production retention design.','Authorization is tenant-level in this fixture, not a claim of per-user entitlement to every object in a tenant.','Single-process atomic insertion does not prove distributed exactly-once execution.']};
console.log(JSON.stringify(result,null,2));if(result.failed)process.exitCode=1;
NA PRÁTICA

Oito pedidos simultâneos com a mesma chave criam um trabalho local. Retirar a permissão antes da repetição devolve 403, mesmo com resultado guardado.

Armadilhas comuns

Chave sem âmbito de tenant; cache antes de autorização; retry cego de POST; resultado local como garantia distribuída; limite de bytes como proteção completa.

Tópicos relacionados: Capacidade, disponibilidade e isolamento · Autorização, repetição e aceitação operacional

Leva esta ideia contigo

Autoriza cada operação e objeto, delimita consumo e distingue repetição controlada de garantias distribuídas.

Criar conta

Referência: API1:2023 Broken Object Level Authorization · CCSP examination outline effective 2026-08-01; January2026 V2 PDF

CCSP® é uma marca registada de ISC2, Inc. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por ISC2. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.