Implements the model-B2 membership authority (ratified compliance auth design) and inbound token verification.
- GET /v1/users/{id}/memberships: Keycloak supplies user->tenant links + org_roles (attribute projection until the realm migrates to Organizations); registered tenants override status/plan/products from registry tables (source: registry|keycloak). Closes the M5.2-deferred org_roles stub.
- New internal/authn: OIDC discovery + JWKS verification (go-oidc/v3), audience tenant-registry. /healthz + /readyz stay PUBLIC_EXPLICIT; all other routes INTERNAL_SERVICE_ONLY. AUTH_ENABLED=true refuses to start on incomplete config (fail-closed); false (dev default) keeps current behavior.
- Full suite green incl. postgres testcontainers; openapi.yaml updated (contract test passes).
Activation is a separate step (Auth-5): requires the orca-infra env merge and a portal service token (portal currently calls with no auth).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com>
Reviewed-on: #14
75 lines
2.5 KiB
Go
75 lines
2.5 KiB
Go
package server_test
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"gitea.meghsakha.com/platform/tenant-registry/internal/keycloak"
|
|
)
|
|
|
|
type membershipsBody struct {
|
|
UserID string `json:"user_id"`
|
|
Memberships []struct {
|
|
keycloak.Claims
|
|
Source string `json:"source"`
|
|
} `json:"memberships"`
|
|
}
|
|
|
|
func TestGetUserMemberships(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
// The KC attribute projection carries a stale plan/status and a
|
|
// legacy tenant_id — the registry row must win (source: registry).
|
|
h.kcMock.Claims["u-1"] = keycloak.Claims{
|
|
TenantID: "kc-legacy-id", TenantSlug: "acme",
|
|
OrgRoles: []string{"IT_ADMIN"}, Plan: "stale-plan", TenantStatus: "stale",
|
|
}
|
|
resp, raw := h.do(http.MethodGet, "/v1/users/u-1/memberships", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status %d: %s", resp.StatusCode, raw)
|
|
}
|
|
body := decode[membershipsBody](t, raw)
|
|
if body.UserID != "u-1" || len(body.Memberships) != 1 {
|
|
t.Fatalf("unexpected body: %s", raw)
|
|
}
|
|
m := body.Memberships[0]
|
|
if m.Source != "registry" {
|
|
t.Errorf("want source registry, got %q", m.Source)
|
|
}
|
|
if m.TenantID != h.tenant.ID || m.TenantSlug != "acme" {
|
|
t.Errorf("registry identity not authoritative: %+v", m.Claims)
|
|
}
|
|
if m.Plan != h.tenant.Plan || m.TenantStatus != h.tenant.Status {
|
|
t.Errorf("registry lifecycle not authoritative: plan=%q status=%q", m.Plan, m.TenantStatus)
|
|
}
|
|
if len(m.OrgRoles) != 1 || m.OrgRoles[0] != "IT_ADMIN" {
|
|
t.Errorf("org_roles must stay keycloak-owned: %v", m.OrgRoles)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetUserMemberships_unknownTenantFallsBackToKeycloak(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
h.kcMock.Claims["u-2"] = keycloak.Claims{
|
|
TenantID: "ghost-001", TenantSlug: "ghost",
|
|
OrgRoles: []string{"USER"}, Plan: "Scale", TenantStatus: "active",
|
|
}
|
|
resp, raw := h.do(http.MethodGet, "/v1/users/u-2/memberships", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status %d: %s", resp.StatusCode, raw)
|
|
}
|
|
m := decode[membershipsBody](t, raw).Memberships[0]
|
|
if m.Source != "keycloak" || m.TenantSlug != "ghost" || m.Plan != "Scale" {
|
|
t.Errorf("expected untouched keycloak projection, got %+v (source %q)", m.Claims, m.Source)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetUserMemberships_unknownUser404(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, h *testHarness) {
|
|
resp, _ := h.do(http.MethodGet, "/v1/users/nobody/memberships", nil)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("want 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|