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
|
- 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
1
.gitignore
vendored
@ -74,3 +74,4 @@ ansible/vars/*.host.yml
|
|||||||
ansible/vars/*.vault.yml
|
ansible/vars/*.vault.yml
|
||||||
|
|
||||||
init.json
|
init.json
|
||||||
|
.worktrees/
|
||||||
|
|||||||
@ -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(¤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 {
|
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
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