1515 */
1616package org .springframework .vault .core .lease ;
1717
18+ import java .time .Clock ;
1819import java .time .Duration ;
1920import java .time .Instant ;
20- import java .util .Date ;
2121import java .util .HashMap ;
2222import java .util .HashSet ;
23+ import java .util .LinkedHashMap ;
2324import java .util .List ;
2425import java .util .Map ;
2526import java .util .Map .Entry ;
2627import java .util .Set ;
2728import java .util .concurrent .ConcurrentHashMap ;
2829import java .util .concurrent .CopyOnWriteArrayList ;
30+ import java .util .concurrent .Executor ;
2931import java .util .concurrent .ScheduledFuture ;
3032import java .util .concurrent .TimeUnit ;
3133import java .util .concurrent .atomic .AtomicInteger ;
3840
3941import org .springframework .beans .factory .DisposableBean ;
4042import org .springframework .beans .factory .InitializingBean ;
43+ import org .springframework .context .SmartLifecycle ;
4144import org .springframework .http .HttpStatus ;
4245import org .springframework .lang .Nullable ;
4346import org .springframework .scheduling .TaskScheduler ;
4649import org .springframework .scheduling .concurrent .ThreadPoolTaskScheduler ;
4750import org .springframework .util .Assert ;
4851import org .springframework .util .StringUtils ;
52+ import org .springframework .util .backoff .BackOff ;
53+ import org .springframework .util .backoff .BackOffExecution ;
54+ import org .springframework .util .backoff .ExponentialBackOff ;
4955import org .springframework .vault .VaultException ;
56+ import org .springframework .vault .authentication .event .AuthenticationErrorEvent ;
57+ import org .springframework .vault .authentication .event .AuthenticationErrorListener ;
58+ import org .springframework .vault .authentication .event .AuthenticationEvent ;
59+ import org .springframework .vault .authentication .event .AuthenticationListener ;
60+ import org .springframework .vault .authentication .event .LoginTokenExpiredEvent ;
61+ import org .springframework .vault .authentication .event .LoginTokenRenewalFailedEvent ;
5062import org .springframework .vault .client .VaultResponses ;
5163import org .springframework .vault .core .VaultOperations ;
5264import org .springframework .vault .core .lease .domain .Lease ;
6274/**
6375 * Event-based container to request secrets from Vault and renew the associated
6476 * {@link Lease}. Secrets can be rotated, depending on the requested
65- * {@link RequestedSecret#getMode()}.
66- *
67- * Usage example:
68- *
69- * <pre>
77+ * {@link RequestedSecret#getMode()}. Usage example: <pre>
7078 * <code>
7179 * SecretLeaseContainer container = new SecretLeaseContainer(vaultOperations,
7280 * taskScheduler);
73- *
7481 * RequestedSecret requestedSecret = container
7582 * .requestRotatingSecret("mysql/creds/my-role");
7683 * container.addLeaseListener(new LeaseListenerAdapter() {
7784 * @Override
7885 * public void onLeaseEvent(SecretLeaseEvent secretLeaseEvent) {
79- *
8086 * if (requestedSecret == secretLeaseEvent.getSource()) {
81- *
8287 * if (secretLeaseEvent instanceof SecretLeaseCreatedEvent) {
83- *
84- * }
85- *
88+ * }
8689 * if (secretLeaseEvent instanceof SecretLeaseExpiredEvent) {
87- *
88- * }
89- * }
90- * }
90+ * }
91+ * }
92+ * }
9193 * });
92- *
9394 * container.afterPropertiesSet();
9495 * container.start(); // events are triggered after starting the container
9596 * </code> </pre>
120121 * @see LeaseEndpoints
121122 * @see LeaseStrategy
122123 */
123- public class SecretLeaseContainer extends SecretLeaseEventPublisher implements InitializingBean , DisposableBean {
124+ public class SecretLeaseContainer extends SecretLeaseEventPublisher
125+ implements InitializingBean , DisposableBean , SmartLifecycle {
124126
125127 private static final AtomicIntegerFieldUpdater <SecretLeaseContainer > UPDATER = AtomicIntegerFieldUpdater
126128 .newUpdater (SecretLeaseContainer .class , "status" );
@@ -136,6 +138,10 @@ public class SecretLeaseContainer extends SecretLeaseEventPublisher implements I
136138 @ SuppressWarnings ("FieldMayBeFinal" ) // allow setting via reflection.
137139 private static Log logger = LogFactory .getLog (SecretLeaseContainer .class );
138140
141+ private final Clock clock = Clock .systemDefaultZone ();
142+
143+ private final LeaseAuthenticationEventListener authenticationListener = new LeaseAuthenticationEventListener ();
144+
139145 private final List <RequestedSecret > requestedSecrets = new CopyOnWriteArrayList <>();
140146
141147 private final Map <RequestedSecret , LeaseRenewalScheduler > renewals = new ConcurrentHashMap <>();
@@ -189,13 +195,31 @@ public SecretLeaseContainer(VaultOperations operations, TaskScheduler taskSchedu
189195 setTaskScheduler (taskScheduler );
190196 }
191197
198+ /**
199+ * Returns the {@link AuthenticationListener} to listen for login token events.
200+ * @return the {@link AuthenticationListener} to listen for login token events.
201+ * @since 3.1
202+ */
203+ public AuthenticationListener getAuthenticationListener () {
204+ return this .authenticationListener ;
205+ }
206+
207+ /**
208+ * Returns the {@link AuthenticationListener} to listen for login token error events.
209+ * @return the {@link AuthenticationListener} to listen for login token error events
210+ * @since 3.1
211+ */
212+ public AuthenticationErrorListener getAuthenticationErrorListener () {
213+ return this .authenticationListener ;
214+ }
215+
192216 /**
193217 * Set the {@link LeaseEndpoints} to delegate renewal/revocation calls to.
194218 * {@link LeaseEndpoints} encapsulates differences between Vault versions that affect
195219 * the location of renewal/revocation endpoints.
196220 * @param leaseEndpoints must not be {@literal null}.
197- * @since 2.1
198221 * @see LeaseEndpoints
222+ * @since 2.1
199223 */
200224 public void setLeaseEndpoints (LeaseEndpoints leaseEndpoints ) {
201225
@@ -336,6 +360,7 @@ public RequestedSecret addRequestedSecret(RequestedSecret requestedSecret) {
336360 * @see #afterPropertiesSet()
337361 * @see #stop()
338362 */
363+ @ Override
339364 public void start () {
340365
341366 Assert .state (this .initialized , "Container is not initialized" );
@@ -409,6 +434,7 @@ private static boolean isRotatingGenericSecret(RequestedSecret requestedSecret,
409434 *
410435 * @see #start()
411436 */
437+ @ Override
412438 public void stop () {
413439
414440 if (UPDATER .compareAndSet (this , STATUS_STARTED , STATUS_INITIAL )) {
@@ -419,30 +445,41 @@ public void stop() {
419445 }
420446 }
421447
448+ @ Override
449+ public boolean isRunning () {
450+ return UPDATER .get (this ) == STATUS_STARTED ;
451+ }
452+
453+ @ Override
454+ public int getPhase () {
455+ return 200 ;
456+ }
457+
422458 @ Override
423459 public void afterPropertiesSet () {
424460
425- if (!this .initialized ) {
461+ if (this .initialized ) {
462+ return ;
463+ }
426464
427- super .afterPropertiesSet ();
465+ super .afterPropertiesSet ();
428466
429- this .initialized = true ;
467+ this .initialized = true ;
430468
431- if (this .taskScheduler == null ) {
469+ if (this .taskScheduler == null ) {
432470
433- ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler ();
434- scheduler .setDaemon (true );
435- scheduler
436- .setThreadNamePrefix (String .format ("%s-%d-" , getClass ().getSimpleName (), poolId .incrementAndGet ()));
437- scheduler .afterPropertiesSet ();
471+ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler ();
472+ scheduler .setDaemon (true );
473+ scheduler
474+ .setThreadNamePrefix (String .format ("%s-%d-" , getClass ().getSimpleName (), poolId .incrementAndGet ()));
475+ scheduler .afterPropertiesSet ();
438476
439- this .taskScheduler = scheduler ;
440- this .manageTaskScheduler = true ;
441- }
477+ this .taskScheduler = scheduler ;
478+ this .manageTaskScheduler = true ;
479+ }
442480
443- for (RequestedSecret requestedSecret : this .requestedSecrets ) {
444- this .renewals .put (requestedSecret , new LeaseRenewalScheduler (this .taskScheduler ));
445- }
481+ for (RequestedSecret requestedSecret : this .requestedSecrets ) {
482+ this .renewals .put (requestedSecret , new LeaseRenewalScheduler (this .taskScheduler ));
446483 }
447484 }
448485
@@ -472,6 +509,7 @@ public void destroy() throws Exception {
472509 doRevokeLease (entry .getKey (), lease );
473510 }
474511 }
512+ this .renewals .clear ();
475513
476514 if (this .manageTaskScheduler ) {
477515
@@ -484,6 +522,51 @@ public void destroy() throws Exception {
484522 }
485523 }
486524
525+ void restartSecrets () {
526+
527+ int status = this .status ;
528+ if (status == STATUS_STARTED ) {
529+
530+ logger .debug ("Restarting all secrets after token expiry/rotation" );
531+
532+ try {
533+
534+ Map <RequestedSecret , LeaseRenewalScheduler > previousLeases = new LinkedHashMap <>(this .renewals );
535+ this .renewals .clear ();
536+ previousLeases .values ().forEach (LeaseRenewalScheduler ::disableScheduleRenewal );
537+
538+ for (RequestedSecret requestedSecret : this .requestedSecrets ) {
539+
540+ LeaseRenewalScheduler renewalScheduler = new LeaseRenewalScheduler (this .taskScheduler );
541+ Lease previousLease = getPreviousLease (previousLeases , requestedSecret );
542+
543+ try {
544+ doStart (requestedSecret , renewalScheduler , (secrets , lease ) -> {
545+ onSecretsRotated (requestedSecret , previousLease , lease , secrets .getRequiredData ());
546+ }, () -> {
547+ });
548+
549+ }
550+ catch (Exception e ) {
551+ onError (requestedSecret , previousLease , e );
552+ }
553+ }
554+ }
555+ catch (Exception e ) {
556+ logger .error ("Cannot restart secrets" , e );
557+ }
558+ }
559+ }
560+
561+ private static Lease getPreviousLease (Map <RequestedSecret , LeaseRenewalScheduler > previousLeases ,
562+ RequestedSecret requestedSecret ) {
563+
564+ LeaseRenewalScheduler leaseRenewalScheduler = previousLeases .get (requestedSecret );
565+ Lease previousLease = leaseRenewalScheduler != null ? leaseRenewalScheduler .getLease () : null ;
566+
567+ return previousLease == null ? Lease .none () : previousLease ;
568+ }
569+
487570 /**
488571 * Renew a {@link RequestedSecret secret}.
489572 * @param secret the {@link RequestedSecret secret}' to renew.
@@ -712,6 +795,7 @@ private Lease doRenew(Lease lease) {
712795 * @param requestedSecret must not be {@literal null}.
713796 * @param lease must not be {@literal null}.
714797 */
798+ @ Override
715799 protected void onLeaseExpired (RequestedSecret requestedSecret , Lease lease ) {
716800
717801 if (requestedSecret .getMode () == Mode .ROTATE ) {
@@ -916,6 +1000,70 @@ private boolean isLeaseRotateOnly(Lease lease, RequestedSecret requestedSecret)
9161000
9171001 }
9181002
1003+ private class LeaseAuthenticationEventListener implements AuthenticationListener , AuthenticationErrorListener {
1004+
1005+ private final BackOff backOff = new ExponentialBackOff (500 , 1.5 );
1006+
1007+ private final AtomicReference <Timeout > timeout = new AtomicReference <>();
1008+
1009+ @ Override
1010+ public void onAuthenticationError (AuthenticationErrorEvent authenticationEvent ) {
1011+ if (authenticationEvent instanceof LoginTokenRenewalFailedEvent ) {
1012+ logger .debug ("LoginTokenRenewalFailedEvent received" );
1013+ restartSecrets ();
1014+ }
1015+ }
1016+
1017+ @ Override
1018+ public void onAuthenticationEvent (AuthenticationEvent leaseEvent ) {
1019+ if (leaseEvent instanceof LoginTokenExpiredEvent ) {
1020+ logger .debug ("LoginTokenExpiredEvent received" );
1021+ restartSecrets ();
1022+ }
1023+ }
1024+
1025+ /**
1026+ * Restart secrets after a changed token. Either the token was rotated or it has
1027+ * expired.
1028+ */
1029+ private void restartSecrets () {
1030+
1031+ if (!isRunning ()) {
1032+ logger .debug ("Ignore token event as the container is not running" );
1033+ }
1034+
1035+ Timeout timeout = this .timeout .get ();
1036+ if (timeout != null && !timeout .isExpired (clock )) {
1037+ logger .debug ("Backoff timeout not reached. Dropping event" );
1038+ return ;
1039+ }
1040+
1041+ Timeout executionToSet = new Timeout (backOff .start (), clock );
1042+
1043+ if (this .timeout .compareAndSet (timeout , executionToSet )) {
1044+
1045+ if (taskScheduler instanceof Executor e ) {
1046+ e .execute (SecretLeaseContainer .this ::restartSecrets );
1047+ }
1048+ else {
1049+ taskScheduler .schedule (SecretLeaseContainer .this ::restartSecrets , Instant .now ());
1050+ }
1051+ }
1052+ }
1053+
1054+ record Timeout (BackOffExecution execution , long timeout ) {
1055+
1056+ public Timeout (BackOffExecution execution , Clock clock ) {
1057+ this (execution , clock .millis () + execution .nextBackOff ());
1058+ }
1059+
1060+ public boolean isExpired (Clock clock ) {
1061+ return clock .millis () > timeout ;
1062+ }
1063+ }
1064+
1065+ }
1066+
9191067 /**
9201068 * This one-shot trigger creates only one execution time to trigger an execution only
9211069 * once.
0 commit comments