Ensure review xworkmate sync contract at startup

This commit is contained in:
Haitao Pan 2026-04-14 17:07:20 +08:00
parent 37bd7ef917
commit d50a2b2486
5 changed files with 345 additions and 0 deletions

View File

@ -270,3 +270,9 @@ jobs:
- name: Validate Deployed Accounts Service - name: Validate Deployed Accounts Service
run: bash ./scripts/github-actions/validate-deploy.sh "${{ needs.build.outputs.service_image_ref }}" https://accounts.svc.plus run: bash ./scripts/github-actions/validate-deploy.sh "${{ needs.build.outputs.service_image_ref }}" https://accounts.svc.plus
- name: Validate Review XWorkmate Sync Contract
env:
REVIEW_ACCOUNT_EMAIL: review@svc.plus
REVIEW_ACCOUNT_PASSWORD: ${{ secrets.REVIEW_ACCOUNT_PASSWORD }}
run: bash ./scripts/github-actions/validate-review-xworkmate-sync.sh https://accounts.svc.plus

1
.gitignore vendored
View File

@ -74,3 +74,4 @@ ansible/vars/*.host.yml
ansible/vars/*.vault.yml ansible/vars/*.vault.yml
init.json init.json
.worktrees/

View File

@ -46,6 +46,9 @@ const (
SandboxEmail = "sandbox@svc.plus" SandboxEmail = "sandbox@svc.plus"
// ReviewEmail is the canonical email for the readonly App Review account. // ReviewEmail is the canonical email for the readonly App Review account.
ReviewEmail = "review@svc.plus" ReviewEmail = "review@svc.plus"
// SharedXWorkmateBridgeServerURL is the managed bridge endpoint exposed to
// tenant-shared xworkmate clients such as the App Review account.
SharedXWorkmateBridgeServerURL = "https://xworkmate-bridge.svc.plus"
) )
const ( const (
@ -61,6 +64,147 @@ var defaultReviewPermissions = []string{
"admin.blacklist.read", "admin.blacklist.read",
} }
type sharedXWorkmateBootstrapConfig struct {
BridgeServerURL string
BridgeAuthToken string
}
func resolveSharedXWorkmateBootstrapConfig() sharedXWorkmateBootstrapConfig {
return sharedXWorkmateBootstrapConfig{
BridgeServerURL: firstNonEmptyString(
os.Getenv("XWORKMATE_BRIDGE_SERVER_URL"),
os.Getenv("BRIDGE_SERVER_URL"),
SharedXWorkmateBridgeServerURL,
),
BridgeAuthToken: firstNonEmptyString(
os.Getenv("BRIDGE_AUTH_TOKEN"),
os.Getenv("INTERNAL_SERVICE_TOKEN"),
),
}
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
return trimmed
}
}
return ""
}
func normalizeBridgeServerOrigin(raw string) string {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return ""
}
return (&url.URL{Scheme: parsed.Scheme, Host: parsed.Host}).String()
}
func managedSharedXWorkmateBridgeLocator() store.XWorkmateSecretLocator {
locator := store.XWorkmateSecretLocator{
ID: strings.Join([]string{"managed", store.SharedXWorkmateTenantID, "", store.XWorkmateProfileScopeTenantShared, store.XWorkmateSecretLocatorTargetBridgeAuthToken}, "|"),
Provider: store.XWorkmateSecretLocatorProviderVault,
SecretPath: fmt.Sprintf("xworkmate/tenants/%s/shared", store.SharedXWorkmateTenantID),
SecretKey: store.XWorkmateSecretLocatorTargetBridgeAuthToken,
Target: store.XWorkmateSecretLocatorTargetBridgeAuthToken,
Required: true,
}
store.NormalizeXWorkmateSecretLocator(&locator)
return locator
}
func upsertXWorkmateSecretLocator(
locators []store.XWorkmateSecretLocator,
locator store.XWorkmateSecretLocator,
) []store.XWorkmateSecretLocator {
next := make([]store.XWorkmateSecretLocator, 0, len(locators)+1)
replaced := false
for _, current := range locators {
store.NormalizeXWorkmateSecretLocator(&current)
if current.Target == locator.Target {
next = append(next, locator)
replaced = true
continue
}
next = append(next, current)
}
if !replaced {
next = append(next, locator)
}
return next
}
func ensureSharedReviewXWorkmateProfile(
ctx context.Context,
st store.Store,
reviewCfg config.ReviewAccount,
bootstrap sharedXWorkmateBootstrapConfig,
writeSecret func(context.Context, store.XWorkmateSecretLocator, string) error,
logger *slog.Logger,
) error {
if !reviewCfg.Enabled {
return nil
}
bridgeServerURL := strings.TrimSpace(bootstrap.BridgeServerURL)
if bridgeServerURL == "" {
return fmt.Errorf("shared xworkmate bridge server url is required")
}
parsedBridgeURL, err := url.Parse(bridgeServerURL)
if err != nil || parsedBridgeURL.Scheme == "" || parsedBridgeURL.Host == "" {
return fmt.Errorf("shared xworkmate bridge server url is invalid: %q", bridgeServerURL)
}
bridgeAuthToken := strings.TrimSpace(bootstrap.BridgeAuthToken)
if bridgeAuthToken == "" {
return fmt.Errorf("shared xworkmate bridge auth token is required")
}
if writeSecret == nil {
return fmt.Errorf("xworkmate vault service is required for shared bridge bootstrap")
}
profile, err := st.GetXWorkmateProfile(
ctx,
store.SharedXWorkmateTenantID,
"",
store.XWorkmateProfileScopeTenantShared,
)
if err != nil && !errors.Is(err, store.ErrXWorkmateProfileNotFound) {
return fmt.Errorf("load shared xworkmate profile: %w", err)
}
if errors.Is(err, store.ErrXWorkmateProfileNotFound) || profile == nil {
profile = &store.XWorkmateProfile{
TenantID: store.SharedXWorkmateTenantID,
Scope: store.XWorkmateProfileScopeTenantShared,
}
}
locator := managedSharedXWorkmateBridgeLocator()
profile.BridgeServerURL = bridgeServerURL
profile.BridgeServerOrigin = normalizeBridgeServerOrigin(bridgeServerURL)
profile.SecretLocators = upsertXWorkmateSecretLocator(
profile.SecretLocators,
locator,
)
if err := st.UpsertXWorkmateProfile(ctx, profile); err != nil {
return fmt.Errorf("upsert shared xworkmate profile: %w", err)
}
if err := writeSecret(ctx, locator, bridgeAuthToken); err != nil {
return fmt.Errorf("persist shared bridge auth token: %w", err)
}
if logger != nil {
logger.Info(
"shared xworkmate bridge contract ensured",
"bridgeServerURL",
bridgeServerURL,
"profileScope",
store.XWorkmateProfileScopeTenantShared,
)
}
return nil
}
func ensureReviewUser(ctx context.Context, st store.Store, cfg config.ReviewAccount, logger *slog.Logger) error { func ensureReviewUser(ctx context.Context, st store.Store, cfg config.ReviewAccount, logger *slog.Logger) error {
email := strings.ToLower(strings.TrimSpace(cfg.Email)) email := strings.ToLower(strings.TrimSpace(cfg.Email))
if email == "" { if email == "" {
@ -916,6 +1060,25 @@ func runServer(ctx context.Context, cfg *config.Config, logger *slog.Logger) err
if err != nil { if err != nil {
return err return err
} }
if err := ensureSharedReviewXWorkmateProfile(
ctx,
st,
cfg.ReviewAccount,
resolveSharedXWorkmateBootstrapConfig(),
func(
ctx context.Context,
locator store.XWorkmateSecretLocator,
value string,
) error {
if xworkmateVaultService == nil {
return errors.New("xworkmate vault service is unavailable")
}
return xworkmateVaultService.WriteSecret(ctx, locator, value)
},
logger,
); err != nil {
logger.Warn("failed to ensure shared review xworkmate profile", "err", err)
}
options := []api.Option{ options := []api.Option{
api.WithStore(st), api.WithStore(st),

116
cmd/accountsvc/main_test.go Normal file
View File

@ -0,0 +1,116 @@
package main
import (
"context"
"log/slog"
"testing"
"account/config"
"account/internal/store"
)
func TestEnsureSharedReviewXWorkmateProfileBootstrapsManagedBridgeContract(t *testing.T) {
t.Parallel()
ctx := context.Background()
st := store.NewMemoryStore()
if err := st.EnsureTenant(ctx, &store.Tenant{
ID: store.SharedXWorkmateTenantID,
Name: store.SharedXWorkmateTenantName,
Edition: store.SharedPublicTenantEdition,
}); err != nil {
t.Fatalf("ensure tenant: %v", err)
}
writes := make([]struct {
locator store.XWorkmateSecretLocator
value string
}, 0, 1)
err := ensureSharedReviewXWorkmateProfile(
ctx,
st,
config.ReviewAccount{Enabled: true},
sharedXWorkmateBootstrapConfig{
BridgeServerURL: SharedXWorkmateBridgeServerURL,
BridgeAuthToken: "bridge-token",
},
func(
ctx context.Context,
locator store.XWorkmateSecretLocator,
value string,
) error {
writes = append(writes, struct {
locator store.XWorkmateSecretLocator
value string
}{locator: locator, value: value})
return nil
},
slog.Default(),
)
if err != nil {
t.Fatalf("ensure shared review xworkmate profile: %v", err)
}
profile, err := st.GetXWorkmateProfile(
ctx,
store.SharedXWorkmateTenantID,
"",
store.XWorkmateProfileScopeTenantShared,
)
if err != nil {
t.Fatalf("load shared profile: %v", err)
}
if got := profile.BridgeServerURL; got != SharedXWorkmateBridgeServerURL {
t.Fatalf("expected bridge server url %q, got %q", SharedXWorkmateBridgeServerURL, got)
}
if got := profile.BridgeServerOrigin; got != SharedXWorkmateBridgeServerURL {
t.Fatalf("expected bridge server origin %q, got %q", SharedXWorkmateBridgeServerURL, got)
}
if len(profile.SecretLocators) != 1 {
t.Fatalf("expected 1 secret locator, got %d", len(profile.SecretLocators))
}
locator := profile.SecretLocators[0]
if locator.Target != store.XWorkmateSecretLocatorTargetBridgeAuthToken {
t.Fatalf("expected bridge auth token locator, got %#v", locator)
}
if locator.SecretPath != "xworkmate/tenants/svc-plus-xworkmate/shared" {
t.Fatalf("expected managed shared secret path, got %#v", locator)
}
if len(writes) != 1 {
t.Fatalf("expected 1 secret write, got %d", len(writes))
}
if writes[0].value != "bridge-token" {
t.Fatalf("expected secret value bridge-token, got %q", writes[0].value)
}
}
func TestEnsureSharedReviewXWorkmateProfileRequiresBridgeContract(t *testing.T) {
t.Parallel()
ctx := context.Background()
st := store.NewMemoryStore()
if err := st.EnsureTenant(ctx, &store.Tenant{
ID: store.SharedXWorkmateTenantID,
Name: store.SharedXWorkmateTenantName,
Edition: store.SharedPublicTenantEdition,
}); err != nil {
t.Fatalf("ensure tenant: %v", err)
}
err := ensureSharedReviewXWorkmateProfile(
ctx,
st,
config.ReviewAccount{Enabled: true},
sharedXWorkmateBootstrapConfig{
BridgeServerURL: SharedXWorkmateBridgeServerURL,
},
func(context.Context, store.XWorkmateSecretLocator, string) error {
return nil
},
nil,
)
if err == nil || err.Error() != "shared xworkmate bridge auth token is required" {
t.Fatalf("expected missing bridge token error, got %v", err)
}
}

View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-https://accounts.svc.plus}"
REVIEW_ACCOUNT_EMAIL="${REVIEW_ACCOUNT_EMAIL:-review@svc.plus}"
REVIEW_ACCOUNT_PASSWORD="${REVIEW_ACCOUNT_PASSWORD:-Review123!}"
login_json="$(
curl \
--silent \
--show-error \
--fail \
--location \
--max-time 20 \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data "{\"identifier\":\"${REVIEW_ACCOUNT_EMAIL}\",\"password\":\"${REVIEW_ACCOUNT_PASSWORD}\"}" \
"${BASE_URL}/api/auth/login"
)"
session_token="$(
LOGIN_JSON="${login_json}" python3 - <<'PY'
import json
import os
payload = json.loads(os.environ["LOGIN_JSON"])
token = (payload.get("token") or payload.get("access_token") or "").strip()
if not token:
raise SystemExit("review account login did not return a session token")
print(token)
PY
)"
sync_json="$(
curl \
--silent \
--show-error \
--fail \
--location \
--max-time 20 \
--header 'Accept: application/json' \
--header "Authorization: Bearer ${session_token}" \
"${BASE_URL}/api/auth/xworkmate/profile/sync"
)"
SYNC_JSON="${sync_json}" python3 - <<'PY'
import json
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()
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")
PY