Skip to content

Commit beeaa47

Browse files
olamylifeofguenter
andauthored
[JENKINS-61200] in case of authentication issue downloading avatar, refresh credentials and make cache time configurable (#637)
* [JENKINS-61200] in case of authentication issue downloading avatar, refresh credentials and make cache time configurable Signed-off-by: Olivier Lamy <[email protected]> * add some logging when authz happen while fetching bb scm source Signed-off-by: Olivier Lamy <[email protected]> Signed-off-by: Olivier Lamy <[email protected]> Co-authored-by: Günter Grodotzki <[email protected]>
1 parent 863acff commit beeaa47

File tree

9 files changed

+149
-72
lines changed

9 files changed

+149
-72
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
</configuration>
208208
<executions>
209209
<execution>
210+
<phase>validate</phase>
210211
<goals>
211212
<goal>check</goal>
212213
</goals>

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -817,62 +817,75 @@ private void retrieveTags(final BitbucketSCMSourceRequest request) throws IOExce
817817
@Override
818818
protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOException, InterruptedException {
819819
final BitbucketApi bitbucket = buildBitbucketClient();
820-
List<? extends BitbucketBranch> branches = bitbucket.getBranches();
821-
if (head instanceof PullRequestSCMHead) {
822-
PullRequestSCMHead h = (PullRequestSCMHead) head;
823-
BitbucketCommit targetRevision = findCommit(h.getTarget().getName(), branches, listener);
824-
if (targetRevision == null) {
825-
LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]",
820+
try {
821+
List<? extends BitbucketBranch> branches = bitbucket.getBranches();
822+
if (head instanceof PullRequestSCMHead) {
823+
PullRequestSCMHead h = (PullRequestSCMHead) head;
824+
BitbucketCommit targetRevision = findCommit(h.getTarget().getName(), branches, listener);
825+
if (targetRevision == null) {
826+
LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]",
826827
new Object[]{repoOwner, repository, h.getTarget().getName()});
827-
return null;
828-
}
829-
BitbucketCommit sourceRevision;
830-
if (bitbucket instanceof BitbucketCloudApiClient) {
831-
branches = head.getOrigin() == SCMHeadOrigin.DEFAULT
828+
return null;
829+
}
830+
BitbucketCommit sourceRevision;
831+
if (bitbucket instanceof BitbucketCloudApiClient) {
832+
branches = head.getOrigin() == SCMHeadOrigin.DEFAULT
832833
? branches
833834
: buildBitbucketClient(h).getBranches();
834-
sourceRevision = findCommit(h.getBranchName(), branches, listener);
835-
} else {
836-
try {
837-
BitbucketPullRequest pr = bitbucket.getPullRequestById(Integer.parseInt(h.getId()));
838-
sourceRevision = findPRCommit(pr, listener);
839-
} catch (NumberFormatException nfe) {
840-
LOGGER.log(Level.WARNING, "Cannot parse the PR id {0}", h.getId());
841-
sourceRevision = null;
835+
sourceRevision = findCommit(h.getBranchName(), branches, listener);
836+
} else {
837+
try {
838+
BitbucketPullRequest pr = bitbucket.getPullRequestById(Integer.parseInt(h.getId()));
839+
sourceRevision = findPRCommit(pr, listener);
840+
} catch (NumberFormatException nfe) {
841+
LOGGER.log(Level.WARNING, "Cannot parse the PR id {0}", h.getId());
842+
sourceRevision = null;
843+
}
842844
}
843-
}
844-
if (sourceRevision == null) {
845-
LOGGER.log(Level.WARNING, "No revision found in {0}/{1} for PR-{2} [{3}]",
845+
if (sourceRevision == null) {
846+
LOGGER.log(Level.WARNING, "No revision found in {0}/{1} for PR-{2} [{3}]",
846847
new Object[]{
847-
h.getRepoOwner(),
848-
h.getRepository(),
849-
h.getId(),
850-
h.getBranchName()
848+
h.getRepoOwner(),
849+
h.getRepository(),
850+
h.getId(),
851+
h.getBranchName()
851852
});
852-
return null;
853-
}
854-
return new PullRequestSCMRevision<>(
853+
return null;
854+
}
855+
return new PullRequestSCMRevision<>(
855856
h,
856857
new BitbucketGitSCMRevision(h.getTarget(), targetRevision),
857858
new BitbucketGitSCMRevision(h, sourceRevision)
858-
);
859-
} else if(head instanceof BitbucketTagSCMHead) {
860-
BitbucketTagSCMHead tagHead = (BitbucketTagSCMHead) head;
861-
List<? extends BitbucketBranch> tags = bitbucket.getTags();
862-
BitbucketCommit revision = findCommit(head.getName(), tags, listener);
863-
if (revision == null) {
864-
LOGGER.log(Level.WARNING, "No tag found in {0}/{1} with name [{2}]", new Object[] { repoOwner, repository, head.getName() });
865-
return null;
866-
}
867-
return new BitbucketTagSCMRevision(tagHead, revision);
868-
} else {
869-
BitbucketCommit revision = findCommit(head.getName(), branches, listener);
870-
if (revision == null) {
871-
LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]",
859+
);
860+
} else if (head instanceof BitbucketTagSCMHead) {
861+
BitbucketTagSCMHead tagHead = (BitbucketTagSCMHead) head;
862+
List<? extends BitbucketBranch> tags = bitbucket.getTags();
863+
BitbucketCommit revision = findCommit(head.getName(), tags, listener);
864+
if (revision == null) {
865+
LOGGER.log(Level.WARNING, "No tag found in {0}/{1} with name [{2}]", new Object[]{repoOwner, repository, head.getName()});
866+
return null;
867+
}
868+
return new BitbucketTagSCMRevision(tagHead, revision);
869+
} else {
870+
BitbucketCommit revision = findCommit(head.getName(), branches, listener);
871+
if (revision == null) {
872+
LOGGER.log(Level.WARNING, "No branch found in {0}/{1} with name [{2}]",
872873
new Object[]{repoOwner, repository, head.getName()});
873-
return null;
874+
return null;
875+
}
876+
return new BitbucketGitSCMRevision(head, revision);
877+
}
878+
} catch (IOException e) {
879+
// here we only want to display the job name to have it in the log
880+
if (e instanceof BitbucketRequestException) {
881+
BitbucketRequestException bre = (BitbucketRequestException) e;
882+
SCMSourceOwner scmSourceOwner = getOwner();
883+
if (bre.getHttpCode() == 401 && scmSourceOwner != null) {
884+
LOGGER.log(Level.WARNING, "BitbucketRequestException: Authz error. Status: 401 for Item '{0}' using credentialId '{1}'",
885+
new Object[]{scmSourceOwner.getFullDisplayName(), getCredentialsId()});
886+
}
874887
}
875-
return new BitbucketGitSCMRevision(head, revision);
888+
throw e;
876889
}
877890
}
878891

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketTeamMetadataAction.java

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@
2727
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi;
2828
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory;
2929
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
30+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException;
3031
import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCache;
3132
import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCacheSource;
33+
import com.cloudbees.plugins.credentials.CredentialsMatchers;
34+
import com.cloudbees.plugins.credentials.CredentialsProvider;
3235
import com.cloudbees.plugins.credentials.common.StandardCredentials;
36+
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
37+
import hudson.model.Item;
38+
import hudson.security.ACL;
39+
import hudson.security.ACLContext;
3340
import java.io.IOException;
3441
import java.io.Serializable;
3542
import java.util.Objects;
@@ -61,7 +68,7 @@ public BitbucketTeamMetadataAction(String serverUrl, StandardCredentials credent
6168
public static class BitbucketAvatarCacheSource implements AvatarCacheSource, Serializable {
6269
private static final long serialVersionUID = 1L;
6370
private final String serverUrl;
64-
private final StandardCredentials credentials;
71+
private StandardCredentials credentials;
6572
private final String repoOwner;
6673

6774
public BitbucketAvatarCacheSource(String serverUrl, StandardCredentials credentials, String repoOwner) {
@@ -72,20 +79,66 @@ public BitbucketAvatarCacheSource(String serverUrl, StandardCredentials credenti
7279
}
7380

7481
@Override
75-
public AvatarImage fetch() {
76-
BitbucketAuthenticator authenticator = AuthenticationTokens
77-
.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);
78-
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null);
82+
public AvatarImage fetch(StandardCredentials credentials) {
7983
try {
80-
return bitbucket.getTeamAvatar();
84+
return doFetch(credentials);
8185
} catch (IOException e) {
86+
if (e.getCause() instanceof BitbucketRequestException) {
87+
BitbucketRequestException bre = (BitbucketRequestException) e.getCause();
88+
if (bre.getHttpCode()==401) {
89+
// credentials not updated here maybe we need to refresh them
90+
StandardCredentials standardCredentials = findCredentials();
91+
// try again with refreshed credentials
92+
// TODO compare previous and new token if we really need to try again
93+
if (standardCredentials != null) {
94+
this.credentials = standardCredentials;
95+
try {
96+
return doFetch(this.credentials);
97+
} catch (IOException | InterruptedException ex) {
98+
LOGGER.log(Level.INFO, ex.getClass().getName()+": " + e.getMessage(), e);
99+
}
100+
}
101+
}
102+
}
82103
LOGGER.log(Level.INFO, "IOException: " + e.getMessage(), e);
83104
} catch (InterruptedException e) {
84105
LOGGER.log(Level.INFO, "InterruptedException: " + e.getMessage(), e);
85106
}
86107
return null;
87108
}
88109

110+
private StandardCredentials findCredentials() {
111+
try (ACLContext ctx = ACL.as(ACL.SYSTEM)) {
112+
return CredentialsMatchers.firstOrNull(
113+
CredentialsProvider.lookupCredentials(
114+
credentials.getClass(),
115+
(Item) null, // context
116+
ACL.SYSTEM,
117+
URIRequirementBuilder.fromUri(serverUrl).build()
118+
),
119+
CredentialsMatchers.allOf(
120+
CredentialsMatchers.withId(credentials.getId()),
121+
CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(credentials.getClass()))
122+
)
123+
);
124+
}
125+
}
126+
127+
private AvatarImage doFetch(StandardCredentials credentials) throws IOException, InterruptedException {
128+
BitbucketAuthenticator authenticator = AuthenticationTokens
129+
.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);
130+
BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null);
131+
return bitbucket.getTeamAvatar();
132+
}
133+
134+
@Override
135+
public AvatarImage fetch() {
136+
if(this.credentials==null) {
137+
throw new UnsupportedOperationException("this method can be used only with credentials");
138+
}
139+
return this.fetch(this.credentials);
140+
}
141+
89142
@Override
90143
public String hashKey() {
91144
return "" + serverUrl + "::" + repoOwner + "::" + (credentials != null ? credentials.getId() : "");

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/credentials/BitbucketOAuthAuthenticator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
44
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
5-
import hudson.util.Secret;
65
import org.apache.http.HttpRequest;
76
import org.scribe.model.OAuthConfig;
87
import org.scribe.model.OAuthConstants;
@@ -20,7 +19,7 @@ public class BitbucketOAuthAuthenticator extends BitbucketAuthenticator {
2019
public BitbucketOAuthAuthenticator(StandardUsernamePasswordCredentials credentials) {
2120
super(credentials);
2221

23-
OAuthConfig config = new OAuthConfig(credentials.getUsername(), Secret.toString(credentials.getPassword()));
22+
OAuthConfig config = new OAuthConfig(credentials.getUsername(), credentials.getPassword().getPlainText());
2423

2524
BitbucketOAuthService OAuthService = (BitbucketOAuthService) new BitbucketOAuth().createService(config);
2625

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/credentials/BitbucketOAuthCredentialMatcher.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.cloudbees.plugins.credentials.Credentials;
44
import com.cloudbees.plugins.credentials.CredentialsMatcher;
55
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
6-
import hudson.util.Secret;
76
import java.util.logging.Level;
87
import java.util.logging.Logger;
98

@@ -30,7 +29,7 @@ public boolean matches(Credentials item) {
3029
UsernamePasswordCredentials usernamePasswordCredential = ((UsernamePasswordCredentials) item);
3130
String username = usernamePasswordCredential.getUsername();
3231
boolean isEMail = username.contains(".") && username.contains("@");
33-
boolean validSecretLength = Secret.toString(usernamePasswordCredential.getPassword()).length() == secretLength;
32+
boolean validSecretLength = usernamePasswordCredential.getPassword().getPlainText().length() == secretLength;
3433
boolean validKeyLength = username.length() == keyLength;
3534

3635
return !isEMail && validKeyLength && validSecretLength;

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/credentials/BitbucketUsernamePasswordAuthenticator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator;
2929
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
30-
import hudson.util.Secret;
3130
import java.util.logging.Level;
3231
import java.util.logging.Logger;
3332
import org.apache.http.HttpHost;
@@ -56,7 +55,7 @@ public class BitbucketUsernamePasswordAuthenticator extends BitbucketAuthenticat
5655
public BitbucketUsernamePasswordAuthenticator(StandardUsernamePasswordCredentials credentials) {
5756
super(credentials);
5857
httpCredentials = new UsernamePasswordCredentials(credentials.getUsername(),
59-
Secret.toString(credentials.getPassword()));
58+
credentials.getPassword().getPlainText());
6059
}
6160

6261
/**

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCache.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ public class AvatarCache implements UnprotectedRootAction {
9393
/**
9494
* The cache of entries. Unused entries will be removed over time.
9595
*/
96-
private final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<String, CacheEntry>();
96+
private final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
9797

9898
/**
9999
* A background thread pool to refresh images.
100100
*/
101101
private final ThreadPoolExecutor service = new ThreadPoolExecutor(CONCURRENT_REQUEST_LIMIT,
102-
CONCURRENT_REQUEST_LIMIT, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
102+
CONCURRENT_REQUEST_LIMIT, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
103103
new NamingThreadFactory(new DaemonThreadFactory(), getClass().getName()));
104104

105105
/**
@@ -552,15 +552,15 @@ private synchronized void setFuture(Future<CacheEntry> future) {
552552
}
553553

554554
private synchronized boolean isStale() {
555-
return System.currentTimeMillis() - lastModified > TimeUnit.HOURS.toMillis(1);
555+
return System.currentTimeMillis() - lastModified > TimeUnit.MINUTES.toMillis(Long.getLong(AvatarCache.class.getName()+".stale.ttl",60));
556556
}
557557

558558
private void touch() {
559559
lastAccessed = System.currentTimeMillis();
560560
}
561561

562562
private boolean isUnused() {
563-
return lastAccessed > 0L && System.currentTimeMillis() - lastAccessed > TimeUnit.HOURS.toMillis(2);
563+
return lastAccessed > 0L && System.currentTimeMillis() - lastAccessed > TimeUnit.MINUTES.toMillis(Long.getLong(AvatarCache.class.getName()+".unused.ttl",60));
564564
}
565565
}
566566

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCacheSource.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
package com.cloudbees.jenkins.plugins.bitbucket.avatars;
2525

26+
import com.cloudbees.plugins.credentials.common.StandardCredentials;
2627
import java.awt.image.BufferedImage;
2728

2829
/**
@@ -49,23 +50,33 @@ public AvatarImage(final BufferedImage image, final long lastModified) {
4950

5051
/**
5152
*
53+
* @deprecated use {@link #fetch(StandardCredentials)}
5254
* Fetch image from source
5355
*
5456
* @return AvatarImage object
5557
*/
56-
public AvatarImage fetch();
58+
@Deprecated
59+
AvatarImage fetch();
60+
61+
/**
62+
*
63+
* Fetch image from source
64+
* @param credentials the credentials to use
65+
* @return AvatarImage object
66+
*/
67+
AvatarImage fetch(StandardCredentials credentials);
5768

5869
/**
5970
* Get unique hashKey for this item
6071
*
6172
* @return AvatarImage object
6273
*/
63-
public String hashKey();
74+
String hashKey();
6475

6576
/**
6677
* Make sure we can fetch
6778
*
6879
* @return true if can fetch
6980
*/
70-
public boolean canFetch();
81+
boolean canFetch();
7182
}

0 commit comments

Comments
 (0)