Route review account through review bridge token

This commit is contained in:
Haitao Pan 2026-05-30 10:34:51 +08:00
parent a38345c69c
commit 7292fca3b8
6 changed files with 194 additions and 0 deletions

View File

@ -237,6 +237,9 @@ jobs:
env:
ACCOUNTS_IMAGE_REF: ${{ steps.deploy_image.outputs.image_ref }}
ACCOUNTS_PULL_IMAGE: "true"
BRIDGE_AUTH_TOKEN: ${{ secrets.BRIDGE_AUTH_TOKEN }}
BRIDGE_REVIEW_AUTH_TOKEN: ${{ secrets.BRIDGE_REVIEW_AUTH_TOKEN }}
BRIDGE_SERVER_URL: https://xworkmate-bridge.svc.plus
run: |
set -euo pipefail
@ -275,4 +278,6 @@ jobs:
env:
REVIEW_ACCOUNT_EMAIL: review@svc.plus
REVIEW_ACCOUNT_PASSWORD: ${{ secrets.REVIEW_ACCOUNT_PASSWORD }}
BRIDGE_AUTH_TOKEN: ${{ secrets.BRIDGE_AUTH_TOKEN }}
BRIDGE_REVIEW_AUTH_TOKEN: ${{ secrets.BRIDGE_REVIEW_AUTH_TOKEN }}
run: bash ./scripts/github-actions/validate-review-xworkmate-sync.sh https://accounts.svc.plus

View File

