// Package tenantengine calls tenant-engine's cache-read endpoint at // token-issuance time to source the optional tenant_roles claim // (net-kingdom/canon/standards/iam-profile_v0.3.md, "Tenant Roles"). // // This is the cache-read direction only, and it fails OPEN -- the opposite // of flex-auth's live-lookup adapter (flex-auth/internal/adapters/tenantengine), // which must fail closed. tenant_roles is documented as a cache callers // must never trust for privileged decisions (flex-auth re-validates live // before authorizing aal2-class actions); losing this claim at issuance // time is a performance regression, not a security one. Blocking login // because a cache source is briefly down would be the wrong trade. package tenantengine import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" ) // Client fetches cached capability roles for a tenant. type Client struct { BaseURL string HTTP *http.Client } // New returns a tenant-engine cache-read client. A nil httpClient gets a // short default timeout -- this call sits on the synchronous token-issuance // path and must not turn a cache miss into a slow login. func New(baseURL string, httpClient *http.Client) *Client { if httpClient == nil { httpClient = &http.Client{Timeout: 2 * time.Second} } return &Client{BaseURL: strings.TrimRight(baseURL, "/"), HTTP: httpClient} } // Roles fetches GET /tenants/{tenantID}/roles. // // Returns (nil, false) -- not an error -- on any failure: unreachable // tenant-engine, non-200 response, or a malformed body. Callers must treat // false as "omit the tenant_roles claim entirely", never as "emit an empty // or stale role list". func (c *Client) Roles(ctx context.Context, tenantID string) ([]string, bool) { if c == nil || c.BaseURL == "" { return nil, false } url := fmt.Sprintf("%s/tenants/%s/roles", c.BaseURL, tenantID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, false } resp, err := c.HTTP.Do(req) if err != nil { return nil, false } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, false } var body struct { Roles []string `json:"roles"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, false } return body.Roles, true }