Skip to content

Commit 562629c

Browse files
committed
Argo: keep the cross-cluster guard intact on failed-connect rollback + free the port-forward on auth failure
Two defects in the Argo credential model surfaced by review: - A failed connect attempt with a fresh token stamped tokenContext to the current cluster before the probe, and the rollback re-pointed the manager with a plain SetConfig that leaves tokenContext untouched — so the restored (older) auto-discovery token was left marked valid for the wrong cluster in memory, defeating the cross-cluster guard until restart. Rollback now restores the captured prior binding via RestoreConfig on both failure paths (probe + persist). - Out-of-cluster discovery installs a port-forward to a reachable argocd-server before verifying the token; a token failure returned without tearing it down, leaving a live localhost tunnel while the manager reports disconnected. Probe now drops the forward on auth failure (unless a config change already owns cleanup).
1 parent 4e9240a commit 562629c

3 files changed

Lines changed: 90 additions & 3 deletions

File tree

internal/argocd/manager.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ func SetConfig(url, token string, insecureTLS bool, tokenIsFresh bool) {
148148
defaultManager.SetConfig(url, token, insecureTLS, tokenIsFresh)
149149
}
150150

151+
// RestoreConfig rolls the default manager back to a captured state, including
152+
// the token's context binding.
153+
func RestoreConfig(url, token string, insecureTLS bool, tokenContext string) {
154+
defaultManager.RestoreConfig(url, token, insecureTLS, tokenContext)
155+
}
156+
151157
// IsConfigured reports whether the default manager has connection settings.
152158
func IsConfigured() bool { return defaultManager.IsConfigured() }
153159

@@ -219,6 +225,28 @@ func (m *Manager) SetConfig(url, token string, insecureTLS bool, tokenIsFresh bo
219225
}
220226
}
221227

228+
// RestoreConfig rolls the manager back to a previously-captured state, INCLUDING
229+
// the token's context binding. It exists for the connect-rollback path: the
230+
// candidate SetConfig may have re-stamped tokenContext for a fresh token, and a
231+
// plain SetConfig(fresh=false) rollback leaves that stamp in place — which would
232+
// mark the restored (older) token as valid for the wrong cluster and defeat the
233+
// auto-discovery cross-cluster guard. Restoring tokenContext explicitly keeps the
234+
// guard intact.
235+
func (m *Manager) RestoreConfig(url, token string, insecureTLS bool, tokenContext string) {
236+
m.mu.Lock()
237+
m.seeded = true
238+
m.manualURL = strings.TrimRight(strings.TrimSpace(url), "/")
239+
m.token = token
240+
m.insecureTLS = insecureTLS
241+
m.tokenContext = tokenContext
242+
m.generation++
243+
fwd := m.dropConnectionLocked()
244+
m.mu.Unlock()
245+
if fwd != nil {
246+
fwd.stop()
247+
}
248+
}
249+
222250
// IsConfigured reports whether the Argo CD integration has connection settings
223251
// (an explicit URL or a token) — i.e. the user has set it up, even if a live
224252
// probe hasn't landed yet. Used to offer the deep-diff capability immediately
@@ -366,6 +394,20 @@ func (m *Manager) Probe(ctx context.Context) error {
366394
return err
367395
}
368396
if err := m.verifyAuth(ctx, url, snap); err != nil {
397+
// resolve may have installed a fresh port-forward to a reachable
398+
// argocd-server; a token failure means we won't use that endpoint, so
399+
// tear the tunnel down rather than leave a disconnected manager holding a
400+
// live forward. Skip when a config change already superseded this probe —
401+
// that SetConfig/Reset owns the cleanup.
402+
m.mu.Lock()
403+
var fwd *activeForward
404+
if !m.staleLocked(snap) {
405+
fwd = m.dropConnectionLocked()
406+
}
407+
m.mu.Unlock()
408+
if fwd != nil {
409+
fwd.stop()
410+
}
369411
return err
370412
}
371413

internal/argocd/manager_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,43 @@ func TestSetConfigPreservesTokenContext(t *testing.T) {
112112
}
113113
}
114114

