Communitygithub.com

Boogy auth (autorisation en-service)

Compétences d'agent pour construire des services Boogy — conception, modélisation de données, authentification, jobs, MCP/REST et limites de la plateforme.

Qu'est-ce que Boogy auth (autorisation en-service) ?

Ce skill fournit une mise en œuvre complète du modèle d'autorisation à trois couches de la plateforme Boogy : admission d'entrée, gardes par itinéraire et assistants de portée de lignes. Il évite toute implémentation manuelle de la sécurité en s'appuyant sur des helpers vérifiés émis par le SDK. Le skill détaille la gestion des itinéraires publics au sein d'un service restreint, ainsi que les sous-arbres privés dans un service ouvert, en utilisant des fonctionnalités comme `caller_is_service_owner()`. Il fournit une référence rapide pour les gardes et helpers essentiels (`auth::required()`, `auth::owns_resource()`, `auth::find_owned()`), et insiste sur la règle de sécurité du 'deny-by-existence-mask' (404 vs 403) pour éviter les oracles d'énumération. Enfin, il couvre la configuration des clés API pour les appelants programmatiques, en garantissant l'absence d'escalade de privilèges et le stockage sécurisé des clés hachées.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add Boogy-ai/boogy-superpowers

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Que fait Boogy auth (autorisation en-service) ?

L'autorisation sur Boogy repose sur trois couches. N'en implémentez aucune manuellement — le SDK émet des helpers vérifiés qui préservent les invariants de sécurité.

Le modèle à trois couches

  1. Admission d'entrée — le mode d'entrée de la plateforme décide si une requête atteint votre service. Au moment où votre code s'exécute, l'appelant est déjà un principal connu ou anonyme.
  2. Gardes de routage — des vérifications par itinéraire s'exécutent avant le handler : exiger une connexion, ou exiger la propriété de la ressource adressée.
  3. Assistants de portée de lignes — les handlers listent/chargent uniquement les lignes de l'appelant.

Résolvez l'appelant avec auth::current_principal() -> Option<String>. Le principal est opaque — ne l'analysez jamais, ne supprimez pas de préfixe et ne supposez pas un UUID ; utilisez-le uniquement comme valeur de votre colonne propriétaire et comme entrée pour auth::*.

Entrée par itinéraire : une route publique dans un service restreint

La couche 1 (entrée) a normalement un seul mode pour l'ensemble du service. Mais parfois, une route doit être accessible par des appelants que le reste du service rejette — le cas canonique est un récepteur public /webhook dans un service autrement authenticated (ou internal/mixed). Isolé-la avec un remplacement par itinéraire ; le reste du service reste restreint :

[ingress]
mode = "authenticated"          # valeur par défaut pour tout le service — tout le reste

[[ingress.routes]]
path = "/webhook"               # chemin relatif au service ; TOUTES les méthodes
mode = "public"                 # n'importe qui peut atteindre /webhook

Le contrat de sécurité (lisez-le — il est fermé par défaut) :

