Route review account through review bridge token
This commit is contained in:
parent
a38345c69c
commit
7292fca3b8
5
.github/workflows/pipeline.yml
vendored
5
.github/workflows/pipeline.yml
vendored
@ -237,6 +237,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ACCOUNTS_IMAGE_REF: ${{ steps.deploy_image.outputs.image_ref }}
|
ACCOUNTS_IMAGE_REF: ${{ steps.deploy_image.outputs.image_ref }}
|
||||||
ACCOUNTS_PULL_IMAGE: "true"
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@ -275,4 +278,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
REVIEW_ACCOUNT_EMAIL: review@svc.plus
|
REVIEW_ACCOUNT_EMAIL: review@svc.plus
|
||||||
REVIEW_ACCOUNT_PASSWORD: ${{ secrets.REVIEW_ACCOUNT_PASSWORD }}
|
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
|
run: bash ./scripts/github-actions/validate-review-xworkmate-sync.sh https://accounts.svc.plus
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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")
|
respondError(c, http.StatusConflict, "bridge_auth_token_unavailable", "bridge auth token is unavailable")
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"BRIDGE_SERVER_URL": bridgeServerURL,
|
"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) {
|
func (h *handler) updateXWorkmateProfile(c *gin.Context) {
|
||||||
user, ok := h.currentAuthenticatedUser(c)
|
user, ok := h.currentAuthenticatedUser(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@ -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) {
|
func TestGetXWorkmateProfileSyncConflictsWhenManagedBridgeContractMissing(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,18 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: internal-service-token
|
name: internal-service-token
|
||||||
key: latest
|
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 ---
|
# --- SMTP Configuration ---
|
||||||
- name: SMTP_HOST
|
- name: SMTP_HOST
|
||||||
value: "smtp.qq.com"
|
value: "smtp.qq.com"
|
||||||
|
|||||||
@ -56,6 +56,18 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: internal-service-token
|
name: internal-service-token
|
||||||
key: latest
|
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 ---
|
# --- SMTP Configuration ---
|
||||||
- name: SMTP_HOST
|
- name: SMTP_HOST
|
||||||
value: "smtp.qq.com"
|
value: "smtp.qq.com"
|
||||||
|
|||||||
@ -50,10 +50,18 @@ import os
|
|||||||
payload = json.loads(os.environ["SYNC_JSON"])
|
payload = json.loads(os.environ["SYNC_JSON"])
|
||||||
bridge_server_url = (payload.get("BRIDGE_SERVER_URL") or "").strip()
|
bridge_server_url = (payload.get("BRIDGE_SERVER_URL") or "").strip()
|
||||||
bridge_auth_token = (payload.get("BRIDGE_AUTH_TOKEN") 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:
|
if not bridge_server_url:
|
||||||
raise SystemExit("review xworkmate sync did not return BRIDGE_SERVER_URL")
|
raise SystemExit("review xworkmate sync did not return BRIDGE_SERVER_URL")
|
||||||
|
|
||||||
if not bridge_auth_token:
|
if not bridge_auth_token:
|
||||||
raise SystemExit("review xworkmate sync did not return 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
|
PY
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user