- internal/rbacclient: HTTP-Client fuer Core RBAC-06 (POST /authorize) - retentionapi.RequireRole (Header-Provisorium) ersetzt durch RequireRBAC, echter Aufruf gegen RBAC-06, fail-closed bei Fehlern - Mount nimmt jetzt rbacclient.Client entgegen - alle bestehenden RET-06-API-Tests weiterhin gruen - neue Tests: verweigerte Rolle (403 gegen echte RBAC-06-Antwort), erlaubte Rolle (200), RBAC-06 nicht erreichbar -> fail-closed (403) - real deployed auf 131, end-zu-ende per curl nachgewiesen (403/403/200/403) Pruefungen siehe archive/docs/RET-08-PRUEFPROTOKOLL.md
83 lines
2.5 KiB
Go
83 lines
2.5 KiB
Go
// Package rbacclient ist ein schlanker HTTP-Client für Core RBAC-06
|
|
// (internal/policyapi, POST /authorize). Archive ist ein physisch
|
|
// getrenntes Go-Modul und kann Core RBAC-02 (internal/policy) daher
|
|
// nicht direkt importieren — nur über die HTTP-Schnittstelle von
|
|
// RBAC-06 aufrufen.
|
|
package rbacclient
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// ErrDenied wird zurückgegeben, wenn RBAC-06 die Anfrage ausdrücklich
|
|
// ablehnt (kein Transport-/Serverfehler, sondern eine bewusste
|
|
// Policy-Entscheidung).
|
|
var ErrDenied = errors.New("rbac: zugriff verweigert")
|
|
|
|
// Client ruft den RBAC-06-Endpunkt auf.
|
|
type Client struct {
|
|
BaseURL string
|
|
ServiceToken string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func New(baseURL, serviceToken string) *Client {
|
|
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: http.DefaultClient}
|
|
}
|
|
|
|
type authorizeRequest struct {
|
|
Role string `json:"role"`
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
type authorizeResponse struct {
|
|
Allowed bool `json:"allowed"`
|
|
}
|
|
|
|
// Authorize fragt RBAC-06, ob role die permission besitzt. Jeder
|
|
// Fehler (Transport, Timeout, unerwarteter Status) wird als Fehler
|
|
// zurückgegeben, NICHT stillschweigend als "erlaubt" behandelt — der
|
|
// Aufrufer muss bei einem Fehler fail-closed (verweigern) reagieren.
|
|
func (c *Client) Authorize(ctx context.Context, role, permission string) (bool, error) {
|
|
body, err := json.Marshal(authorizeRequest{Role: role, Permission: permission})
|
|
if err != nil {
|
|
return false, fmt.Errorf("rbac: request kodieren: %w", err)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/authorize", bytes.NewReader(body))
|
|
if err != nil {
|
|
return false, fmt.Errorf("rbac: request bauen: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Service-Token", c.ServiceToken)
|
|
|
|
resp, err := c.httpClient().Do(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("rbac: aufruf fehlgeschlagen: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return false, fmt.Errorf("rbac: unerwarteter status %d", resp.StatusCode)
|
|
}
|
|
var out authorizeResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return false, fmt.Errorf("rbac: antwort dekodieren: %w", err)
|
|
}
|
|
if !out.Allowed {
|
|
return false, ErrDenied
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (c *Client) httpClient() *http.Client {
|
|
if c.HTTPClient != nil {
|
|
return c.HTTPClient
|
|
}
|
|
return http.DefaultClient
|
|
}
|