Communitygithub.com

Autorização Boogy (autorização em serviço)

Habilidades de agente para construir serviços Boogy — design, modelagem de dados, autenticação, jobs, MCP/REST e limites da plataforma.

O que é Autorização Boogy (autorização em serviço)?

Este arquivo de habilidade descreve o modelo de autorização de três camadas da plataforma Boogy: (1) admissão de ingresso — decide se uma solicitação chega ao serviço; (2) guardas por rota — verificações pré-handler como exigir login ou propriedade do recurso; e (3) auxiliares que escopam linhas — handlers carregam apenas as linhas do chamador. Ele documenta como expor uma rota pública dentro de um serviço restrito (ex.: `/webhook`) usando substituições por rota no manifesto TOML, com regras de segurança de fail-closed (bloqueio por padrão). Também cobre como proteger uma subárvore restrita (ex.: `/admin`) para apenas o proprietário do serviço, usando a verificação `caller_is_service_owner()` sem identidades codificadas. A habilidade fornece referência rápida para guardas e auxiliares verificados (`auth::required()`, `auth::owns_resource()`, `auth::find_owned()`), impõe a Lei de Ferro de mascarar existência (sempre 404 para itens não encontrados ou não pertencentes ao usuário, nunca 403), e inclui uma receita verificada para chaves de API (`api_keys_glue!`) com gerenciamento e guarda de escopo. A habilidade é compatível com qualquer plataforma de agente de engenharia de software (Claude Code, Cursor, Codex, Copilot, etc.). O guia integra-se com habilidades adjacentes: design de serviços, auth de conta, delegação OBO e webhooks.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add Boogy-ai/boogy-superpowers

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que Autorização Boogy (autorização em serviço) faz?

A autorização no Boogy tem três camadas. Não implemente nenhuma delas manualmente — o SDK emite auxiliares verificados que mantêm as invariantes de segurança intactas.

O modelo de três camadas

  1. Admissão de ingresso — o modo de ingresso da plataforma decide se uma solicitação chega ao seu serviço. Quando seu código executa, o chamador já é um principal conhecido ou anônimo.
  2. Guardas protegem rotas — verificações por rota são executadas antes do handler: exigir login, ou exigir propriedade do recurso endereçado.
  3. Auxiliares escopam linhas — handlers listam/carregam apenas as linhas do chamador.

Resolva o chamador com auth::current_principal() -> Option<String>. O principal é opaco — nunca analise, remova prefixos ou presuma um UUID; use-o apenas como valor da coluna de proprietário e como entrada para auth::*.

Ingresso por rota: uma rota pública dentro de um serviço restrito

A camada 1 (ingresso) normalmente é um único mode para todo o serviço. Mas às vezes uma rota deve ser acessível por chamadores que o resto do serviço rejeita — o caso canônico é um receptor público /webhook dentro de um serviço que, de outra forma, é authenticated (ou internal/mixed). Abra uma exceção com substituição por rota; o resto do serviço permanece restrito:

[ingress]
mode = "authenticated"          # padrão para todo o serviço — todo o resto

[[ingress.routes]]
path = "/webhook"               # caminho relativo ao serviço; TODOS os métodos
mode = "public"                 # qualquer um pode acessar /webhook

O contrato de segurança (leia-o — é fail-closed):