115+
// TestRestoreConfigRestoresTokenContextBinding pins the connect-rollback guard:
116+
// trying a fresh token in a new cluster re-stamps tokenContext to that cluster
117+
// BEFORE the probe. When the probe fails, the rollback must put the PRIOR binding
118+
// back onto the restored (older) token — otherwise that token would be left
119+
// marked valid for the wrong cluster in memory, defeating the cross-cluster guard
120+
// until restart. A plain SetConfig(fresh=false) rollback would leave the new
121+
// stamp; RestoreConfig restores the binding explicitly.
122+
func TestRestoreConfigRestoresTokenContextBinding(t *testing.T) {
123+
ctx := "cluster-a"
124+
m := newTestManager(config.Config{})
125+
m.contextName = func() string { return ctx }
126+
127+
m.SetConfig("", "token-a", false, true) // fresh auto-discovery token on cluster-a
128+
if m.tokenContext != "cluster-a" {
129+
t.Fatalf("tokenContext = %q, want cluster-a", m.tokenContext)
130+
}
131+
132+
// Operator switches to cluster-b and tries to connect a fresh token there; the
133+
// candidate SetConfig(fresh=true) stamps tokenContext=cluster-b before the probe.
134+
ctx = "cluster-b"
135+
m.SetConfig("", "token-b", false, true)
136+
if m.tokenContext != "cluster-b" {
137+
t.Fatalf("candidate tokenContext = %q, want cluster-b", m.tokenContext)
138+
}
139+
140+
// The probe fails, so the handler rolls back to the prior token + binding.
141+
m.RestoreConfig("", "token-a", false, "cluster-a")
142+
if m.tokenContext != "cluster-a" {
143+
t.Fatalf("tokenContext after rollback = %q, want cluster-a restored", m.tokenContext)
144+
}
145+
// Still in cluster-b: the restored token is bound to cluster-a, so the guard
146+
// must fire rather than send token-a to cluster-b's argocd-server.
147+
if err := m.Probe(context.Background()); !errors.Is(err, errTokenContextMismatch) {
148+
t.Fatalf("Probe after rollback = %v, want errTokenContextMismatch (guard intact)", err)
149+
}
150+
}
151+
115152
// TestIsConfigured pins that IsConfigured reflects whether the integration has
116153
// connection settings (explicit URL or token), independent of any live probe.
117154
func TestIsConfigured(t *testing.T) {

internal/server/argocd_integration.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,18 @@ func (s *Server) handleApplyArgoCDConfig(w http.ResponseWriter, r *http.Request)
9999
return
100100
}
101101

102+
// Capture the live token→context binding before the candidate SetConfig
103+
// re-stamps it for a fresh token. If the probe fails we must restore THIS
104+
// binding, not just the URL/token: a fresh-token SetConfig stamps
105+
// tokenContext to the current cluster, and a plain re-point rollback would
106+
// leave that stamp on the restored (older) token — marking it valid for the
107+
// wrong cluster and defeating the auto-discovery cross-cluster guard.
108+
prevTokenContext := argocd.TokenContext()
102109
argocd.SetConfig(rawURL, token, body.ArgoCDInsecureTLS, suppliesNewToken)
103110
ctx, cancel := context.WithTimeout(r.Context(), 12*time.Second)
104111
defer cancel()
105112
if err := argocd.Probe(ctx); err != nil {
106-
argocd.SetConfig(prev.ArgoCDURL, prev.ArgoCDToken, prev.ArgoCDInsecureTLS, false)
113+
argocd.RestoreConfig(prev.ArgoCDURL, prev.ArgoCDToken, prev.ArgoCDInsecureTLS, prevTokenContext)
107114
// The upstream error can embed the raw response body (proxy headers, a
108115
// render error with Secret data). Log it server-side; return only the
109116
// mapped guidance so nothing from Argo's body reaches the browser.
@@ -132,8 +139,9 @@ func (s *Server) handleApplyArgoCDConfig(w http.ResponseWriter, r *http.Request)
132139
}
133140
}
134141
}); err != nil {
135-
// The running client must agree with the on-disk config; roll it back.
136-
argocd.SetConfig(prev.ArgoCDURL, prev.ArgoCDToken, prev.ArgoCDInsecureTLS, false)
142+
// The running client must agree with the on-disk config; roll it back —
143+
// including the token's context binding (see prevTokenContext above).
144+
argocd.RestoreConfig(prev.ArgoCDURL, prev.ArgoCDToken, prev.ArgoCDInsecureTLS, prevTokenContext)
137145
log.Printf("[argocd] Failed to persist Argo CD config: %v", err)
138146
s.writeError(w, http.StatusInternalServerError, err.Error())
139147
return

0 commit comments

Comments
 (0)