Boogy Auth (dienstinterne Autorisierung)
Die Autorisierung auf Boogy erfolgt in drei Schichten. Keine davon sollte selbst implementiert werden – das SDK gibt geprüfte Hilfsfunktionen aus, die die Sicherheitsinvarianten intakt halten.
Das Drei-Schichten-Modell
- Ingress lässt zu – der Ingress-Modus der Plattform entscheidet, ob eine Anfrage Ihren Dienst erreicht. Wenn Ihr Code läuft, ist der Aufrufer bereits ein bekannter Principal oder anonym.
- Guards schützen Routen – pro Route werden vor dem Handler Prüfungen durchgeführt: Anmeldung erforderlich oder Besitz der adressierten Ressource erforderlich.
- Helpers begrenzen Zeilen – Handler listen/laden nur die Zeilen des Aufrufers.
Ermitteln Sie den Aufrufer mit auth::current_principal() -> Option<String>.
Der Principal ist undurchsichtig – parsen, Präfix entfernen oder Annahmen über eine UUID sind tabu; verwenden Sie ihn nur als Wert Ihrer Besitzer-Spalte und als Eingabe für auth::*.
Pro-Route-Ingress: eine öffentliche Route in einem eingeschränkten Dienst
Schicht 1 (Ingress) ist normalerweise ein dienstweiter mode. Aber manchmal muss eine Route für Aufrufer erreichbar sein, die der Rest des Dienstes ablehnt – der kanonische Fall ist ein öffentlicher /webhook-Empfänger innerhalb eines ansonsten authenticated (oder internal/mixed) Dienstes. Heben Sie ihn mit einer Pro-Route-Überschreibung hervor; der Rest des Dienstes bleibt eingeschränkt:
[ingress]
mode = "authenticated" # dienstweite Voreinstellung – alles andere
[[ingress.routes]]
path = "/webhook" # dienstrelativer Pfad; ALLE Methoden
mode = "public" # jeder kann /webhook erreichen
Der Sicherheitsvertrag (lesen Sie ihn – er ist fail-closed):
| Regel | Warum wichtig |
|---|---|
Dienstrelative, literale Pfade. Exakt (/webhook) oder Präfix (/stripe/*, Segmentgrenze). Kein {param}-Capture. | Der path ist der Pfad, den Ihr Router sieht, derselbe Kontext wie Ihre Handler. |
| Spezifischste Übereinstimmung gewinnt; genau ein Modus wird angewendet. Exakt schlägt Präfix; längerer Präfix schlägt kürzeren. | Eine öffentliche Unterroute kann einen Geschwisterpfad niemals aufweiten: /webhook öffentlich lockert /data nicht. |
Standard = der dienstweite Modus. Ein nicht gematchter Pfad (oder ein ..-Traversalversuch) fällt auf den eingeschränkten Standard zurück, niemals auf die permissivste Route. | Wenn Sie vergessen, eine Route aufzulisten, bleibt sie geschützt, nicht exponiert. |
Jede Überschreibung trägt ihren EIGENEN Modus + Erlaubnislisten. Eine Überschreibung erbt NICHT allowed_agents/allowed_origins. | Listen Sie sie im Routenblock; dieselbe Nicht-Leer-Validierung gilt, sodass ein Tippfehler nicht stillschweigend „Alle ablehnen“ verursachen kann. |
Pro PFAD, nicht pro Methode (derzeit). path = "/webhook" gilt gleichermaßen für GET, POST usw. | Ein öffentliches /webhook ist mit jedem Verb erreichbar – validieren Sie die Anfrage also im Handler (ein versehentlicher GET sollte nichts tun). |
| Host-erzwungen am Edge. Ingress läuft bevor Ihr Wasm instanziiert wird. | Sie schützen eine öffentliche Route NICHT selbst im Code; aber eine öffentliche Route bedeutet, dass jeder sie erreicht – authentifizieren Sie sie auf andere Weise (z. B. eine HMAC-Signatur; siehe boogy:boogy-webhooks). |
| Delegations-Gate + Ratenbegrenzung bleiben DIENSTWEIT. | Eine öffentliche Aussparung kann das [ingress.delegation]-Gate nicht umgehen und teilt sich den Ratenbegrenzungs-Bucket. |
Ein Manifest ohne [[ingress.routes]] verhält sich genau wie zuvor – dies ist rein additiv.
Das Gegenteil: ein EINGESCHRÄNKTER Teilbaum in einem offenen Dienst (nur Besitzer /admin)
Die andere Richtung – ein Dienst, dessen Standard offen ist (authenticated), aber dessen /admin/*-Teilbaum nur für den Dienstbesitzer (den Provisioner) erreichbar ist. Die Falle: Ein provisionierbares Modul wird von jedem bereitgestellt, daher dürfen Sie KEINE Identität (@alice, boogy://alice/services/*) im Manifest fest codieren – dieser literale Besitzer ist für jeden anderen Provisioner falsch, und Ingress-Erlaubnislisten-Strings werden zur Bereitstellungszeit nicht substituiert. Ingress hat keinen „derselbe Besitzer wie ich“-Matcher.
Verwenden Sie die ungatede caller_is_service_owner()-Fähigkeit – der Host bezeugt, ob der Aufrufer der BESITZER dieses Dienstes ist (ihr Agent-Token, hostseitig gegen die Agenten-Registry aufgelöst, ODER einer ihrer eigenen Workloads). Kein Pro-Route-Ingress, keine fest codierte Identität:
[ingress]
mode = "authenticated" # alle Routen; der Handler schützt /admin selbst
// require_operator(): host-bezeugt, nichts fest codiert.
fn require_operator() -> Result<(), ApiError> {
if caller_is_service_owner() { return Ok(()); } // der Besitzer: ihr Agent ODER eigener Workload
// OBO: das Backend des Besitzers, das für einen Benutzer handelt – der BEZEUGTE Akteur ist der 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("nur Betreiber"))
}
Warum dies das richtige Primitiv ist: Der menschliche Besitzer kann /admin direkt mit seinem eigenen Token aufrufen (das Wasm kann den Handle eines Agenten nicht auflösen, aber der Host kann – das tut die Fähigkeit), UND das Backend des Besitzers funktioniert als Workload. Fail-closed: anonym, ein anderer Besitzer oder ein nicht auflösbarer Aufrufer → false. (Siehe das Katalogmodul resend-base.) Das frühere „internal + selber-Besitzer-Workload“-Muster funktioniert auch, schließt aber das DIREKTE menschliche Curl aus – bevorzugen Sie caller_is_service_owner für besitzerbeschränkte Oberflächen.
Guard- und Helper-Kurzreferenz (geprüft)
| Element | Verwendung |
|---|---|
auth::required() -> Guard | 401 bei anonym. Für Sammlungsrouten (list, create). |
auth::owns_resource(table, owner_col, id_param) -> Guard | Elementrouten (GET/DELETE /things/{id}). Lädt die Zeile, 404 bei fehlend ODER nicht Ihnen gehörend, legt sie in req.ctx ab. .slot("name") für mehrere Ladevorgänge. |
auth::find_owned(table, owner_col) -> Result<Vec<Row>, _> | Principal-begrenzte Liste für Index-Endpunkte. 401 bei anonym. |
auth::load_owned(table, owner_col, id) -> Result<Option<Row>, _> | Einzelladung + Besitzprüfung für MCP/JSON-RPC (id im Body, nicht im Pfad). None = fehlend ODER nicht Ihnen gehörend. |
auth::require_scope(scope) -> Guard | Grobe Fähigkeitsprüfung: 401 bei anonym, 403 bei angemeldet, aber ohne den Bereich. |
Die Besitzer-Spalte ist immer DEFAULT_OWNER_COL ("owner_principal") – niemals owner_id/created_by erfinden. Verdrahten Sie Elementrouten als .group([auth::owns_resource("things", DEFAULT_OWNER_COL, "id")], |g| …). Handler hinter diesem Guard lesen die im Kontext abgelegte Zeile – nicht erneut abrufen.
Besitzer-vom-Token-Regel: Beim Erstellen die Besitzer-Spalte aus auth::current_principal() setzen. NIEMALS den Besitzer aus dem Anforderungsbody lesen – ein Client könnte die ID eines anderen Benutzers beanspruchen.
Eisernes Gesetz: Verweigerung durch Existenzmaskierung
Fehlend und existiert-aber-nicht-Ihnen gehörend geben BEIDES 404 zurück – NIEMALS 403. Eine Plattform-Sicherheitsinvariante, keine Stilfrage. Eine 403-vs-404-Aufteilung ist ein Aufzählungsorakel: Ein authentifizierter Benutzer testet IDs und erfährt, welche existieren (403) gegenüber welchen nicht (404). Die SDK-Guards erzwingen dies.
„Aber die UI möchte dem Benutzer den Grund mitteilen.“ Machen Sie es clientseitig: Behandeln Sie 404 einheitlich als „für Sie nicht verfügbar“ und geben Sie diese Nachricht aus; der Drahtstatus bleibt 404. Verzweigen oder patchen Sie den SDK-Guard NICHT, um die Maskierung aufzuteilen. (require_scope‘s 403 ist eine andere Achse – grobe Fähigkeit, nicht Ressourcenbesitz – und ist korrekt.)
API-Schlüssel für programmatische Aufrufer (geprüftes Rezept)
Rufen Sie api_keys_glue!(bindings) neben wit_glue! auf, dann:
api_key_routes::install_table()ininit_tables.- Management-Routen über die
ApiKeyRoutes-Erweiterungs-Trait mounten:Router::new().with_api_key_routes()(/_keys) oder.with_api_key_routes_at("/admin/keys"). - Ihre Routen schützen:
.group([api_key_routes::guard], |g| ...).
| Fakt | Detail |
|---|---|
| Doppelte Anmeldedaten | guard akzeptiert ein Session-Bearer ODER einen sk_*-Schlüssel; beide vereinheitlichen sich zu current_principal(), sodass owns_resource/find_owned unverändert funktionieren. |
| Schlüssel verwalten | Erfordert eine Session-Identität; ein sk_*-Schlüssel kann keine Schlüssel erstellen. |
| Keine Eskalation | Ein Schlüssel trägt nur Bereiche, die der Ersteller bereits besitzt (sonst 403). |
| Speicherung | Schlüssel leben gehasht im eigenen Speicher des Dienstes; niemals eine eigene Tabelle erstellen. |
| Format | sk_<env>_<…>_<crc>. |
Warnsignale
| Gedanke | Realität |
|---|---|
| „403 ‚nicht Ihr‘ sagt dem Benutzer, warum.“ | Es ist ein ID-Aufzählungsorakel. Geben Sie für beide 404 zurück; erklären Sie clientseitig. |
| „Ich werde die Besitzprüfung im Handler nach dem Laden der Zeile durchführen.“ | owns_resource erledigt Laden + Prüfen + Kontext-Ablegen in einem Guard. Lesen Sie die abgelegte Zeile; implementieren Sie die Prüfung nicht neu. |
| „Ich werde eine eigene api_keys-Tabelle hinzufügen.“ | api_keys_glue! liefert eine gehashte, isolierte, bereichsbewusste Tabelle. Verwenden Sie sie. |
| „Besitzer-ID aus dem Anforderungsbody lesen.“ | Setzen Sie sie aus current_principal(). Der Body wird vom Angreifer kontrolliert. |
„Eine öffentliche [[ingress.routes]]-Route braucht trotzdem eine Auth-Prüfung im Wasm.“ | Öffentlich bedeutet, dass jeder sie erreicht – authentifizieren Sie sie auf andere Weise (HMAC-Signatur für Webhooks). Die Überschreibung schützt nicht selbst. |
Integration
← boogy:designing-boogy-services wählt den Ingress-Modus, der Schicht 1 speist. boogy:boogy-account-auth ist die Quelle der Principals (Login → das Token, das Sie lesen). Für das Handeln im Namen eines Benutzers über Dienste hinweg siehe boogy:boogy-obo-delegation. Eine öffentliche Pro-Route-Aussparung für einen signierten Callback ist die vordere Hälfte von boogy:boogy-webhooks.