feat(identity): 接入认证中心多租户登录
支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestTenantContextClientUsesApplicationMachineTokenAndETag(t *testing.T) {
|
||||
tenantID, applicationID := uuid.NewString(), uuid.NewString()
|
||||
var issuer string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/issuer/shared/.well-known/openid-configuration":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"issuer": issuer, "token_endpoint": serverURL(r) + "/issuer/shared/token",
|
||||
})
|
||||
case "/issuer/shared/token":
|
||||
clientID, secret, ok := r.BasicAuth()
|
||||
if !ok || clientID != "gateway-machine" || secret != "machine-secret-material" ||
|
||||
r.FormValue("grant_type") != "client_credentials" || r.FormValue("scope") != "tenant.context.read" {
|
||||
t.Fatal("tenant context token request was not least-privilege client credentials")
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "opaque-machine-token", "token_type": "Bearer", "expires_in": 300,
|
||||
})
|
||||
case "/api/v1/runtime/tenants/" + tenantID:
|
||||
if r.Header.Get("Authorization") != "Bearer opaque-machine-token" {
|
||||
t.Fatal("tenant context request omitted the machine token")
|
||||
}
|
||||
if r.Header.Get("If-None-Match") == `"tenant-v7"` {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", `"tenant-v7"`)
|
||||
_ = json.NewEncoder(w).Encode(TenantContext{
|
||||
ApplicationID: applicationID, TenantID: tenantID, DisplayName: "Tenant A", Slug: "tenant-a",
|
||||
TenantStatus: "active", TenantApplicationStatus: "active", Version: "7",
|
||||
UpdatedAt: time.Unix(1_780_000_000, 0).UTC(),
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
issuer = server.URL + "/issuer/shared"
|
||||
client, err := NewTenantContextClient(
|
||||
"test", issuer, applicationID, server.URL+"/api/v1/runtime/tenants/{tenantId}",
|
||||
"urn:easyai:auth-center:tenant-context", "tenant.context.read",
|
||||
func(context.Context) (string, []byte, error) {
|
||||
return "gateway-machine", []byte("machine-secret-material"), nil
|
||||
}, server.Client(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tenant, unchanged, err := client.Get(context.Background(), tenantID, "")
|
||||
if err != nil || unchanged || tenant.ETag != `"tenant-v7"` || !tenant.Active() {
|
||||
t.Fatalf("tenant=%#v unchanged=%t error=%v", tenant, unchanged, err)
|
||||
}
|
||||
_, unchanged, err = client.Get(context.Background(), tenantID, tenant.ETag)
|
||||
if err != nil || !unchanged {
|
||||
t.Fatalf("etag request unchanged=%t error=%v", unchanged, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantContextClientDistinguishesNotFoundFromTemporaryFailure(t *testing.T) {
|
||||
tenantID, applicationID := uuid.NewString(), uuid.NewString()
|
||||
for _, test := range []struct {
|
||||
status int
|
||||
want error
|
||||
}{
|
||||
{status: http.StatusNotFound, want: ErrTenantContextNotFound},
|
||||
{status: http.StatusServiceUnavailable, want: ErrTenantContextUnavailable},
|
||||
} {
|
||||
t.Run(http.StatusText(test.status), func(t *testing.T) {
|
||||
var issuer string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/issuer/shared/.well-known/openid-configuration":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"issuer": issuer, "token_endpoint": serverURL(r) + "/issuer/shared/token",
|
||||
})
|
||||
case r.URL.Path == "/issuer/shared/token":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "opaque-machine-token", "token_type": "Bearer", "expires_in": 300,
|
||||
})
|
||||
default:
|
||||
w.WriteHeader(test.status)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
issuer = server.URL + "/issuer/shared"
|
||||
client, err := NewTenantContextClient(
|
||||
"test", issuer, applicationID, server.URL+"/api/v1/runtime/tenants/{tenantId}",
|
||||
"urn:easyai:auth-center:tenant-context", "tenant.context.read",
|
||||
func(context.Context) (string, []byte, error) {
|
||||
return "gateway-machine", []byte("machine-secret-material"), nil
|
||||
}, server.Client(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := client.Get(context.Background(), tenantID, ""); err != test.want {
|
||||
t.Fatalf("error=%v want=%v", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serverURL(request *http.Request) string {
|
||||
return "http://" + request.Host
|
||||
}
|
||||
Reference in New Issue
Block a user