Skip to content

Commit 85235b0

Browse files
mawinter69timja
authored andcommitted
API tokens with expiration date (jenkinsci#23859)
* API tokens with expiration API tokens can now get an optional expiration date. Added a select drop down from where you can choose from predefined durations, a custom date (max 1 year) and no expiration. Expiration date is shown in the list. Not expiring tokens are marked with warning. fixes jenkinsci#16695 * revert changes in SetupWizard * fix linter * fix ordering * show when token expired * notify on expiring and expired tokens * format * 2 lines only make the creation date a tooltip of the expiration * feedback * feedback 2 don't add token when choosing date in the past directly warn after creation when about to expire * prettier --------- Co-authored-by: Tim Jacomb <timjacomb1@gmail.com>
1 parent 06cefb7 commit 85235b0

File tree

14 files changed

+450
-39
lines changed

14 files changed

+450
-39
lines changed

core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
import java.util.Collection;
3535
import java.util.Collections;
3636
import java.util.List;
37+
import jenkins.management.Badge;
3738
import jenkins.model.Jenkins;
39+
import jenkins.security.ApiTokenProperty;
3840
import org.jenkinsci.Symbol;
3941
import org.kohsuke.accmod.Restricted;
4042
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -64,6 +66,14 @@ public String getUrlName() {
6466
return UserProperty.allByCategoryClass(UserPropertyCategory.Security.class);
6567
}
6668

69+
public Badge getBadge() {
70+
ApiTokenProperty apiTokenProperty = getTargetUser().getProperty(ApiTokenProperty.class);
71+
if (apiTokenProperty != null) {
72+
return apiTokenProperty.getBadge();
73+
}
74+
return null;
75+
}
76+
6777
/**
6878
* Inject the outer class configuration page into the sidenav and the request routing of the user
6979
*/

core/src/main/java/jenkins/model/navigation/UserAction.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import hudson.model.User;
3333
import java.util.ArrayList;
3434
import java.util.List;
35+
import jenkins.management.Badge;
36+
import jenkins.security.ApiTokenProperty;
3537
import org.kohsuke.accmod.Restricted;
3638
import org.kohsuke.accmod.restrictions.NoExternalUse;
3739

@@ -83,6 +85,20 @@ public User getUser() {
8385
return User.current();
8486
}
8587

88+
@Override
89+
public Badge getBadge() {
90+
User current = User.current();
91+
92+
if (User.current() == null) {
93+
return null;
94+
}
95+
ApiTokenProperty apiTokenProperty = current.getProperty(ApiTokenProperty.class);
96+
if (apiTokenProperty != null) {
97+
return apiTokenProperty.getBadge();
98+
}
99+
return null;
100+
}
101+
86102
@Override
87103
public boolean isPrimaryAction() {
88104
return true;

core/src/main/java/jenkins/security/ApiTokenProperty.java

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626

2727
import edu.umd.cs.findbugs.annotations.CheckForNull;
2828
import edu.umd.cs.findbugs.annotations.NonNull;
29+
import edu.umd.cs.findbugs.annotations.Nullable;
2930
import hudson.Extension;
31+
import hudson.Functions;
3032
import hudson.Util;
3133
import hudson.model.Descriptor.FormException;
3234
import hudson.model.User;
@@ -43,6 +45,7 @@
4345
import java.time.ZoneId;
4446
import java.time.ZonedDateTime;
4547
import java.time.format.DateTimeFormatter;
48+
import java.time.format.FormatStyle;
4649
import java.util.Collection;
4750
import java.util.Collections;
4851
import java.util.Date;
@@ -51,6 +54,7 @@
5154
import java.util.logging.Level;
5255
import java.util.logging.Logger;
5356
import java.util.stream.Collectors;
57+
import jenkins.management.Badge;
5458
import jenkins.model.Jenkins;
5559
import jenkins.security.apitoken.ApiTokenPropertyConfiguration;
5660
import 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

Comments
 (0)