358 lines
13 KiB
Go
358 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"account/internal/auth"
|
|
"account/internal/store"
|
|
)
|
|
|
|
func TestXWorkmateVaultLiveIntegration(t *testing.T) {
|
|
vaultAddr := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_ADDR"))
|
|
vaultToken := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_TOKEN"))
|
|
vaultMount := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_MOUNT"))
|
|
if vaultAddr == "" || vaultToken == "" {
|
|
t.Skip("live vault integration requires XWORKMATE_VAULT_ADDR and XWORKMATE_VAULT_TOKEN")
|
|
}
|
|
if vaultMount == "" {
|
|
vaultMount = "kv"
|
|
}
|
|
|
|
vaultService, err := NewXWorkmateVaultService(XWorkmateVaultConfig{
|
|
Address: vaultAddr,
|
|
Token: vaultToken,
|
|
Namespace: strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
|
|
Mount: vaultMount,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create vault service: %v", err)
|
|
}
|
|
|
|
router, _, token := newXWorkmateTestHarnessWithVault(t, nil, vaultService)
|
|
|
|
profileBody, err := json.Marshal(map[string]any{
|
|
"profile": map[string]any{
|
|
"openclawUrl": "wss://gateway.example.com",
|
|
"openclawOrigin": "https://gateway.example.com",
|
|
"vaultUrl": vaultAddr,
|
|
"vaultNamespace": strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
|
|
"apisixUrl": "https://apigw.example.com",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal profile payload: %v", err)
|
|
}
|
|
|
|
putProfileReq := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/profile", bytes.NewReader(profileBody))
|
|
putProfileReq.Header.Set("Content-Type", "application/json")
|
|
putProfileReq.Header.Set("Authorization", "Bearer "+token)
|
|
putProfileReq.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
|
|
putProfileRec := httptest.NewRecorder()
|
|
router.ServeHTTP(putProfileRec, putProfileReq)
|
|
if putProfileRec.Code != http.StatusOK {
|
|
t.Fatalf("expected profile update success, got %d: %s", putProfileRec.Code, putProfileRec.Body.String())
|
|
}
|
|
|
|
targets := []string{
|
|
store.XWorkmateSecretLocatorTargetOpenclawGatewayToken,
|
|
store.XWorkmateSecretLocatorTargetVaultRootToken,
|
|
store.XWorkmateSecretLocatorTargetAIGatewayAccessToken,
|
|
}
|
|
secretValuePrefix := "live-vault-check-" + time.Now().UTC().Format("20060102T150405.000000000")
|
|
|
|
for _, target := range targets {
|
|
body, err := json.Marshal(map[string]any{"value": secretValuePrefix + "-" + target})
|
|
if err != nil {
|
|
t.Fatalf("marshal secret payload for %s: %v", target, err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/secrets/"+target, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected live secret write success for %s, got %d: %s", target, rec.Code, rec.Body.String())
|
|
}
|
|
if strings.Contains(rec.Body.String(), secretValuePrefix) {
|
|
t.Fatalf("expected live secret write response to hide raw value for %s", target)
|
|
}
|
|
}
|
|
|
|
getSecretsReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/secrets", nil)
|
|
getSecretsReq.Header.Set("Authorization", "Bearer "+token)
|
|
getSecretsReq.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
|
|
getSecretsRec := httptest.NewRecorder()
|
|
router.ServeHTTP(getSecretsRec, getSecretsReq)
|
|
if getSecretsRec.Code != http.StatusOK {
|
|
t.Fatalf("expected live secret status fetch success, got %d: %s", getSecretsRec.Code, getSecretsRec.Body.String())
|
|
}
|
|
if strings.Contains(getSecretsRec.Body.String(), secretValuePrefix) {
|
|
t.Fatalf("expected live secret status response to hide raw values")
|
|
}
|
|
|
|
var getSecretsResp struct {
|
|
TokenConfigured struct {
|
|
Openclaw bool `json:"openclaw"`
|
|
Vault bool `json:"vault"`
|
|
Apisix bool `json:"apisix"`
|
|
} `json:"tokenConfigured"`
|
|
}
|
|
if err := json.Unmarshal(getSecretsRec.Body.Bytes(), &getSecretsResp); err != nil {
|
|
t.Fatalf("decode live secret status response: %v", err)
|
|
}
|
|
if !getSecretsResp.TokenConfigured.Openclaw || !getSecretsResp.TokenConfigured.Vault || !getSecretsResp.TokenConfigured.Apisix {
|
|
t.Fatalf("expected all live tokenConfigured statuses true, got %#v", getSecretsResp.TokenConfigured)
|
|
}
|
|
|
|
for _, target := range targets {
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/auth/xworkmate/secrets/"+target, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected live secret delete success for %s, got %d: %s", target, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
getProfileReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile", nil)
|
|
getProfileReq.Header.Set("Authorization", "Bearer "+token)
|
|
getProfileReq.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
|
|
getProfileRec := httptest.NewRecorder()
|
|
router.ServeHTTP(getProfileRec, getProfileReq)
|
|
if getProfileRec.Code != http.StatusOK {
|
|
t.Fatalf("expected live profile fetch success, got %d: %s", getProfileRec.Code, getProfileRec.Body.String())
|
|
}
|
|
|
|
var profileResp struct {
|
|
TokenConfigured struct {
|
|
Openclaw bool `json:"openclaw"`
|
|
Vault bool `json:"vault"`
|
|
Apisix bool `json:"apisix"`
|
|
} `json:"tokenConfigured"`
|
|
}
|
|
if err := json.Unmarshal(getProfileRec.Body.Bytes(), &profileResp); err != nil {
|
|
t.Fatalf("decode live profile response: %v", err)
|
|
}
|
|
if profileResp.TokenConfigured.Openclaw || profileResp.TokenConfigured.Vault || profileResp.TokenConfigured.Apisix {
|
|
t.Fatalf("expected live profile tokenConfigured statuses to reset after cleanup, got %#v", profileResp.TokenConfigured)
|
|
}
|
|
}
|
|
|
|
func TestXWorkmateVaultLiveIntegrationPrivateScope(t *testing.T) {
|
|
vaultAddr := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_ADDR"))
|
|
vaultToken := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_TOKEN"))
|
|
vaultMount := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_MOUNT"))
|
|
if vaultAddr == "" || vaultToken == "" {
|
|
t.Skip("live vault integration requires XWORKMATE_VAULT_ADDR and XWORKMATE_VAULT_TOKEN")
|
|
}
|
|
if vaultMount == "" {
|
|
vaultMount = "kv"
|
|
}
|
|
|
|
vaultService, err := NewXWorkmateVaultService(XWorkmateVaultConfig{
|
|
Address: vaultAddr,
|
|
Token: vaultToken,
|
|
Namespace: strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
|
|
Mount: vaultMount,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create vault service: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
st := store.NewMemoryStore()
|
|
|
|
tenantID := "sandbox-live-" + time.Now().UTC().Format("20060102t150405000000000")
|
|
tenantDomain := tenantID + ".svc.plus"
|
|
tenant := &store.Tenant{
|
|
ID: tenantID,
|
|
Name: "Sandbox Live Tenant",
|
|
Edition: store.TenantPrivateEdition,
|
|
}
|
|
if err := st.EnsureTenant(ctx, tenant); err != nil {
|
|
t.Fatalf("ensure tenant: %v", err)
|
|
}
|
|
if err := st.EnsureTenantDomain(ctx, &store.TenantDomain{
|
|
TenantID: tenant.ID,
|
|
Domain: tenantDomain,
|
|
Kind: store.TenantDomainKindGenerated,
|
|
IsPrimary: true,
|
|
Status: store.TenantDomainStatusVerified,
|
|
}); err != nil {
|
|
t.Fatalf("ensure tenant domain: %v", err)
|
|
}
|
|
|
|
user := &store.User{
|
|
Name: "Vault Sandbox Operator",
|
|
Email: "vault-sandbox-operator@example.com",
|
|
EmailVerified: true,
|
|
Role: store.RoleAdmin,
|
|
Level: store.LevelAdmin,
|
|
Active: true,
|
|
}
|
|
if err := st.CreateUser(ctx, user); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
if err := st.UpsertTenantMembership(ctx, &store.TenantMembership{
|
|
TenantID: tenant.ID,
|
|
UserID: user.ID,
|
|
Role: store.TenantMembershipRoleAdmin,
|
|
}); err != nil {
|
|
t.Fatalf("upsert tenant membership: %v", err)
|
|
}
|
|
|
|
token := "sandbox-live-token"
|
|
if err := st.CreateSession(ctx, token, user.ID, time.Now().Add(time.Hour)); err != nil {
|
|
t.Fatalf("create session: %v", err)
|
|
}
|
|
|
|
engine := newPrivateScopeLiveRouter(t, st, vaultService)
|
|
|
|
profileBody, err := json.Marshal(map[string]any{
|
|
"profile": map[string]any{
|
|
"openclawUrl": "wss://gateway.example.com",
|
|
"openclawOrigin": "https://gateway.example.com",
|
|
"vaultUrl": vaultAddr,
|
|
"vaultNamespace": strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
|
|
"apisixUrl": "https://apigw.example.com",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal profile payload: %v", err)
|
|
}
|
|
|
|
putProfileReq := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/profile", bytes.NewReader(profileBody))
|
|
putProfileReq.Header.Set("Content-Type", "application/json")
|
|
putProfileReq.Header.Set("Authorization", "Bearer "+token)
|
|
putProfileReq.Header.Set("X-Forwarded-Host", tenantDomain)
|
|
putProfileRec := httptest.NewRecorder()
|
|
engine.ServeHTTP(putProfileRec, putProfileReq)
|
|
if putProfileRec.Code != http.StatusOK {
|
|
t.Fatalf("expected private-scope profile update success, got %d: %s", putProfileRec.Code, putProfileRec.Body.String())
|
|
}
|
|
|
|
targets := []string{
|
|
store.XWorkmateSecretLocatorTargetOpenclawGatewayToken,
|
|
store.XWorkmateSecretLocatorTargetVaultRootToken,
|
|
store.XWorkmateSecretLocatorTargetAIGatewayAccessToken,
|
|
}
|
|
secretValuePrefix := "private-live-vault-check-" + time.Now().UTC().Format("20060102T150405.000000000")
|
|
|
|
for _, target := range targets {
|
|
body, err := json.Marshal(map[string]any{"value": secretValuePrefix + "-" + target})
|
|
if err != nil {
|
|
t.Fatalf("marshal secret payload for %s: %v", target, err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/secrets/"+target, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("X-Forwarded-Host", tenantDomain)
|
|
rec := httptest.NewRecorder()
|
|
engine.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected private-scope secret write success for %s, got %d: %s", target, rec.Code, rec.Body.String())
|
|
}
|
|
if strings.Contains(rec.Body.String(), secretValuePrefix) {
|
|
t.Fatalf("expected private-scope secret write response to hide raw value for %s", target)
|
|
}
|
|
}
|
|
|
|
getSecretsReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/secrets", nil)
|
|
getSecretsReq.Header.Set("Authorization", "Bearer "+token)
|
|
getSecretsReq.Header.Set("X-Forwarded-Host", tenantDomain)
|
|
getSecretsRec := httptest.NewRecorder()
|
|
engine.ServeHTTP(getSecretsRec, getSecretsReq)
|
|
if getSecretsRec.Code != http.StatusOK {
|
|
t.Fatalf("expected private-scope secret status fetch success, got %d: %s", getSecretsRec.Code, getSecretsRec.Body.String())
|
|
}
|
|
|
|
var getSecretsResp struct {
|
|
ProfileScope string `json:"profileScope"`
|
|
TokenConfigured struct {
|
|
Openclaw bool `json:"openclaw"`
|
|
Vault bool `json:"vault"`
|
|
Apisix bool `json:"apisix"`
|
|
} `json:"tokenConfigured"`
|
|
}
|
|
if err := json.Unmarshal(getSecretsRec.Body.Bytes(), &getSecretsResp); err != nil {
|
|
t.Fatalf("decode private-scope secret status response: %v", err)
|
|
}
|
|
if getSecretsResp.ProfileScope != store.XWorkmateProfileScopeUserPrivate {
|
|
t.Fatalf("expected private profile scope, got %q", getSecretsResp.ProfileScope)
|
|
}
|
|
if !getSecretsResp.TokenConfigured.Openclaw || !getSecretsResp.TokenConfigured.Vault || !getSecretsResp.TokenConfigured.Apisix {
|
|
t.Fatalf("expected all private tokenConfigured statuses true, got %#v", getSecretsResp.TokenConfigured)
|
|
}
|
|
|
|
for _, target := range targets {
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/auth/xworkmate/secrets/"+target, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("X-Forwarded-Host", tenantDomain)
|
|
rec := httptest.NewRecorder()
|
|
engine.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected private-scope secret delete success for %s, got %d: %s", target, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
getProfileReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile", nil)
|
|
getProfileReq.Header.Set("Authorization", "Bearer "+token)
|
|
getProfileReq.Header.Set("X-Forwarded-Host", tenantDomain)
|
|
getProfileRec := httptest.NewRecorder()
|
|
engine.ServeHTTP(getProfileRec, getProfileReq)
|
|
if getProfileRec.Code != http.StatusOK {
|
|
t.Fatalf("expected private-scope profile fetch success, got %d: %s", getProfileRec.Code, getProfileRec.Body.String())
|
|
}
|
|
|
|
var profileResp struct {
|
|
ProfileScope string `json:"profileScope"`
|
|
TokenConfigured struct {
|
|
Openclaw bool `json:"openclaw"`
|
|
Vault bool `json:"vault"`
|
|
Apisix bool `json:"apisix"`
|
|
} `json:"tokenConfigured"`
|
|
}
|
|
if err := json.Unmarshal(getProfileRec.Body.Bytes(), &profileResp); err != nil {
|
|
t.Fatalf("decode private-scope profile response: %v", err)
|
|
}
|
|
if profileResp.ProfileScope != store.XWorkmateProfileScopeUserPrivate {
|
|
t.Fatalf("expected private profile scope after cleanup, got %q", profileResp.ProfileScope)
|
|
}
|
|
if profileResp.TokenConfigured.Openclaw || profileResp.TokenConfigured.Vault || profileResp.TokenConfigured.Apisix {
|
|
t.Fatalf("expected private profile tokenConfigured statuses to reset after cleanup, got %#v", profileResp.TokenConfigured)
|
|
}
|
|
}
|
|
|
|
func newPrivateScopeLiveRouter(t *testing.T, st store.Store, vaultService xworkmateVaultService) *gin.Engine {
|
|
t.Helper()
|
|
|
|
router := gin.New()
|
|
RegisterRoutes(
|
|
router,
|
|
WithStore(st),
|
|
WithEmailVerification(false),
|
|
WithTokenService(auth.NewTokenService(auth.TokenConfig{
|
|
PublicToken: "public-token",
|
|
RefreshSecret: "refresh-secret",
|
|
AccessSecret: "access-secret",
|
|
AccessExpiry: time.Hour,
|
|
RefreshExpiry: time.Hour,
|
|
Store: st,
|
|
})),
|
|
WithXWorkmateVaultService(vaultService),
|
|
)
|
|
return router
|
|
}
|