@@ -108,6 +108,28 @@ impl TokenProvider {
108108 return Ok ( true ) ;
109109 }
110110
111+ // Strategy 3: self-issue a new token and write it to Vault.
112+ // Stacker validates by reading from the same Vault KV path, so a token
113+ // the agent writes there is immediately accepted on the next poll.
114+ if let Some ( vault) = & self . vault_client {
115+ let new_token = generate_secure_token ( ) ;
116+ match vault
117+ . store_agent_token ( & self . deployment_hash , & new_token, None )
118+ . await
119+ {
120+ Ok ( ( ) ) => {
121+ let mut token = self . token . write ( ) . await ;
122+ * token = new_token;
123+ super :: audit_log:: AuditLogger :: new ( )
124+ . token_self_issued ( & self . deployment_hash , "primary_strategies_failed" ) ;
125+ return Ok ( true ) ;
126+ }
127+ Err ( e) => {
128+ warn ! ( error = %e, "Vault self-issue failed; token unchanged" ) ;
129+ }
130+ }
131+ }
132+
111133 debug ! ( "No new token available after refresh attempt" ) ;
112134 Ok ( false )
113135 }
@@ -121,6 +143,14 @@ impl TokenProvider {
121143 }
122144}
123145
146+ fn generate_secure_token ( ) -> String {
147+ use ring:: rand:: { SecureRandom , SystemRandom } ;
148+ let rng = SystemRandom :: new ( ) ;
149+ let mut bytes = [ 0u8 ; 32 ] ;
150+ rng. fill ( & mut bytes) . expect ( "CSPRNG failure" ) ;
151+ bytes. iter ( ) . map ( |b| format ! ( "{:02x}" , b) ) . collect ( )
152+ }
153+
124154#[ cfg( test) ]
125155mod tests {
126156 use super :: * ;
@@ -190,4 +220,93 @@ mod tests {
190220 tp2. swap ( "b" . into ( ) ) . await ;
191221 assert_eq ! ( tp. get( ) . await , "b" ) ;
192222 }
223+
224+ #[ tokio:: test]
225+ async fn refresh_self_issues_token_when_vault_has_no_entry ( ) {
226+ use crate :: security:: vault_client:: VaultClient ;
227+ use mockito:: Server ;
228+
229+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
230+
231+ let mut server = Server :: new_async ( ) . await ;
232+
233+ // Strategy 1: Vault read returns 404 (KV entry was deleted)
234+ let read = server
235+ . mock ( "GET" , "/v1/status_panel/dep-abc/status_panel_token" )
236+ . with_status ( 404 )
237+ . with_body ( r#"{"errors":[]}"# )
238+ . expect ( 1 )
239+ . create_async ( )
240+ . await ;
241+
242+ // Strategy 3: Vault write accepted (must be called exactly once)
243+ let write = server
244+ . mock ( "POST" , "/v1/status_panel/dep-abc/status_panel_token" )
245+ . with_status ( 200 )
246+ . with_body ( "{}" )
247+ . expect ( 1 )
248+ . create_async ( )
249+ . await ;
250+
251+ let _addr = EnvGuard :: set ( "VAULT_ADDRESS" , & server. url ( ) ) ;
252+ let _tok = EnvGuard :: set ( "VAULT_TOKEN" , "vault-root" ) ;
253+ let _prefix = EnvGuard :: set ( "VAULT_AGENT_PATH_PREFIX" , "status_panel" ) ;
254+ // Strategy 2 is a no-op: env token matches current
255+ let _agent = EnvGuard :: set ( "AGENT_TOKEN" , "stale-token" ) ;
256+
257+ let vault = VaultClient :: from_env ( ) . unwrap ( ) . unwrap ( ) ;
258+ let tp = TokenProvider :: new ( "stale-token" . into ( ) , Some ( vault) , "dep-abc" . into ( ) ) ;
259+
260+ let changed = tp. refresh ( ) . await . unwrap ( ) ;
261+ assert ! ( changed, "expected self-issued token to be applied" ) ;
262+
263+ let new_token = tp. get ( ) . await ;
264+ assert_ne ! ( new_token, "stale-token" ) ;
265+ assert_eq ! ( new_token. len( ) , 64 , "expected 32-byte hex token" ) ;
266+ assert ! ( new_token. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
267+
268+ read. assert_async ( ) . await ;
269+ write. assert_async ( ) . await ;
270+ }
271+
272+ #[ tokio:: test]
273+ async fn refresh_keeps_token_when_vault_write_forbidden ( ) {
274+ use crate :: security:: vault_client:: VaultClient ;
275+ use mockito:: Server ;
276+
277+ let _guard = env_lock ( ) . lock ( ) . unwrap ( ) ;
278+
279+ let mut server = Server :: new_async ( ) . await ;
280+
281+ // Strategy 1: Vault read returns 404
282+ let _read = server
283+ . mock ( "GET" , "/v1/status_panel/dep-xyz/status_panel_token" )
284+ . with_status ( 404 )
285+ . with_body ( r#"{"errors":[]}"# )
286+ . create_async ( )
287+ . await ;
288+
289+ // Strategy 3: Vault write rejected (agent lacks write capability)
290+ let write = server
291+ . mock ( "POST" , "/v1/status_panel/dep-xyz/status_panel_token" )
292+ . with_status ( 403 )
293+ . with_body ( r#"{"errors":["permission denied"]}"# )
294+ . expect ( 1 )
295+ . create_async ( )
296+ . await ;
297+
298+ let _addr = EnvGuard :: set ( "VAULT_ADDRESS" , & server. url ( ) ) ;
299+ let _tok = EnvGuard :: set ( "VAULT_TOKEN" , "vault-root" ) ;
300+ let _prefix = EnvGuard :: set ( "VAULT_AGENT_PATH_PREFIX" , "status_panel" ) ;
301+ let _agent = EnvGuard :: set ( "AGENT_TOKEN" , "stale-token" ) ;
302+
303+ let vault = VaultClient :: from_env ( ) . unwrap ( ) . unwrap ( ) ;
304+ let tp = TokenProvider :: new ( "stale-token" . into ( ) , Some ( vault) , "dep-xyz" . into ( ) ) ;
305+
306+ let changed = tp. refresh ( ) . await . unwrap ( ) ;
307+ assert ! ( !changed, "Strategy 3 must not claim success on write failure" ) ;
308+ assert_eq ! ( tp. get( ) . await , "stale-token" , "token must not change on failure" ) ;
309+
310+ write. assert_async ( ) . await ;
311+ }
193312}
0 commit comments