RegraPor que é importante
Caminhos literais relativos ao serviço. Exato (/webhook) ou prefixo (/stripe/*, com limite de segmento). Sem captura {param}.O path é o caminho que seu Router vê, o mesmo contexto que seus handlers.
Correspondência mais específica vence; exatamente um modo se aplica. Exato vence prefixo; prefixo mais longo vence mais curto.Uma sub-rota pública nunca pode alargar uma irmã: /webhook público não afrouxa /data.
Padrão = o modo de todo o serviço. Um caminho sem correspondência (ou uma tentativa de travessia ..) cai no padrão restrito, nunca na rota mais permissiva.Esquecer de listar uma rota a deixa protegida, não exposta.
Cada substituição carrega SEU PRÓPRIO modo + listas de permissão. Uma substituição NÃO herda allowed_agents/allowed_origins.Liste-os no bloco da rota; a mesma validação não vazia se aplica para que um erro de digitação não possa negar-tudo silenciosamente.
Por CAMINHO, não por método (hoje). path = "/webhook" se aplica a GET, POST, etc. igualmente.Um /webhook público é acessível por qualquer verbo — então ainda valide a requisição no handler (um GET indevido deve não fazer nada).
Aplicado pelo host na borda. O ingresso executa antes de seu wasm ser instanciado.Você NÃO autoprotege uma rota pública em código; mas uma rota pública significa que qualquer um a acessa — autentique-a de outra forma (ex.: uma assinatura HMAC; veja boogy:boogy-webhooks).
Portão de delegação + limite de taxa permanecem PARA TODO O SERVIÇO.Uma exceção pública não pode contornar o portão [ingress.delegation] e compartilha o balde de limite de taxa.

Um manifesto sem [[ingress.routes]] se comporta exatamente como antes — isto é puramente aditivo.

O inverso: uma subárvore RESTRITA dentro de um serviço aberto (somente /admin do proprietário)

A outra direção — um serviço cujo padrão é aberto (authenticated) mas cuja subárvore /admin/* é acessível apenas pelo proprietário do serviço (o provisionador). A armadilha: um módulo provisionável é implantado por qualquer pessoa, então você NÃO deve codificar uma identidade (@alice, boogy://alice/services/*) no manifesto — esse proprietário literal está errado para todos os outros provisionadores, e as strings de lista de permissão de ingresso não são substituídas no momento da implantação. O ingresso não tem um correspondente "mesmo proprietário que eu".

Use a capacidade não portada caller_is_service_owner() — o host atesta se o chamador é o proprietário DESTE serviço (seu token de agente, resolvido no lado do host contra o registro de agentes, OU uma de suas próprias cargas de trabalho). Sem ingresso por rota, sem identidade codificada:

[ingress]
mode = "authenticated"          # todas as rotas; o handler protege /admin
// require_operator(): atestado pelo host, nada codificado.
fn require_operator() -> Result<(), ApiError> {
    if caller_is_service_owner() { return Ok(()); }   // o proprietário: seu agente OU própria carga de trabalho
    // OBO: o backend do proprietário agindo por um usuário — o ATOR ATESTADO é a carga de trabalho.
    let id = current_identity();
    let actor_owner = id.and_then(|i| i.actor).and_then(workload_owner);
    if actor_owner == Some(self_identity().owner) { return Ok(()); }
    Err(ApiError::forbidden("somente operador"))
}

Por que este é o primitivo correto: o proprietário humano pode acessar /admin diretamente com seu próprio token (o wasm não pode resolver o handle de um agente, mas o host pode — é isso que a capacidade faz), E o backend do proprietário funciona como uma carga de trabalho. Fail-closed: anônimo, um proprietário diferente ou um chamador irresolúvel → false. (Veja o módulo de catálogo resend-base.) O padrão anterior "internal + carga de trabalho do mesmo proprietário" também funciona, mas EXCLUI o acesso humano direto — prefira caller_is_service_owner para superfícies somente do proprietário.

Referência rápida de guardas e auxiliares (verificados)

ItemUso
auth::required() -> Guard401 se anônimo. Coloque em rotas de coleção (list, create).
auth::owns_resource(table, owner_col, id_param) -> GuardRotas de item (GET/DELETE /things/{id}). Carrega a linha, 404 se ausente OU não for sua, a armazena em req.ctx. .slot("name") para múltiplos carregamentos.
auth::find_owned(table, owner_col) -> Result<Vec<Row>, _>Lista com escopo de principal para endpoints de índice. 401 quando anônimo.
auth::load_owned(table, owner_col, id) -> Result<Option<Row>, _>Carregamento único + verificação de propriedade para MCP/JSON-RPC (id no corpo, não no caminho). None = ausente OU não é sua.
auth::require_scope(scope) -> GuardPortão de capacidade grosseira: 401 se anônimo, 403 se logado mas sem o escopo.

A coluna de proprietário é sempre DEFAULT_OWNER_COL ("owner_principal") — nunca invente owner_id/created_by. Conecte rotas de item como .group([auth::owns_resource("things", DEFAULT_OWNER_COL, "id")], |g| …). Handlers atrás desse guarda leem a linha armazenada em ctx — não a busque novamente.

Regra de proprietário a partir do token: ao criar, estampe a coluna de proprietário a partir de auth::current_principal(). NUNCA leia o proprietário do corpo da requisição — um cliente poderia reivindicar o id de outro usuário.

Lei de Ferro: negar por máscara de existência

Ausente e existe-mas-não-é-seu ambos retornam 404 — NUNCA 403. Uma invariante de segurança da plataforma, não uma escolha de estilo. Uma divisão 403-vs-404 é um oráculo de enumeração: um usuário autenticado testa ids e descobre quais existem (403) versus quais não existem (404). Os guardas do SDK impõem isso.

"Mas a UI quer dizer ao usuário o porquê." Faça isso no lado do cliente: trate 404 uniformemente como "não disponível para você" e renderize essa mensagem; o status do fio permanece 404. Não bifurque ou corrija o guarda do SDK para quebrar a máscara. (O 403 do require_scope é um eixo diferente — capacidade grosseira, não propriedade de recurso — e está correto.)

Chaves de API para chamadores programáticos (receita verificada)

Invoque api_keys_glue!(bindings) ao lado de wit_glue!, então:

  1. api_key_routes::install_table() em init_tables.
  2. Monte rotas de gerenciamento via trait de extensão ApiKeyRoutes: Router::new().with_api_key_routes() (/_keys) ou .with_api_key_routes_at("/admin/keys").
  3. Proteja suas rotas: .group([api_key_routes::guard], |g| ...).
FatoDetalhe
Credencial duplaguard aceita um bearer de sessão OU uma chave sk_*; ambos se unificam em current_principal(), então owns_resource/find_owned funcionam sem alterações.
Gerenciar chavesRequer uma identidade de sessão; uma chave sk_* não pode criar chaves.
Sem escalonamentoUma chave carrega apenas escopos que o criador já possui (403 caso contrário).
ArmazenamentoChaves vivem hashadas no próprio armazenamento do serviço; nunca crie sua própria tabela.
Formatosk_<env>_<…>_<crc>.

Bandeiras vermelhas

PensamentoRealidade
"403 'não é seu' diz ao usuário o porquê."É um oráculo de enumeração de id. Retorne 404 para ambos; explique no lado do cliente.
"Vou verificar a propriedade no handler após carregar a linha."owns_resource faz carregamento + verificação + armazenamento em ctx em um guarda. Leia a linha armazenada; não reimplemente a verificação.
"Vou adicionar uma tabela customizada de chaves de API."api_keys_glue! fornece uma tabela hashada, isolada e ciente de escopo. Use-a.
"Leia o id do proprietário do corpo da requisição."Estampe-o a partir de current_principal(). O corpo é controlado pelo atacante.
"Uma rota pública [[ingress.routes]] ainda precisa de uma verificação de auth no wasm."Pública significa que qualquer um a acessa — autentique-a de outra forma (assinatura HMAC para webhooks). A substituição não se autoprotege.

Integração

← boogy:designing-boogy-services escolhe o modo de ingresso alimentando a camada

  1. boogy:boogy-account-auth é de onde vêm os principais (login → o token que você lê). Para agir em nome de um usuário entre serviços, veja boogy:boogy-obo-delegation. Uma exceção pública por rota para um callback assinado é a metade frontal de boogy:boogy-webhooks.

Habilidades Relacionadas