@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/gin-gonic/gin"
@ -668,6 +669,14 @@ func (h *handler) getXWorkmateProfileSync(c *gin.Context) {
respondError(c, http.StatusConflict, "bridge_auth_token_unavailable", "bridge auth token is unavailable")
return
}
if isReviewXWorkmateAccount(user) {
reviewToken := strings.TrimSpace(os.Getenv("BRIDGE_REVIEW_AUTH_TOKEN"))
if reviewToken == "" {
respondError(c, http.StatusConflict, "bridge_review_auth_token_unavailable", "bridge review auth token is unavailable")
return
}
bridgeAuthToken = reviewToken
}
c.JSON(http.StatusOK, gin.H{
"BRIDGE_SERVER_URL": bridgeServerURL,
@ -675,6 +684,13 @@ func (h *handler) getXWorkmateProfileSync(c *gin.Context) {
})
}
func isReviewXWorkmateAccount(user *store.User) bool {
if user == nil {
return false
}
return strings.EqualFold(strings.TrimSpace(user.Email), "review@svc.plus")
}
func (h *handler) updateXWorkmateProfile(c *gin.Context) {
user, ok := h.currentAuthenticatedUser(c)
if !ok {

View File

@ -221,6 +221,147 @@ func TestGetXWorkmateProfileSyncReturnsManagedBridgeCredentials(t *testing.T) {
}
}
func TestGetXWorkmateProfileSyncReturnsReviewBridgeTokenForReviewAccount(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("BRIDGE_REVIEW_AUTH_TOKEN", "review-token-value")
vaultService := newMemoryXWorkmateVaultService()
router, _, token := newXWorkmateTestHarnessWithVault(t, &store.User{
Name: "Apple Review",
Email: "review@svc.plus",
EmailVerified: true,
Role: store.RoleAdmin,
Level: store.LevelAdmin,
Active: true,
}, vaultService)
profileBody, err := json.Marshal(map[string]any{
"profile": map[string]any{
"BRIDGE_SERVER_URL": "https://xworkmate-bridge.svc.plus",
"secretLocators": []map[string]any{
{
"id": "locator-openclaw",
"provider": "vault",
"secretPath": "kv/openclaw",
"secretKey": "token",
"target": store.XWorkmateSecretLocatorTargetBridgeAuthToken,
"required": true,
},
},
},
})
if err != nil {
t.Fatalf("marshal profile: %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())
}
if err := vaultService.WriteSecret(context.Background(), store.XWorkmateSecretLocator{
Provider: "vault",
SecretPath: "kv/openclaw",
SecretKey: "token",
Target: store.XWorkmateSecretLocatorTargetBridgeAuthToken,
}, "production-token-value"); err != nil {
t.Fatalf("write secret: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile/sync", 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 profile sync success, got %d: %s", rec.Code, rec.Body.String())
}
var payload struct {
BridgeServerURL string `json:"BRIDGE_SERVER_URL"`
BridgeAuthToken string `json:"BRIDGE_AUTH_TOKEN"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode profile sync response: %v", err)
}
if payload.BridgeServerURL != "https://xworkmate-bridge.svc.plus" {
t.Fatalf("expected bridge server url, got %#v", payload)
}
if payload.BridgeAuthToken != "review-token-value" {
t.Fatalf("expected review bridge auth token, got %#v", payload)
}
if payload.BridgeAuthToken == "production-token-value" {
t.Fatalf("review account must not receive production bridge token")
}
}
func TestGetXWorkmateProfileSyncRejectsReviewAccountWithoutReviewBridgeToken(t *testing.T) {
gin.SetMode(gin.TestMode)
vaultService := newMemoryXWorkmateVaultService()
router, _, token := newXWorkmateTestHarnessWithVault(t, &store.User{
Name: "Apple Review",
Email: "review@svc.plus",
EmailVerified: true,
Role: store.RoleAdmin,
Level: store.LevelAdmin,
Active: true,
}, vaultService)
profileBody, err := json.Marshal(map[string]any{
"profile": map[string]any{
"BRIDGE_SERVER_URL": "https://xworkmate-bridge.svc.plus",
"secretLocators": []map[string]any{
{
"id": "locator-openclaw",
"provider": "vault",
"secretPath": "kv/openclaw",
"secretKey": "token",
"target": store.XWorkmateSecretLocatorTargetBridgeAuthToken,
"required": true,
},
},
},
})
if err != nil {
t.Fatalf("marshal profile: %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())
}
if err := vaultService.WriteSecret(context.Background(), store.XWorkmateSecretLocator{
Provider: "vault",
SecretPath: "kv/openclaw",
SecretKey: "token",
Target: store.XWorkmateSecretLocatorTargetBridgeAuthToken,
}, "production-token-value"); err != nil {
t.Fatalf("write secret: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile/sync", 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.StatusConflict {
t.Fatalf("expected profile sync conflict, got %d: %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "bridge_review_auth_token_unavailable") {
t.Fatalf("expected review token error, got %s", rec.Body.String())
}
}
func TestGetXWorkmateProfileSyncConflictsWhenManagedBridgeContractMissing(t *testing.T) {
gin.SetMode(gin.TestMode)

View File

@ -56,6 +56,18 @@ spec:
secretKeyRef:
name: internal-service-token
key: latest
- name: BRIDGE_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: bridge-auth-token
key: latest
- name: BRIDGE_REVIEW_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: bridge-review-auth-token
key: latest
- name: BRIDGE_SERVER_URL
value: "https://xworkmate-bridge.svc.plus"
# --- SMTP Configuration ---
- name: SMTP_HOST
value: "smtp.qq.com"

View File

@ -56,6 +56,18 @@ spec:
secretKeyRef:
name: internal-service-token
key: latest
- name: BRIDGE_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: bridge-auth-token
key: latest
- name: BRIDGE_REVIEW_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: bridge-review-auth-token
key: latest
- name: BRIDGE_SERVER_URL
value: "https://xworkmate-bridge.svc.plus"
# --- SMTP Configuration ---
- name: SMTP_HOST
value: "smtp.qq.com"

View File

@ -50,10 +50,18 @@ import os
payload = json.loads(os.environ["SYNC_JSON"])
bridge_server_url = (payload.get("BRIDGE_SERVER_URL") or "").strip()
bridge_auth_token = (payload.get("BRIDGE_AUTH_TOKEN") or "").strip()
expected_review_token = os.environ.get("BRIDGE_REVIEW_AUTH_TOKEN", "").strip()
production_token = os.environ.get("BRIDGE_AUTH_TOKEN", "").strip()
if not bridge_server_url:
raise SystemExit("review xworkmate sync did not return BRIDGE_SERVER_URL")
if not bridge_auth_token:
raise SystemExit("review xworkmate sync did not return BRIDGE_AUTH_TOKEN")
if expected_review_token and bridge_auth_token != expected_review_token:
raise SystemExit("review xworkmate sync did not return the review bridge token")
if production_token and bridge_auth_token == production_token:
raise SystemExit("review xworkmate sync returned the production bridge token")
PY