2626
2727import edu .umd .cs .findbugs .annotations .CheckForNull ;
2828import edu .umd .cs .findbugs .annotations .NonNull ;
29+ import edu .umd .cs .findbugs .annotations .Nullable ;
2930import hudson .Extension ;
31+ import hudson .Functions ;
3032import hudson .Util ;
3133import hudson .model .Descriptor .FormException ;
3234import hudson .model .User ;
4345import java .time .ZoneId ;
4446import java .time .ZonedDateTime ;
4547import java .time .format .DateTimeFormatter ;
48+ import java .time .format .FormatStyle ;
4649import java .util .Collection ;
4750import java .util .Collections ;
4851import java .util .Date ;
5154import java .util .logging .Level ;
5255import java .util .logging .Logger ;
5356import java .util .stream .Collectors ;
57+ import jenkins .management .Badge ;
5458import jenkins .model .Jenkins ;
5559import jenkins .security .apitoken .ApiTokenPropertyConfiguration ;
5660import jenkins .security .apitoken .ApiTokenStats ;
@@ -255,13 +259,26 @@ public static class TokenInfoAndStats {
255259 public final int useCounter ;
256260 public final Date lastUseDate ;
257261 public final long numDaysUse ;
262+ public final String expirationDate ;
263+ public final boolean expired ;
264+ public final boolean aboutToExpire ;
258265
259266 public TokenInfoAndStats (@ NonNull ApiTokenStore .HashedToken token , @ NonNull ApiTokenStats .SingleTokenStats stats ) {
260267 this .uuid = token .getUuid ();
261268 this .name = token .getName ();
262269 this .creationDate = token .getCreationDate ();
263270 this .numDaysCreation = token .getNumDaysCreation ();
264271 this .isLegacy = token .isLegacy ();
272+ this .expired = token .isExpired ();
273+ this .aboutToExpire = token .isAboutToExpire ();
274+
275+ LocalDate expirationDate = token .getExpirationDate ();
276+ if (expirationDate == null ) {
277+ this .expirationDate = "never" ;
278+ } else {
279+ this .expirationDate = expirationDate .format (DateTimeFormatter .ofLocalizedDate (FormatStyle .SHORT )
280+ .withLocale (Functions .getCurrentLocale ()));
281+ }
265282
266283 this .useCounter = stats .getUseCounter ();
267284 this .lastUseDate = stats .getLastUseDate ();
@@ -427,15 +444,27 @@ public ApiTokenStats getTokenStats() {
427444 // essentially meant for scripting
428445 @ Restricted (Beta .class )
429446 public @ NonNull String addFixedNewToken (@ NonNull String name , @ NonNull String tokenPlainValue ) throws IOException {
430- String tokenUuid = this .tokenStore .addFixedNewToken (name , tokenPlainValue );
447+ return addFixedNewToken (name , tokenPlainValue , null );
448+ }
449+
450+ // essentially meant for scripting
451+ @ Restricted (Beta .class )
452+ public @ NonNull String addFixedNewToken (@ NonNull String name , @ NonNull String tokenPlainValue , @ Nullable LocalDate expirationDate ) throws IOException {
453+ String tokenUuid = this .tokenStore .addFixedNewToken (name , tokenPlainValue , expirationDate );
431454 user .save ();
432455 return tokenUuid ;
433456 }
434457
435458 // essentially meant for scripting
436459 @ Restricted (Beta .class )
437460 public @ NonNull TokenUuidAndPlainValue generateNewToken (@ NonNull String name ) throws IOException {
438- TokenUuidAndPlainValue tokenUuidAndPlainValue = tokenStore .generateNewToken (name );
461+ return generateNewToken (name , null );
462+ }
463+
464+ // essentially meant for scripting
465+ @ Restricted (Beta .class )
466+ public @ NonNull TokenUuidAndPlainValue generateNewToken (@ NonNull String name , @ Nullable LocalDate expirationDate ) throws IOException {
467+ TokenUuidAndPlainValue tokenUuidAndPlainValue = tokenStore .generateNewToken (name , expirationDate );
439468 user .save ();
440469 return tokenUuidAndPlainValue ;
441470 }
@@ -538,7 +567,7 @@ public boolean hasCurrentUserRightToGenerateNewToken(User propertyOwner) {
538567 }
539568
540569 /**
541- * @deprecated use {@link #doGenerateNewToken(User, String)} instead
570+ * @deprecated use {@link #doGenerateNewToken(User, String, String, String )} instead
542571 */
543572 @ Deprecated
544573 @ RequirePOST
@@ -570,7 +599,8 @@ public HttpResponse doChangeToken(@AncestorInPath User u, StaplerResponse rsp) t
570599 }
571600
572601 @ RequirePOST
573- public HttpResponse doGenerateNewToken (@ AncestorInPath User u , @ QueryParameter String newTokenName ) throws IOException {
602+ public HttpResponse doGenerateNewToken (@ AncestorInPath User u , @ QueryParameter String newTokenName ,
603+ @ QueryParameter String tokenExpiration , @ QueryParameter String expirationDuration ) throws IOException {
574604 if (!hasCurrentUserRightToGenerateNewToken (u )) {
575605 return HttpResponses .forbidden ();
576606 }
@@ -582,21 +612,57 @@ public HttpResponse doGenerateNewToken(@AncestorInPath User u, @QueryParameter S
582612 tokenName = newTokenName ;
583613 }
584614
615+ LocalDate expirationDate = getExpirationDate (tokenExpiration , expirationDuration );
616+
585617 ApiTokenProperty p = u .getProperty (ApiTokenProperty .class );
586618 if (p == null ) {
587619 p = forceNewInstance (u , false );
588620 u .addProperty (p );
589621 }
590622
591- TokenUuidAndPlainValue tokenUuidAndPlainValue = p .generateNewToken (tokenName );
623+ String expirationDateString = "never" ;
624+ if (expirationDate != null ) {
625+ expirationDateString = expirationDate .format (DateTimeFormatter .ofLocalizedDate (FormatStyle .SHORT )
626+ .withLocale (Functions .getCurrentLocale ()));
627+ }
628+
629+ Map <String , Object > data = new HashMap <>();
630+ if (expirationDate != null && LocalDate .now ().isAfter (expirationDate )) {
631+ return HttpResponses .errorJSON (Messages .ApiTokenProperty_expirationInPast ());
632+ }
633+
634+ TokenUuidAndPlainValue tokenUuidAndPlainValue = p .generateNewToken (tokenName , expirationDate );
592635
593- Map <String , String > data = new HashMap <>();
594636 data .put ("tokenUuid" , tokenUuidAndPlainValue .tokenUuid );
595637 data .put ("tokenName" , tokenName );
596638 data .put ("tokenValue" , tokenUuidAndPlainValue .plainValue );
639+ data .put ("expirationDate" , expirationDateString );
640+ data .put ("aboutToExpire" , tokenUuidAndPlainValue .aboutToExpire );
597641 return HttpResponses .okJSON (data );
598642 }
599643
644+ private LocalDate getExpirationDate (String tokenExpiration , String expirationDuration ) {
645+ if (expirationDuration == null ) {
646+ expirationDuration = "" ;
647+ }
648+ expirationDuration = expirationDuration .trim ();
649+
650+ return switch (expirationDuration ) {
651+ case "" , "never" -> {
652+ yield null ;
653+ }
654+ case "custom" -> {
655+ yield LocalDate .parse (tokenExpiration );
656+ }
657+ default -> {
658+ LocalDate now = LocalDate .now ();
659+ int days = Integer .parseInt (expirationDuration );
660+ yield now .plusDays (days );
661+ }
662+ };
663+
664+ }
665+
600666 /**
601667 * This method is dangerous and should not be used without caution.
602668 * The token passed here could have been tracked by different network system during its trip.
@@ -606,7 +672,9 @@ public HttpResponse doGenerateNewToken(@AncestorInPath User u, @QueryParameter S
606672 @ Restricted (NoExternalUse .class )
607673 public HttpResponse doAddFixedToken (@ AncestorInPath User u ,
608674 @ QueryParameter String newTokenName ,
609- @ QueryParameter String newTokenPlainValue ) throws IOException {
675+ @ QueryParameter String newTokenPlainValue ,
676+ @ QueryParameter String tokenExpiration ,
677+ @ QueryParameter String expirationDuration ) throws IOException {
610678 if (!hasCurrentUserRightToGenerateNewToken (u )) {
611679 return HttpResponses .forbidden ();
612680 }
@@ -618,13 +686,15 @@ public HttpResponse doAddFixedToken(@AncestorInPath User u,
618686 tokenName = newTokenName ;
619687 }
620688
689+ LocalDate expirationDate = getExpirationDate (tokenExpiration , expirationDuration );
690+
621691 ApiTokenProperty p = u .getProperty (ApiTokenProperty .class );
622692 if (p == null ) {
623693 p = forceNewInstance (u , false );
624694 u .addProperty (p );
625695 }
626696
627- String tokenUuid = p .tokenStore .addFixedNewToken (tokenName , newTokenPlainValue );
697+ String tokenUuid = p .tokenStore .addFixedNewToken (tokenName , newTokenPlainValue , expirationDate );
628698 u .save ();
629699
630700 Map <String , String > data = new HashMap <>();
@@ -742,4 +812,25 @@ public HttpResponse doRevokeAllExcept(@AncestorInPath User u,
742812 @ Deprecated
743813 @ Restricted (NoExternalUse .class )
744814 public static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey (ApiTokenProperty .class , "seed" , 16 );
815+
816+ @ Restricted (NoExternalUse .class )
817+ public Badge getBadge () {
818+ long expiringTokenCount = getTokenList ().stream ().filter (t -> t .aboutToExpire && !t .expired ).count ();
819+ long expiredTokenCount = getTokenList ().stream ().filter (t -> t .expired ).count ();
820+ StringBuilder tooltip = new StringBuilder ();
821+ if (expiringTokenCount > 0 ) {
822+ tooltip .append (Messages .ApiTokenProperty_aboutToExpireTokens (expiringTokenCount ));
823+ }
824+ if (expiredTokenCount > 0 ) {
825+ if (expiringTokenCount > 0 ) {
826+ tooltip .append ("\n " );
827+ }
828+ tooltip .append (Messages .ApiTokenProperty_expiredTokens (expiredTokenCount ));
829+ }
830+ if (expiredTokenCount + expiringTokenCount > 0 ) {
831+ return new Badge (Long .toString (expiringTokenCount + expiredTokenCount ), tooltip .toString (), Badge .Severity .WARNING );
832+ } else {
833+ return null ;
834+ }
835+ }
745836}
0 commit comments