Skip to content

Commit

Permalink
Add support for short-lived tokens (#138)
Browse files Browse the repository at this point in the history
* Add support for short-lived tokens

* Allow configuring short-lived access token expiry

* Add retries for retrieving short-lived tokens

* Update the implementation of the client

* Require Bamboo 7.2.10

* Improve assertion and change the type
  • Loading branch information
welandaz authored Apr 17, 2024
1 parent cd5c815 commit 11d3842
Show file tree
Hide file tree
Showing 19 changed files with 495 additions and 133 deletions.
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</scm>

<properties>
<bamboo.version>6.9.2</bamboo.version>
<bamboo.version>7.2.10</bamboo.version>
<bamboo.data.version>${bamboo.version}</bamboo.data.version>
<bamboo.product.data.path>
${basedir}/src/test/resources/generated-test-resources-692-license-723.zip
Expand Down Expand Up @@ -126,6 +126,12 @@
<version>2.8.9</version>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
2 changes: 2 additions & 0 deletions src/main/i18n/develocity-bamboo-plugin.properties
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ develocity.config.enforce-url.description=Whether to enforce the Develocity serv
develocity.config.general.title=General settings
develocity.config.general.vcs-repository-filter=Auto-injection Git VCS repository filters
develocity.config.general.vcs-repository-filter.info=Newline-delimited set of rules in the form of +|-:repository_matching_keyword, for which to enable/disable Develocity Gradle plugin/Maven extension auto-injection.<br/>By default, all Git VCS repositories have auto-injection enabled.
develocity.config.general.short-lived-token-expiry=Develocity short-lived access token expiry
develocity.config.general.short-lived-token-expiry.description=The short-lived access tokens expiry in hours. Defaults to 2 hours. For more information, please refer to the <a href="https://docs.gradle.com/enterprise/api-manual/#short_lived_access_tokens" target="_blank">documentation</a>.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.gradle.develocity.bamboo;

import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;

public final class DevelocityAccessCredential {

private final String hostname;
private final String key;

private DevelocityAccessCredential(String hostname, String key) {
this.hostname = hostname;
this.key = key;
}

public static DevelocityAccessCredential of(String hostname, String key) {
return new DevelocityAccessCredential(hostname, key);
}

public static Optional<DevelocityAccessCredential> parse(String rawAccessKey, String host) {
return Arrays.stream(rawAccessKey.split(";"))
.map(k -> k.split("="))
.filter(hostKey -> hostKey[0].equals(host))
.map(hostKey -> new DevelocityAccessCredential(hostKey[0], hostKey[1]))
.findFirst();
}

public static boolean isValid(String value) {
if (StringUtils.isBlank(value)) {
return false;
}

String[] entries = value.split(";");

for (String entry : entries) {
String[] parts = entry.split("=", 2);
if (parts.length < 2) {
return false;
}

String servers = parts[0];
String accessKey = parts[1];

if (StringUtils.isBlank(servers) || StringUtils.isBlank(accessKey)) {
return false;
}

for (String server : servers.split(",")) {
if (StringUtils.isBlank(server)) {
return false;
}
}
}

return true;
}

public String getRawAccessKey() {
return hostname + "=" + key;
}

public String getHostname() {
return hostname;
}

public String getKey() {
return key;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DevelocityAccessCredential that = (DevelocityAccessCredential) o;
return Objects.equals(hostname, that.hostname) && Objects.equals(key, that.key);
}

@Override
public int hashCode() {
return Objects.hash(hostname, key);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

public class DevelocityPreJobAction implements PreJobAction {
Expand All @@ -22,13 +24,18 @@ public class DevelocityPreJobAction implements PreJobAction {
private final PersistentConfigurationManager configurationManager;
private final UsernameAndPasswordCredentialsProvider credentialsProvider;
private final List<BuildScanInjector<? extends BuildToolConfiguration>> injectors;
private final ShortLivedTokenClient shortLivedTokenClient;

public DevelocityPreJobAction(PersistentConfigurationManager configurationManager,
UsernameAndPasswordCredentialsProvider credentialsProvider,
List<BuildScanInjector<? extends BuildToolConfiguration>> injectors) {
public DevelocityPreJobAction(
PersistentConfigurationManager configurationManager,
UsernameAndPasswordCredentialsProvider credentialsProvider,
List<BuildScanInjector<? extends BuildToolConfiguration>> injectors,
ShortLivedTokenClient shortLivedTokenClient
) {
this.configurationManager = configurationManager;
this.credentialsProvider = credentialsProvider;
this.injectors = injectors;
this.shortLivedTokenClient = shortLivedTokenClient;
}

@Override
Expand All @@ -46,8 +53,8 @@ public void execute(@NotNull StageExecution stageExecution, @NotNull BuildContex
UsernameAndPassword credentials = credentialsProvider.findByName(sharedCredentialName).orElse(null);
if (credentials == null) {
LOGGER.warn(
"Shared credentials with the name {} are not found. Environment variable {} will not be set",
sharedCredentialName, Constants.DEVELOCITY_ACCESS_KEY
"Shared credentials with the name {} are not found. Environment variable {} will not be set",
sharedCredentialName, Constants.DEVELOCITY_ACCESS_KEY
);
return;
}
Expand All @@ -56,20 +63,27 @@ public void execute(@NotNull StageExecution stageExecution, @NotNull BuildContex
String accessKey = credentials.getPassword();
if (StringUtils.isBlank(accessKey)) {
LOGGER.warn(
"Shared credentials with the name {} do not have password set. Environment variable {} will not be set",
sharedCredentialName, Constants.DEVELOCITY_ACCESS_KEY
"Shared credentials with the name {} do not have password set. Environment variable {} will not be set",
sharedCredentialName, Constants.DEVELOCITY_ACCESS_KEY
);
return;
}

injectors.stream()
.filter(i -> i.hasSupportedTasks(buildContext))
.map(i -> i.buildToolConfiguration(configuration))
.filter(BuildToolConfiguration::isEnabled)
.findFirst()
.ifPresent(__ ->
buildContext
.getVariableContext()
.addLocalVariable(Constants.ACCESS_KEY, accessKey));
DevelocityAccessCredential.parse(accessKey, getHostnameFromServerUrl(configuration.getServer()))
.flatMap(parsedKey -> injectors.stream()
.filter(i -> i.hasSupportedTasks(buildContext))
.map(i -> i.buildToolConfiguration(configuration))
.filter(BuildToolConfiguration::isEnabled)
.findFirst()
.flatMap(__ -> shortLivedTokenClient.get(configuration.getServer(), parsedKey, configuration.getShortLivedTokenExpiry())))
.ifPresent(shortLivedToken -> buildContext.getVariableContext().addLocalVariable(Constants.ACCESS_KEY, shortLivedToken.getRawAccessKey()));
}

private static String getHostnameFromServerUrl(String serverUrl) {
try {
return new URL(serverUrl).getHost();
} catch (MalformedURLException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.gradle.develocity.bamboo;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Component
public class ShortLivedTokenClient {

private static final Logger LOGGER = LoggerFactory.getLogger(ShortLivedTokenClient.class);

private static final RequestBody EMPTY_BODY = RequestBody.create(new byte[]{});

private static final int MAX_RETRIES = 3;
private static final Duration RETRY_INTERVAL = Duration.ofSeconds(1);

private final OkHttpClient httpClient;

public ShortLivedTokenClient() {
this.httpClient = new OkHttpClient().newBuilder()
.callTimeout(10, TimeUnit.SECONDS)
.build();
}

public Optional<DevelocityAccessCredential> get(String server, DevelocityAccessCredential accessKey, String expiryInHours) {
String url = normalize(server) + "api/auth/token";
if (StringUtils.isNotBlank(expiryInHours)) {
url += "?expiresInHours=" + expiryInHours;
}

Request request = new Request.Builder()
.url(url)
.addHeader("Authorization", "Bearer " + accessKey.getKey())
.addHeader("Content-Type", "application/json")
.post(EMPTY_BODY)
.build();

int tryCount = 0;
Integer errorCode = null;
while (tryCount < MAX_RETRIES) {
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() == 200 && response.body() != null) {
return Optional.of(DevelocityAccessCredential.of(accessKey.getHostname(), response.body().string()));
} else if (response.code() == 401) {
LOGGER.warn("Short lived token request failed {} with status code 401", url);
return Optional.empty();
} else {
tryCount++;
errorCode = response.code();
Thread.sleep(RETRY_INTERVAL.toMillis());
}
} catch (IOException e) {
LOGGER.warn("Short lived token request failed {}", url, e);
return Optional.empty();
} catch (InterruptedException e) {
// Ignore sleep exception as
}
}

LOGGER.warn("Develocity short lived token request failed {} with status code {}", url, errorCode);
return Optional.empty();
}

private static String normalize(String server) {
return server.endsWith("/") ? server : server + "/";
}

}

This file was deleted.

Loading

0 comments on commit 11d3842

Please sign in to comment.