From cd16c3db4fa7805c1116472735a4297646859c26 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 9 Oct 2026 16:25:21 +0200 Subject: [PATCH 1/2] feat(insurance): add default-off Archera settings, lazy client and status endpoint Adds internal/archera (ARCHERA_ORG_ID, ARCHERA_PLAN_ID, ARCHERA_API_KEY_SECRET; key resolved lazily through the secret resolver, mutex-guarded so a failed resolve is retried) and GET /api/insurance/status, gated on view:recommendations plus an unrestricted account scope (scoped sessions get 404). Status reports setting presence by name only and makes no outbound call. The comparison endpoint follows in a separate PR. Refs #785 #786 Co-Authored-By: Claude Sonnet 5.5 --- internal/api/handler.go | 2 + internal/api/handler_insurance.go | 46 +++++++ internal/api/handler_insurance_test.go | 133 +++++++++++++++++++ internal/api/router.go | 5 + internal/api/types.go | 1 + internal/archera/provider.go | 145 ++++++++++++++++++++ internal/archera/provider_test.go | 177 +++++++++++++++++++++++++ internal/server/app.go | 8 ++ 8 files changed, 517 insertions(+) create mode 100644 internal/api/handler_insurance.go create mode 100644 internal/api/handler_insurance_test.go create mode 100644 internal/archera/provider.go create mode 100644 internal/archera/provider_test.go diff --git a/internal/api/handler.go b/internal/api/handler.go index aca78168..0800c58e 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -42,6 +42,7 @@ type Handler struct { analyticsCollector AnalyticsCollectorInterface // Optional: snapshot collector analyticsSnapshots AnalyticsSnapshotStoreInterface // Optional: savings-snapshot time-series store signer oidc.Signer // Optional: OIDC issuer signer (backed by cloud KMS) + insurance InsuranceProvider // Optional: Archera comparison (nil = off) issuerURL string // Canonical OIDC issuer URL (falls back to dashboardURL / request domain) awsCfgOnce sync.Once // guards one-time loading of the base AWS config @@ -185,6 +186,7 @@ func NewHandler(cfg HandlerConfig) *Handler { analyticsSnapshots: cfg.AnalyticsSnapshots, signer: cfg.OIDCSigner, issuerURL: cfg.OIDCIssuerURL, + insurance: cfg.Insurance, commitmentOpts: cfg.CommitmentOpts, encryptionKeySource: cfg.EncryptionKeySource, } diff --git a/internal/api/handler_insurance.go b/internal/api/handler_insurance.go new file mode 100644 index 00000000..eaad790d --- /dev/null +++ b/internal/api/handler_insurance.go @@ -0,0 +1,46 @@ +package api + +import ( + "context" + + "github.com/aws/aws-lambda-go/events" + + "github.com/LeanerCloud/cloud-commitments-platform/internal/archera" +) + +// InsuranceProvider is the Archera comparison surface. nil means not wired. +type InsuranceProvider interface { + Status() archera.Status +} + +// getInsuranceStatus reports whether the Archera comparison settings are +// present (names only, no values, no outbound call, no secret read). +// +// Gate: view:recommendations plus an unrestricted account scope. The Archera +// plan is deployment-global and cannot be attributed to one cloud account, so +// a scoped session gets 404 (same non-leaky shape as requireAccountAccess). +func (h *Handler) getInsuranceStatus(ctx context.Context, req *events.LambdaFunctionURLRequest) (*archera.Status, error) { + if err := h.requireInsuranceAccess(ctx, req); err != nil { + return nil, err + } + status := archera.Settings{}.Status() + if h.insurance != nil { + status = h.insurance.Status() + } + return &status, nil +} + +func (h *Handler) requireInsuranceAccess(ctx context.Context, req *events.LambdaFunctionURLRequest) error { + session, err := h.requirePermission(ctx, req, "view", "recommendations") + if err != nil { + return err + } + scope, err := h.getAccountScope(ctx, session) + if err != nil { + return err + } + if !scope.AllowsAll() { + return errNotFound + } + return nil +} diff --git a/internal/api/handler_insurance_test.go b/internal/api/handler_insurance_test.go new file mode 100644 index 00000000..b5f8338f --- /dev/null +++ b/internal/api/handler_insurance_test.go @@ -0,0 +1,133 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/aws/aws-lambda-go/events" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-platform/internal/archera" + "github.com/LeanerCloud/cloud-commitments-platform/internal/secrets" +) + +const ( + insTestKey = "synthetic-archera-key-0123456789" + insTestOrg = "11111111-1111-4111-8111-111111111111" + insTestPlan = "44444444-4444-4444-8444-444444444444" + insTestRef = "SYNTHETIC_ARCHERA_KEY_REF" +) + +type panicTransport struct{ t *testing.T } + +func (p panicTransport) RoundTrip(*http.Request) (*http.Response, error) { + p.t.Fatal("status must make no outbound request") + return nil, nil +} + +func insuranceProvider(t *testing.T, s archera.Settings) *archera.Provider { + return archera.NewProvider(s, nil, &http.Client{Transport: panicTransport{t}}) +} + +func sessionPrincipalCtx(userID string) context.Context { + return contextWithPrincipal(context.Background(), &Principal{ + Kind: PrincipalSession, Session: &Session{UserID: userID}, UserID: userID, + }) +} + +func TestInsuranceStatus_AdminAPIKey_ReportsPresenceOnly(t *testing.T) { + full := archera.Settings{OrgID: insTestOrg, PlanID: insTestPlan, KeySecretRef: insTestRef} + for name, tc := range map[string]struct { + s archera.Settings + ok bool + missing []string + }{ + "off": {archera.Settings{}, false, []string{archera.EnvKeySecret, archera.EnvOrgID, archera.EnvPlanID}}, + "complete": {full, true, []string{}}, + } { + t.Run(name, func(t *testing.T) { + h := &Handler{apiKey: "admin-key", insurance: insuranceProvider(t, tc.s)} + req := &events.LambdaFunctionURLRequest{Headers: map[string]string{"x-api-key": "admin-key"}} + st, err := h.getInsuranceStatus(context.Background(), req) + require.NoError(t, err) + assert.Equal(t, tc.ok, st.Configured) + assert.Equal(t, tc.missing, st.Missing) + assert.NotContains(t, fmt.Sprintf("%+v", st), insTestOrg, "status must never carry IDs") + }) + } +} + +func TestInsuranceStatus_UnwiredIsOff(t *testing.T) { + h := &Handler{apiKey: "admin-key"} + req := &events.LambdaFunctionURLRequest{Headers: map[string]string{"x-api-key": "admin-key"}} + st, err := h.getInsuranceStatus(context.Background(), req) + require.NoError(t, err) + assert.False(t, st.Configured) +} + +func TestInsuranceStatus_DeniedUserGets403(t *testing.T) { + m := new(MockAuthService) + m.On("HasPermissionAPI", mock.Anything, "u1", "view", "recommendations").Return(false, nil) + h := &Handler{auth: m, insurance: insuranceProvider(t, archera.Settings{})} + _, err := h.getInsuranceStatus(sessionPrincipalCtx("u1"), &events.LambdaFunctionURLRequest{}) + ce, ok := IsClientError(err) + require.True(t, ok, "got %v", err) + assert.Equal(t, 403, ce.code) +} + +func TestInsuranceStatus_ScopedSessionGets404(t *testing.T) { + m := new(MockAuthService) + m.On("HasPermissionAPI", mock.Anything, "u1", "view", "recommendations").Return(true, nil) + m.On("GetAllowedAccountsAPI", mock.Anything, "u1").Return([]string{"acct-1"}, nil) + h := &Handler{auth: m, insurance: insuranceProvider(t, archera.Settings{})} + _, err := h.getInsuranceStatus(sessionPrincipalCtx("u1"), &events.LambdaFunctionURLRequest{}) + assert.ErrorIs(t, err, errNotFound) +} + +func TestInsuranceStatus_UnrestrictedSessionAllowed(t *testing.T) { + m := new(MockAuthService) + m.On("HasPermissionAPI", mock.Anything, "u1", "view", "recommendations").Return(true, nil) + m.On("GetAllowedAccountsAPI", mock.Anything, "u1").Return([]string(nil), nil) + h := &Handler{auth: m, insurance: insuranceProvider(t, archera.Settings{})} + st, err := h.getInsuranceStatus(sessionPrincipalCtx("u1"), &events.LambdaFunctionURLRequest{}) + require.NoError(t, err) + assert.False(t, st.Configured) +} + +func TestInsuranceStatus_ScopedUserAPIKeyGets404(t *testing.T) { + m := new(MockAuthService) + m.On("HasAPIKeyPermissionAPI", mock.Anything, "user-key", "view", "recommendations").Return("u2", "key-1", true, nil) + m.On("GetAllowedAccountsAPI", mock.Anything, "u2").Return([]string{"acct-1"}, nil) + h := &Handler{auth: m, insurance: insuranceProvider(t, archera.Settings{})} + ctx := contextWithPrincipal(context.Background(), &Principal{Kind: PrincipalUserAPIKey, UserID: "u2", APIKeyID: "key-1"}) + req := &events.LambdaFunctionURLRequest{Headers: map[string]string{"x-api-key": "user-key"}} + _, err := h.getInsuranceStatus(ctx, req) + assert.ErrorIs(t, err, errNotFound) +} + +func TestInsuranceStatus_RouteIsAuthenticated(t *testing.T) { + h := &Handler{auth: new(MockAuthService), insurance: insuranceProvider(t, archera.Settings{})} + _, err := NewRouter(h).Route(context.Background(), "GET", "/api/insurance/status", &events.LambdaFunctionURLRequest{}) + require.Error(t, err, "an unauthenticated request must not reach the handler") +} + +// The Handler holds a lazily built *insurance.Client behind the provider; no +// fmt verb applied to the Handler may print the key. +func TestInsurance_HandlerFormattingNeverPrintsKey(t *testing.T) { + t.Setenv(insTestRef, insTestKey) + p := archera.NewProvider(archera.Settings{OrgID: insTestOrg, PlanID: insTestPlan, KeySecretRef: insTestRef}, secrets.NewEnvResolver(), nil) + h := &Handler{insurance: p} + check := func(label string) { + for _, verb := range []string{"%v", "%+v", "%#v", "%s"} { + assert.NotContains(t, fmt.Sprintf(verb, h), insTestKey, label+" "+verb) + } + } + check("before the client is built") + _, err := p.Client(context.Background()) + require.NoError(t, err) + check("after the client is built") +} diff --git a/internal/api/router.go b/internal/api/router.go index 6909f457..b64025c1 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -123,6 +123,7 @@ func (r *Router) registerRoutes() { // recommendation feed (handlers still scope rows by account // permission grants). {ExactPath: "/api/recommendations", Method: "GET", Handler: r.getRecommendationsHandler, Auth: AuthUser}, + {ExactPath: "/api/insurance/status", Method: "GET", Handler: r.getInsuranceStatusHandler, Auth: AuthUser}, {ExactPath: "/api/recommendations/freshness", Method: "GET", Handler: r.getRecommendationsFreshnessHandler, Auth: AuthUser}, // AuthUser: any signed-in user can trigger refresh; the handler // then enforces requirePermission(view, recommendations) so @@ -509,6 +510,10 @@ func (r *Router) refreshRecommendationsHandler(ctx context.Context, req *events. return r.h.postRefreshRecommendations(ctx, req) } +func (r *Router) getInsuranceStatusHandler(ctx context.Context, req *events.LambdaFunctionURLRequest, params map[string]string) (any, error) { + return r.h.getInsuranceStatus(ctx, req) +} + func (r *Router) getRecommendationsFreshnessHandler(ctx context.Context, req *events.LambdaFunctionURLRequest, params map[string]string) (any, error) { return r.h.getRecommendationsFreshness(ctx, req) } diff --git a/internal/api/types.go b/internal/api/types.go index d851bbb4..5f84701e 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -41,6 +41,7 @@ type HandlerConfig struct { AuthService AuthServiceInterface CommitmentOpts CommitmentOptsInterface OIDCSigner oidc.Signer + Insurance InsuranceProvider AnalyticsSnapshots AnalyticsSnapshotStoreInterface CredentialStore credentials.CredentialStore EmailNotifier email.SenderInterface diff --git a/internal/archera/provider.go b/internal/archera/provider.go new file mode 100644 index 00000000..f35e3922 --- /dev/null +++ b/internal/archera/provider.go @@ -0,0 +1,145 @@ +// Package archera wires the explicit Archera comparison (pkg/insurance) into +// the server. The surface is off unless all three settings are present, and +// the API key is resolved lazily on the first explicit request. +package archera + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "sync" + "sync/atomic" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/insurance" + "github.com/LeanerCloud/cloud-commitments-go/pkg/logging" + + "github.com/LeanerCloud/cloud-commitments-platform/internal/secrets" +) + +// Setting names, shared with the CLI and MCP consumers. +const ( + EnvOrgID = "ARCHERA_ORG_ID" + EnvPlanID = "ARCHERA_PLAN_ID" + EnvKeySecret = "ARCHERA_API_KEY_SECRET" +) + +// ErrNotConfigured is returned (wrapped, naming the missing settings) when an +// explicit request arrives and one or more settings are absent. +var ErrNotConfigured = errors.New("archera comparison is not configured") + +// ErrKeyUnavailable is returned when the API key secret cannot be resolved or +// yields an unusable client. The message is fixed so no vendor or secret text +// can reach a response. +var ErrKeyUnavailable = errors.New("archera API key could not be resolved from " + EnvKeySecret) + +// Settings are the non-secret Archera settings. KeySecretRef is a reference to +// the secret (name, ARN or URI), not the key itself. +type Settings struct { + OrgID string + PlanID string + KeySecretRef string +} + +// SettingsFromEnv reads the settings from the process environment. +func SettingsFromEnv() Settings { + return Settings{ + OrgID: os.Getenv(EnvOrgID), + PlanID: os.Getenv(EnvPlanID), + KeySecretRef: os.Getenv(EnvKeySecret), + } +} + +// Missing lists the names (never values) of absent settings, in a fixed order. +func (s Settings) Missing() []string { + missing := []string{} + if s.KeySecretRef == "" { + missing = append(missing, EnvKeySecret) + } + if s.OrgID == "" { + missing = append(missing, EnvOrgID) + } + if s.PlanID == "" { + missing = append(missing, EnvPlanID) + } + return missing +} + +// Status reports whether the three settings are present. It resolves nothing +// and never carries a value. +type Status struct { + Missing []string `json:"missing"` + Configured bool `json:"configured"` +} + +// Status returns the presence-only status for the settings. +func (s Settings) Status() Status { + m := s.Missing() + return Status{Configured: len(m) == 0, Missing: m} +} + +// Provider builds the insurance client on first use. It holds the secret +// resolver and a *insurance.Client only, never the raw key or a Config value. +type Provider struct { + resolver secrets.Resolver + hc *http.Client // nil in production (SSRF-hardened default); tests inject + // client is an atomic pointer so fmt prints an address, never the Client's + // fields: fmt dereferences a plain *Client held in an unexported field for + // verbs such as %s, which would print the key. + client atomic.Pointer[insurance.Client] + settings Settings + mu sync.Mutex +} + +// NewProvider returns a Provider. hc must be nil in production. +func NewProvider(settings Settings, resolver secrets.Resolver, hc *http.Client) *Provider { + return &Provider{settings: settings, resolver: resolver, hc: hc} +} + +// Status reports setting presence without any outbound call or secret read. +func (p *Provider) Status() Status { + if p == nil { + return Settings{}.Status() + } + return p.settings.Status() +} + +// Format keeps the provider's contents out of every fmt verb. +func (p *Provider) Format(s fmt.State, _ rune) { + _, _ = fmt.Fprint(s, "archera.Provider{}") +} + +// PlanID is the configured Archera plan ID. +func (p *Provider) PlanID() string { return p.settings.PlanID } + +// Client returns the lazily built client. A failure is not cached: the next +// call retries (a mutex, not sync.Once, guards the build). +func (p *Provider) Client(ctx context.Context) (insurance.QuoteClient, error) { + if p == nil { + return nil, fmt.Errorf("%w: missing %v", ErrNotConfigured, Settings{}.Missing()) + } + if missing := p.settings.Missing(); len(missing) > 0 { + return nil, fmt.Errorf("%w: missing %v", ErrNotConfigured, missing) + } + p.mu.Lock() + defer p.mu.Unlock() + if c := p.client.Load(); c != nil { + return c, nil + } + if p.resolver == nil { + return nil, ErrKeyUnavailable + } + key, err := p.resolver.GetSecret(ctx, p.settings.KeySecretRef) + if err != nil { + logging.Warnf("archera: resolving %s failed: %v", EnvKeySecret, err) + return nil, ErrKeyUnavailable + } + c, err := insurance.NewClient(insurance.Config{APIKey: key, OrgID: p.settings.OrgID}, p.hc) + if err != nil { + logging.Warnf("archera: building client failed: %v", err) + return nil, ErrKeyUnavailable + } + p.client.Store(c) + return c, nil +} diff --git a/internal/archera/provider_test.go b/internal/archera/provider_test.go new file mode 100644 index 00000000..3fa3fa2d --- /dev/null +++ b/internal/archera/provider_test.go @@ -0,0 +1,177 @@ +package archera + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "sync/atomic" + "testing" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/insurance" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-platform/internal/secrets" +) + +// All values are synthetic. +const ( + testKey = "synthetic-archera-key-0123456789" + testOrg = "11111111-1111-4111-8111-111111111111" + testPlan = "44444444-4444-4444-8444-444444444444" + testLineID = "22222222-2222-4222-8222-222222222222" + testSecret = "SYNTHETIC_ARCHERA_KEY_REF" + financials = `{"commitment_cost":{"total":110,"breakdown":{"cloud_provider_cost":{"total":100},"archera_premium":10}},"commitment_savings":{"net":-5.5,"gross":4.5},"covered_ondemand_cost":104.5}` + offerEntry = `{"is_current":true,"offer_id":"11111111-1111-4111-8111-111111111111","offer_org_id":"public","offer":{"provider":"aws","type":"aws/AmazonEC2","region":"us-east-1","guaranteed_display_name":null},"lease_menu_item_id":null,"selected_amount":3,"commitment_type":"aws/AmazonEC2","contract_term":null,"payment_option":null,"discount_rate":0.3,"breakeven_days":null,"commitment_upfront_cost":1200,"commitment_financials_monthly_rate":` + financials + `,"delta_vs_current":{"monthly_net_savings":0,"upfront_cost":0,"discount_rate":0,"breakeven_days":null}}` + comparison = `{"current_totals":{"commitment_financials_monthly_rate":` + financials + `,"commitment_upfront_cost":1200},"hypothetical_totals":[],"data":[{"line_item_id":"` + testLineID + `","current":` + offerEntry + `,"candidates":[]}]}` +) + +// hostAssertingTransport fails the test unless the request targets the fixed +// Archera origin, then serves a synthetic response in place of the network. +type hostAssertingTransport struct { + t *testing.T + calls atomic.Int32 + body string + code int + gotKey, gotPath string +} + +func (rt *hostAssertingTransport) RoundTrip(r *http.Request) (*http.Response, error) { + rt.calls.Add(1) + assert.Equal(rt.t, "api.archera.ai", r.URL.Host) + assert.Equal(rt.t, "https", r.URL.Scheme) + rt.gotKey, rt.gotPath = r.Header.Get("x-api-key"), r.URL.Path + code := rt.code + if code == 0 { + code = 200 + } + return &http.Response{StatusCode: code, Header: http.Header{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(rt.body)), Request: r}, nil +} + +func fullSettings() Settings { + return Settings{OrgID: testOrg, PlanID: testPlan, KeySecretRef: testSecret} +} + +func envResolver(t *testing.T) secrets.Resolver { + t.Helper() + t.Setenv(testSecret, testKey) + return secrets.NewEnvResolver() +} + +func TestStatus_NamesOnly(t *testing.T) { + cases := []struct { + name string + s Settings + missing []string + ok bool + }{ + {"off", Settings{}, []string{EnvKeySecret, EnvOrgID, EnvPlanID}, false}, + {"no key", Settings{OrgID: testOrg, PlanID: testPlan}, []string{EnvKeySecret}, false}, + {"no org", Settings{PlanID: testPlan, KeySecretRef: testSecret}, []string{EnvOrgID}, false}, + {"no plan", Settings{OrgID: testOrg, KeySecretRef: testSecret}, []string{EnvPlanID}, false}, + {"complete", fullSettings(), []string{}, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rt := &hostAssertingTransport{t: t} + st := NewProvider(tc.s, nil, &http.Client{Transport: rt}).Status() + assert.Equal(t, tc.ok, st.Configured) + assert.Equal(t, tc.missing, st.Missing) + assert.Zero(t, rt.calls.Load(), "status must make no outbound call") + assert.NotContains(t, fmt.Sprintf("%+v", st), testOrg) + }) + } +} + +func TestStatus_NilProviderIsOff(t *testing.T) { + var p *Provider + assert.False(t, p.Status().Configured) + _, err := p.Client(context.Background()) + assert.ErrorIs(t, err, ErrNotConfigured) +} + +func TestClient_IncompleteConfigNamesMissingSettingAndSendsNothing(t *testing.T) { + rt := &hostAssertingTransport{t: t} + p := NewProvider(Settings{OrgID: testOrg, KeySecretRef: testSecret}, envResolver(t), &http.Client{Transport: rt}) + _, err := p.Client(context.Background()) + require.ErrorIs(t, err, ErrNotConfigured) + assert.Contains(t, err.Error(), EnvPlanID) + assert.NotContains(t, err.Error(), testKey) + assert.Zero(t, rt.calls.Load()) +} + +type flakyResolver struct { + calls atomic.Int32 + fails int32 +} + +func (f *flakyResolver) GetSecret(context.Context, string) (string, error) { + if f.calls.Add(1) <= f.fails { + // The error text carries the key to prove it is not propagated. + return "", errors.New("vault down while reading " + testKey) + } + return testKey, nil +} +func (*flakyResolver) GetSecretJSON(context.Context, string) (map[string]any, error) { return nil, nil } +func (*flakyResolver) PutSecret(context.Context, string, string) error { return nil } +func (*flakyResolver) ListSecrets(context.Context, string) ([]string, error) { return nil, nil } +func (*flakyResolver) Close() error { return nil } + +func TestClient_ResolveFailureIsNotCachedAndIsSanitized(t *testing.T) { + res := &flakyResolver{fails: 1} + p := NewProvider(fullSettings(), res, &http.Client{Transport: &hostAssertingTransport{t: t, body: comparison}}) + + _, err := p.Client(context.Background()) + require.ErrorIs(t, err, ErrKeyUnavailable) + assert.Contains(t, err.Error(), EnvKeySecret) + assert.NotContains(t, err.Error(), testKey) + + c, err := p.Client(context.Background()) + require.NoError(t, err, "a failed build must be retried on the next call, not cached") + require.NotNil(t, c) + assert.EqualValues(t, 2, res.calls.Load()) + + again, err := p.Client(context.Background()) + require.NoError(t, err) + assert.Same(t, c, again) + assert.EqualValues(t, 2, res.calls.Load(), "a built client is reused without resolving again") +} + +func TestClient_InvalidOrgIDIsSanitized(t *testing.T) { + s := fullSettings() + s.OrgID = "not-a-uuid" + _, err := NewProvider(s, envResolver(t), nil).Client(context.Background()) + require.ErrorIs(t, err, ErrKeyUnavailable) + assert.NotContains(t, err.Error(), testKey) +} + +// Production composition: env -> secret resolver -> lazy NewClient (hc injected +// only to replace the network) -> documented GET -> decode. +func TestComposition_EnvToDecodedComparison_AndRedaction(t *testing.T) { + rt := &hostAssertingTransport{t: t, body: comparison} + p := NewProvider(fullSettings(), envResolver(t), &http.Client{Transport: rt}) + + assertNoKey := func(label string) { + for _, verb := range []string{"%v", "%+v", "%#v", "%s"} { + out := fmt.Sprintf(verb, p) + fmt.Sprintf(verb, struct{ P *Provider }{p}) + fmt.Sprintf(verb, fullSettings()) + assert.NotContains(t, out, testKey, "%s %s", label, verb) + } + } + assertNoKey("before first comparison") + + c, err := p.Client(context.Background()) + require.NoError(t, err) + got, err := c.Comparison(context.Background(), insurance.ComparisonRequest{PlanID: p.PlanID()}) + require.NoError(t, err) + assert.Equal(t, 1, int(rt.calls.Load())) + assert.Equal(t, "/v1/org/"+testOrg+"/commitment-plans/"+testPlan+"/comparison", rt.gotPath) + assert.Equal(t, testKey, rt.gotKey) + assert.Equal(t, testPlan, got.PlanID) + + assertNoKey("after a successful comparison (client now built)") + assert.NotContains(t, fmt.Sprintf("%+v %v %#v", c, c, c), testKey) +} diff --git a/internal/server/app.go b/internal/server/app.go index 130bc387..cff073ac 100644 --- a/internal/server/app.go +++ b/internal/server/app.go @@ -19,6 +19,7 @@ import ( awsladder "github.com/LeanerCloud/cloud-commitments-go/providers/aws/ladder" "github.com/LeanerCloud/cloud-commitments-platform/internal/analytics" "github.com/LeanerCloud/cloud-commitments-platform/internal/api" + "github.com/LeanerCloud/cloud-commitments-platform/internal/archera" "github.com/LeanerCloud/cloud-commitments-platform/internal/auth" "github.com/LeanerCloud/cloud-commitments-platform/internal/commitmentopts" "github.com/LeanerCloud/cloud-commitments-platform/internal/config" @@ -76,6 +77,7 @@ type Application struct { // Lazy initialization fields for PostgreSQL (Lambda ENI readiness) dbConfig *database.Config secretResolver secrets.Resolver + archera *archera.Provider dbMu sync.Mutex dbConnected bool dbErr error @@ -504,6 +506,9 @@ func NewApplicationFromDeps(ctx context.Context, cfg ApplicationConfig, deps Ext rateLimiter := api.RateLimiterInterface(api.NewInMemoryRateLimiter()) log.Println("Initialized temporary in-memory rate limiter until database connection") + // Archera comparison: default off, key resolved lazily on first explicit request. + archeraProvider := archera.NewProvider(archera.SettingsFromEnv(), deps.SecretResolver, nil) + // Initialize API handler apiHandler := api.NewHandler(api.HandlerConfig{ ConfigStore: deps.ConfigStore, @@ -519,6 +524,7 @@ func NewApplicationFromDeps(ctx context.Context, cfg ApplicationConfig, deps Ext DashboardURL: cfg.DashboardURL, OIDCSigner: signer, OIDCIssuerURL: resolveOIDCIssuerURL(cfg), + Insurance: archeraProvider, }) log.Printf("CUDly Server initialization complete") @@ -536,6 +542,7 @@ func NewApplicationFromDeps(ctx context.Context, cfg ApplicationConfig, deps Ext staticDir: staticDirFromEnv(), dbConfig: deps.DBConfig, secretResolver: deps.SecretResolver, + archera: archeraProvider, appConfig: cfg, signer: signer, scheduledAuth: scheduledAuth, @@ -882,6 +889,7 @@ func (app *Application) reinitializeAfterConnect(ctx context.Context, dbConn *da OIDCIssuerURL: resolveOIDCIssuerURL(app.appConfig), CommitmentOpts: commitmentOpts, EncryptionKeySource: app.encKeySource, + Insurance: app.archera, }) if app.API == nil { return fmt.Errorf("failed to create API handler") From 0c2cb2e1cbfa166011654971060175f2c1308965 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 9 Oct 2026 19:39:58 +0200 Subject: [PATCH 2/2] fix(insurance): annotate the setting-name constant for gosec G101 Refs #785 #786 Co-Authored-By: Claude Sonnet 5.5 --- internal/archera/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/archera/provider.go b/internal/archera/provider.go index f35e3922..b2c9d3ee 100644 --- a/internal/archera/provider.go +++ b/internal/archera/provider.go @@ -22,7 +22,7 @@ import ( const ( EnvOrgID = "ARCHERA_ORG_ID" EnvPlanID = "ARCHERA_PLAN_ID" - EnvKeySecret = "ARCHERA_API_KEY_SECRET" + EnvKeySecret = "ARCHERA_API_KEY_SECRET" // #nosec G101 -- setting name, not a credential value ) // ErrNotConfigured is returned (wrapped, naming the missing settings) when an