Ensure review xworkmate sync contract at startup
This commit is contained in:
parent
37bd7ef917
commit
d50a2b2486
6
.github/workflows/pipeline.yml
vendored
6
.github/workflows/pipeline.yml
vendored
@ -270,3 +270,9 @@ jobs:
|
||||
|
||||
- name: Validate Deployed Accounts Service
|
||||
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
1
.gitignore
vendored
@ -74,3 +74,4 @@ ansible/vars/*.host.yml
|
||||
ansible/vars/*.vault.yml
|
||||
|
||||
init.json
|
||||
.worktrees/
|
||||
|
||||
@ -46,6 +46,9 @@ const (
|
||||
SandboxEmail = "sandbox@svc.plus"
|
||||
// ReviewEmail is the canonical email for the readonly App Review account.
|
||||
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 (
|
||||
@ -61,6 +64,147 @@ var defaultReviewPermissions = []string{
|
||||
"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(¤t)
|
||||
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 {
|
||||
email := strings.ToLower(strings.TrimSpace(cfg.Email))
|
||||
if email == "" {
|
||||
@ -916,6 +1060,25 @@ func runServer(ctx context.Context, cfg *config.Config, logger *slog.Logger) err
|
||||
if err != nil {
|
||||
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{
|
||||
api.WithStore(st),
|
||||
|
||||
116
cmd/accountsvc/main_test.go
Normal file
116
cmd/accountsvc/main_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
59
scripts/github-actions/validate-review-xworkmate-sync.sh
Normal file
59
scripts/github-actions/validate-review-xworkmate-sync.sh
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user