RèglePourquoi c'est important
Chemins littéraux relatifs au service. Exact (/webhook) ou préfixe (/stripe/*, à la limite du segment). Pas de capture {param}.Le path est le chemin que voit votre Routeur, le même cadre que vos handlers.
La correspondance la plus spécifique gagne ; exactement un mode s'applique. Exact bat le préfixe ; le préfixe le plus long bat le plus court.Un sous-itinéraire public ne peut jamais élargir un voisin : /webhook public n'assouplit pas /data.
Par défaut = le mode de l'ensemble du service. Un chemin non correspondant (ou une tentative de traversée ..) retombe sur la valeur par défaut restreinte, jamais sur l'itinéraire le plus permissif.Oublier de lister un itinéraire le laisse protégé, pas exposé.
Chaque remplacement a son PROPRE mode + listes d'autorisation. Un remplacement N'HÉRITE PAS de allowed_agents/allowed_origins.Listez-les sur le bloc d'itinéraire ; la même validation non vide s'applique, donc une faute de frappe ne peut pas refuser silencieusement tout le monde.
Par CHEMIN, pas par méthode (aujourd'hui). path = "/webhook" s'applique à GET, POST, etc. de la même manière.Un /webhook public est accessible par n'importe quel verbe — validez donc toujours la requête dans le handler (un GET errant ne devrait rien faire).
Appliqué par l'hôte à la périphérie. L'entrée s'exécute avant l'instanciation de votre wasm.Vous n'auto-gardez PAS un itinéraire public dans le code ; mais un itinéraire public signifie que n'importe qui l'atteint — authentifiez-le d'une autre manière (par exemple, une signature HMAC ; voir boogy:boogy-webhooks).
La porte de délégation + la limite de débit restent à L'ÉCHELLE DU SERVICE.Une exception publique ne peut pas contourner la porte [ingress.delegation] et partage le quota de limite de débit.

Un manifeste sans [[ingress.routes]] se comporte exactement comme avant — c'est purement additif.

L'inverse : un sous-arbre RESTREINT dans un service ouvert (admin propriétaire uniquement)

L'autre direction — un service dont la valeur par défaut est ouverte (authenticated) mais dont le sous-arbre /admin/* n'est accessible que par le propriétaire du service (le provisionneur). Le piège : un module provisionnable est déployé par n'importe qui, vous ne devez donc PAS coder en dur une identité (@alice, boogy://alice/services/*) dans le manifeste — ce propriétaire littéral est erroné pour tout autre provisionneur, et les chaînes de la liste d'autorisation d'entrée ne sont pas substituées au moment du déploiement. L'entrée n'a pas de correspondance "même propriétaire que moi".

Utilisez la capacité non gardée caller_is_service_owner() — l'hôte atteste si l'appelant est le propriétaire DE CE service (leur jeton d'agent, résolu côté hôte par rapport au registre des agents, OU l'un de leurs propres workloads). Pas d'entrée par itinéraire, pas d'identité codée en dur :

[ingress]
mode = "authenticated"          # toutes les routes ; le handler garde /admin lui-même
// require_operator() : attesté par l'hôte, rien n'est codé en dur.
fn require_operator() -> Result<(), ApiError> {
    if caller_is_service_owner() { return Ok(()); }   // le propriétaire : son agent OU son propre workload
    // OBO : le backend du propriétaire agissant pour un utilisateur — l'acteur ATTESTÉ est le workload.
    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("operator only"))
}

Pourquoi c'est la bonne primitive : le propriétaire humain peut accéder à /admin directement avec son propre jeton (le wasm ne peut pas résoudre le handle d'un agent, mais l'hôte le peut — c'est ce que fait la capacité), ET le backend du propriétaire fonctionne en tant que workload. Fermé par défaut : anonyme, un autre propriétaire ou un appelant non résoluble → false. (Voir le module de catalogue resend-base.) Le modèle précédent "internal + workload du même propriétaire" fonctionne également mais EXCLUT l'accès curl humain direct — préférez caller_is_service_owner pour les surfaces réservées au propriétaire.

Référence rapide des gardes et assistants (vérifiés)

ÉlémentUtilisation
auth::required() -> Guard401 si anonyme. Placez-le sur les routes de collection (list, create).
auth::owns_resource(table, owner_col, id_param) -> GuardRoutes d'élément (GET/DELETE /things/{id}). Charge la ligne, 404 si manquante OU pas à vous, la stocke dans req.ctx. .slot("name") pour plusieurs chargements.
auth::find_owned(table, owner_col) -> Result<Vec<Row>, _>Liste limitée au principal pour les points de terminaison d'index. 401 si anonyme.
auth::load_owned(table, owner_col, id) -> Result<Option<Row>, _>Chargement unique + vérification de propriété pour MCP/JSON-RPC (id dans le corps, pas dans le chemin). None = manquant OU pas à vous.
auth::require_scope(scope) -> GuardPorte de capacité grossière : 401 si anonyme, 403 si connecté mais ne possède pas la portée.

La colonne propriétaire est toujours DEFAULT_OWNER_COL ("owner_principal") — n'inventez jamais owner_id/created_by. Câblez les routes d'élément comme .group([auth::owns_resource("things", DEFAULT_OWNER_COL, "id")], |g| …). Les handlers derrière ce garde lisent la ligne stockée dans ctx — ne récupérez pas à nouveau.

Règle propriétaire-à-partir-du-jeton : lors de la création, estampez la colonne propriétaire à partir de auth::current_principal(). Ne lisez JAMAIS le propriétaire depuis le corps de la requête — un client pourrait revendiquer l'id d'un autre utilisateur.

Loi de fer : refus par masque d'existence

Manquant et existe-mais-pas-à-vous retournent tous les deux 404 — JAMAIS 403. Un invariant de sécurité de la plateforme, pas un choix de style. Une séparation 403 contre 404 est un oracle d'énumération : un utilisateur authentifié sonde les identifiants et apprend lesquels existent (403) par rapport à ceux qui n'existent pas (404). Les gardes du SDK appliquent cela.

"Mais l'interface utilisateur veut dire à l'utilisateur pourquoi." Faites-le côté client : traitez 404 uniformément comme "pas disponible pour vous" et affichez ce message ; le statut du fil reste 404. Ne fourchez pas et ne patchez pas le garde du SDK pour séparer le masque. (Le 403 de require_scope est un axe différent — capacité grossière, pas propriété de ressource — et est correct.)

Clés API pour les appelants programmatiques (recette vérifiée)

Invoquez api_keys_glue!(bindings) à côté de wit_glue!, puis :

  1. api_key_routes::install_table() dans init_tables.
  2. Montez les routes de gestion via le trait d'extension ApiKeyRoutes : Router::new().with_api_key_routes() (/_keys) ou .with_api_key_routes_at("/admin/keys").
  3. Gardez vos routes : .group([api_key_routes::guard], |g| ...).
FaitDétail
Double identifiantguard accepte un bearer de session OU une clé sk_* ; les deux s'unifient en current_principal(), donc owns_resource/find_owned fonctionnent inchangés.
Gestion des clésNécessite une identité de session ; une clé sk_* ne peut pas créer de clés.
Pas d'escaladeUne clé ne porte que les portées que le créateur possède déjà (403 sinon).
StockageLes clés vivent hachées dans le propre magasin du service ; ne créez jamais votre propre table.
Formatsk_<env>_<…>_<crc>.

Drapeaux rouges

PenséeRéalité
"Un 403 'pas à vous' dit à l'utilisateur pourquoi."C'est un oracle d'énumération d'identifiant. Retournez 404 pour les deux ; expliquez côté client.
"Je vérifierai la propriété dans le handler après avoir chargé la ligne."owns_resource charge + vérifie + stocke dans ctx en un seul garde. Lisez la ligne stockée ; ne réimplémentez pas la vérification.
"J'ajouterai une table api_keys personnalisée."api_keys_glue! fournit une table hachée, isolée et consciente des portées. Utilisez-la.
"Lisez l'identifiant du propriétaire depuis le corps de la requête."Estampez-le à partir de current_principal(). Le corps est contrôlé par l'attaquant.
"Un itinéraire public [[ingress.routes]] a toujours besoin d'une vérification auth dans le wasm."Public signifie que n'importe qui l'atteint — authentifiez-le autrement (signature HMAC pour les webhooks). Le remplacement ne s'auto-garde pas.

Intégration

← boogy:designing-boogy-services choisit le mode d'entrée alimentant la couche

  1. boogy:boogy-account-auth est l'origine des principaux (connexion → le jeton que vous lisez). Pour agir au nom d'un utilisateur entre services, voir boogy:boogy-obo-delegation. Une exception publique par itinéraire pour un callback signé est la première moitié de boogy:boogy-webhooks.

Skills associés