FLEX-WP-0008 T03-T04: tenant-engine live-lookup context adapter, close
internal/adapters/tenantengine: HTTPClient.LiveRoles() calls tenant-engine's
GET /tenants/{id}/roles/live. LiveRolesResult.Available is the load-bearing
field -- false on any transport error, non-200, or malformed body, never
inferred as zero roles. AttachToContext() writes both tenant_roles and
tenant_roles_available into a CheckRequest.Context map; a consuming Rego
policy must check tenant_roles_available before trusting tenant_roles.
Architectural finding recorded in the workplan: engine.go's Check() has no
context-adapter hook, and none of the existing topaz/relationship/rule
adapters are wired into cmd/flex-auth either -- they're standalone packages
for downstream composition. This adapter is a request-preparation helper a
protected system's own request-building code calls before POST /v1/check,
not an engine-internal hook, matching that precedent exactly.
9 Go tests; gofmt/vet/build clean; go test ./... green repo-wide.
Verified as a real three-service chain: live flex-auth serve + live
tenant-engine, created a tenant and granted it a CUS role through the real
flex-auth-gated write path, then read it back through this adapter (via a
throwaway harness, not committed) -- known tenant: roles=[CUS]
available=true; unknown tenant: roles=[] available=false err="status 404".
FLEX-WP-0008 closed: T01-T04 all done. tenant-engine's write path is now
real end-to-end; any other protected system can pull live tenant capability
role context, fail-closed. Remaining open item unchanged from TEN-WP-0003:
KEY-WP-0005 (key-cape's IAM Profile core-claims gap).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ea84d240b
commit
d2dc39a8b8
5 changed files with 372 additions and 3 deletions
117
internal/adapters/tenantengine/http_client_test.go
Normal file
117
internal/adapters/tenantengine/http_client_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package tenantengine_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netkingdom/flex-auth/internal/adapters/tenantengine"
|
||||
)
|
||||
|
||||
func TestLiveRolesReturnsRolesOnSuccess(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tenants/t-1/roles/live" {
|
||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"tenant_id":"t-1","roles":["CUS","VEN"]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := tenantengine.NewHTTPClient(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClient: %v", err)
|
||||
}
|
||||
|
||||
result, err := client.LiveRoles(context.Background(), "t-1")
|
||||
if err != nil {
|
||||
t.Fatalf("LiveRoles: %v", err)
|
||||
}
|
||||
if !result.Available {
|
||||
t.Fatal("expected Available = true")
|
||||
}
|
||||
if len(result.Roles) != 2 || result.Roles[0] != "CUS" || result.Roles[1] != "VEN" {
|
||||
t.Fatalf("unexpected roles: %v", result.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveRolesReturnsUnavailableOnNon200(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, _ := tenantengine.NewHTTPClient(server.URL)
|
||||
result, err := client.LiveRoles(context.Background(), "t-1")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if result.Available {
|
||||
t.Fatal("expected Available = false on a 503, not indistinguishable from zero roles")
|
||||
}
|
||||
if result.Roles != nil {
|
||||
t.Fatalf("expected nil roles on failure, got %v", result.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveRolesReturnsUnavailableOnMalformedBody(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("not json"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, _ := tenantengine.NewHTTPClient(server.URL)
|
||||
result, err := client.LiveRoles(context.Background(), "t-1")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if result.Available {
|
||||
t.Fatal("expected Available = false on malformed body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveRolesReturnsUnavailableOnConnectionFailure(t *testing.T) {
|
||||
client, _ := tenantengine.NewHTTPClient("http://127.0.0.1:1")
|
||||
|
||||
result, err := client.LiveRoles(context.Background(), "t-1")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
if result.Available {
|
||||
t.Fatal("expected Available = false on connection failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveRolesRespectsContextTimeout(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"tenant_id":"t-1","roles":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, _ := tenantengine.NewHTTPClient(server.URL)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result, err := client.LiveRoles(ctx, "t-1")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected a timeout error")
|
||||
}
|
||||
if result.Available {
|
||||
t.Fatal("expected Available = false on timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClientRequiresBaseURL(t *testing.T) {
|
||||
if _, err := tenantengine.NewHTTPClient(""); err == nil {
|
||||
t.Fatal("expected an error for empty base URL")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue