Skip to content

Commit a512da1

Browse files
authored
Support conventional access key sources (#455)
Support using an access key from the conventional locations, matching the official tooling. Simplifies configuration for users of this library, which most likely also use the official tooling. Currently, they're required to set both `DEVELOCITY_API_TOKEN` (for this library) and `DEVELOCITY_ACCESS_KEY` (for official tooling), which is possibly redundant if the access keys are from the same user and instance. If an access key different from the one defined in usual locations must be used for the API, then a custom `Config.accessKey` is still supported. The name "API token" was also unintuitive considering that all Develocity documentation refers to them as "access keys". Resolves #338. This is a breaking change for projects using `DEVELOCITY_API_TOKEN` or `Config.apiToken`, targeting the upcoming 2025.1.0 major release. Thanks to @mcumings for providing an initial reference implementation.
1 parent 9422d1b commit a512da1

File tree

15 files changed

+290
-29
lines changed

15 files changed

+290
-29
lines changed

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
env:
1515
DEVELOCITY_API_URL: "${{ vars.DEVELOCITY_API_URL }}"
16-
DEVELOCITY_API_TOKEN: "${{ secrets.DEVELOCITY_API_TOKEN }}"
16+
DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}"
1717
DEVELOCITY_API_CACHE_ENABLED: "false"
1818
steps:
1919
- name: Checkout

.github/workflows/publish-library.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
runs-on: ubuntu-latest
2727
env:
2828
DEVELOCITY_API_URL: "${{ vars.DEVELOCITY_API_URL }}"
29-
DEVELOCITY_API_TOKEN: "${{ secrets.DEVELOCITY_API_TOKEN }}"
29+
DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}"
3030
steps:
3131
- name: Checkout
3232
uses: actions/checkout@v5

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ slf4j = "2.0.17"
1414
guava = "33.4.8-jre"
1515

1616
[libraries]
17+
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" }
1718
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
19+
okio-fakeFileSystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" }
1820
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
1921
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
2022
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }

library/api/library.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public final class com/gabrielfeo/develocity/api/Config {
9090
public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)Lcom/gabrielfeo/develocity/api/Config;
9191
public static synthetic fun copy$default (Lcom/gabrielfeo/develocity/api/Config;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILjava/lang/Object;)Lcom/gabrielfeo/develocity/api/Config;
9292
public fun equals (Ljava/lang/Object;)Z
93-
public final fun getApiToken ()Lkotlin/jvm/functions/Function0;
93+
public final fun getAccessKey ()Lkotlin/jvm/functions/Function0;
9494
public final fun getApiUrl ()Ljava/lang/String;
9595
public final fun getCacheConfig ()Lcom/gabrielfeo/develocity/api/Config$CacheConfig;
9696
public final fun getClientBuilder ()Lokhttp3/OkHttpClient$Builder;

library/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ dependencies {
2828
runtimeOnly(libs.slf4j.simple)
2929
compileOnly(libs.kotlin.jupyter.api)
3030
testImplementation(libs.okhttp.mockwebserver)
31+
testImplementation(libs.okio.fakeFileSystem)
3132
testImplementation(libs.okio)
3233
testImplementation(libs.kotlin.coroutines.test)
34+
testImplementation(libs.junit.jupiter.params)
3335
integrationTestImplementation(libs.kotlin.coroutines.test)
3436
integrationTestImplementation(libs.guava)
3537
integrationTestImplementation(libs.kotlin.jupyter.testkit)
@@ -104,6 +106,7 @@ tasks.named("compileKotlin", KotlinCompile::class) {
104106
}
105107

106108
tasks.withType<Test>().configureEach {
109+
maxParallelForks = 4
107110
systemProperty(
108111
"junit.jupiter.tempdir.cleanup.mode.default",
109112
System.getProperty("junit.jupiter.tempdir.cleanup.mode.default") ?: "always",
@@ -120,5 +123,4 @@ tasks.named<Test>("examplesTest") {
120123
inputs.files(files(publishUnsignedSnapshotDevelocityApiKotlinPublicationToMavenLocal))
121124
.withPropertyName("snapshotPublicationArtifacts")
122125
.withNormalizer(ClasspathNormalizer::class)
123-
maxParallelForks = 4
124126
}

library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/DevelocityApiIntegrationTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class DevelocityApiIntegrationTest {
3636
assertDoesNotThrow {
3737
val config = Config(
3838
apiUrl = "https://google.com/api/",
39-
apiToken = { "" },
39+
accessKey = { "" },
4040
)
4141
DevelocityApi.newInstance(config)
4242
}

library/src/main/kotlin/com/gabrielfeo/develocity/api/Config.kt

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.gabrielfeo.develocity.api
22

3-
import com.gabrielfeo.develocity.api.Config.CacheConfig
4-
import com.gabrielfeo.develocity.api.internal.*
3+
import com.gabrielfeo.develocity.api.internal.auth.accessKeyResolver
4+
import com.gabrielfeo.develocity.api.internal.basicOkHttpClient
5+
import com.gabrielfeo.develocity.api.internal.env
6+
import com.gabrielfeo.develocity.api.internal.systemProperties
57
import okhttp3.Dispatcher
68
import okhttp3.OkHttpClient
79
import java.io.File
10+
import java.net.URI
811
import kotlin.time.Duration.Companion.days
912

1013
/**
@@ -46,15 +49,32 @@ data class Config(
4649
*/
4750
val apiUrl: String =
4851
env["DEVELOCITY_API_URL"]
52+
?.also { requireValidUrl(it) }
4953
?: error(ERROR_NULL_API_URL),
5054

5155
/**
52-
* Provides the access token for a Develocity API instance. By default, uses environment
53-
* variable `DEVELOCITY_API_TOKEN`.
56+
* Provides the access key for a Develocity API instance. By default, resolves to the first
57+
* key from these sources that matches the host of [apiUrl]:
58+
*
59+
* - variable `DEVELOCITY_ACCESS_KEY`
60+
* - variable `GRADLE_ENTERPRISE_ACCESS_KEY`
61+
* - file `$GRADLE_USER_HOME/.gradle/develocity/keys.properties` or, if `GRADLE_USER_HOME` is
62+
* not set, `~/.gradle/develocity/keys.properties`
63+
* - file `~/.m2/.develocity/keys.properties`
64+
*
65+
* Refer to Develocity documentation for details on the format of such variables and files:
66+
*
67+
* - [Develocity Gradle Plugin User Manual][1]
68+
* - [Develocity Maven Extension User Manual][2]
69+
*
70+
* [1]: https://docs.gradle.com/develocity/gradle-plugin/current/#manual_access_key_configuration
71+
* [2]: https://docs.gradle.com/develocity/maven-extension/current/#manual_access_key_configuration
72+
*
73+
* @throws IllegalArgumentException if no matching key is found.
5474
*/
55-
val apiToken: () -> String = {
56-
env["DEVELOCITY_API_TOKEN"]
57-
?: error(ERROR_NULL_API_TOKEN)
75+
val accessKey: () -> String = {
76+
val host = URI(apiUrl).host
77+
requireNotNull(accessKeyResolver.resolve(host)) { ERROR_NULL_ACCESS_KEY }
5878
},
5979

6080
/**
@@ -216,6 +236,15 @@ data class Config(
216236
)
217237
}
218238

239+
private fun requireValidUrl(string: String) {
240+
requireNotNull(runCatching { URI(string) }.getOrNull()) {
241+
ERROR_MALFORMED_API_URL.format(string)
242+
}
243+
}
244+
219245
private const val ERROR_NULL_API_URL = "DEVELOCITY_API_URL is required"
220-
private const val ERROR_NULL_API_TOKEN = "DEVELOCITY_API_TOKEN is required"
246+
private const val ERROR_MALFORMED_API_URL = "DEVELOCITY_API_URL contains a malformed URL: %s"
247+
private const val ERROR_NULL_ACCESS_KEY = "Develocity access key not found. " +
248+
"Please set DEVELOCITY_ACCESS_KEY='[host]=[accessKey]' or see Config.accessKey javadoc for " +
249+
"other supported options."
221250
private const val ERROR_NULL_USER_HOME = "'user.home' system property must not be null"

library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/OkHttpClient.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ private fun OkHttpClient.Builder.addNetworkInterceptors(
6161
val logger = loggerFactory.newLogger(HttpLoggingInterceptor::class)
6262
addNetworkInterceptor(HttpLoggingInterceptor(logger = logger::debug).apply { level = BASIC })
6363
addNetworkInterceptor(HttpLoggingInterceptor(logger = logger::trace).apply { level = BODY })
64-
addNetworkInterceptor(HttpBearerAuth("bearer", config.apiToken()))
64+
// Add authentication after logging to prevent clients from leaking their access key
65+
addNetworkInterceptor(HttpBearerAuth("bearer", config.accessKey()))
6566
}
6667

6768
internal fun buildCache(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.gabrielfeo.develocity.api.internal.auth
2+
3+
import com.gabrielfeo.develocity.api.internal.Env
4+
import com.gabrielfeo.develocity.api.internal.env
5+
import com.gabrielfeo.develocity.api.internal.systemProperties
6+
import okio.FileSystem
7+
import okio.Path
8+
import okio.Path.Companion.toPath
9+
10+
internal var accessKeyResolver = AccessKeyResolver(
11+
env,
12+
homeDirectory = checkNotNull(systemProperties.userHome).toPath(),
13+
fileSystem = FileSystem.SYSTEM,
14+
)
15+
16+
internal class AccessKeyResolver(
17+
private val env: Env,
18+
private val homeDirectory: Path,
19+
private val fileSystem: FileSystem,
20+
) {
21+
22+
private val gradleUserHome: Path
23+
get() = env["GRADLE_USER_HOME"]?.toPath() ?: (homeDirectory / ".gradle")
24+
25+
fun resolve(host: String): String? {
26+
val keyEntry = fromEnvVar("DEVELOCITY_ACCESS_KEY", host)
27+
?: fromEnvVar("GRADLE_ENTERPRISE_ACCESS_KEY", host)
28+
?: fromFile(gradleUserHome / "develocity/keys.properties", host)
29+
?: fromFile(homeDirectory / ".m2/.develocity/keys.properties", host)
30+
return keyEntry?.accessKey
31+
}
32+
33+
private fun fromEnvVar(varName: String, host: String): HostAccessKeyEntry? =
34+
env[varName]?.let { envVar ->
35+
envVar.split(';')
36+
.firstNotNullOfOrNull { entry ->
37+
if (entry.isBlank()) null
38+
else HostAccessKeyEntry(entry).takeIf { it.host == host }
39+
}
40+
}
41+
42+
private fun fromFile(path: Path, host: String): HostAccessKeyEntry? {
43+
if (!fileSystem.exists(path)) return null
44+
fileSystem.read(path) {
45+
while (true) {
46+
val line = readUtf8Line()?.trim(' ') ?: break
47+
if (line.isBlank() || line.isComment()) continue
48+
val entry = HostAccessKeyEntry(line)
49+
if (entry.host == host) return entry
50+
}
51+
}
52+
return null
53+
}
54+
55+
private fun String.isComment() = startsWith('#')
56+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.gabrielfeo.develocity.api.internal.auth
2+
3+
internal class HostAccessKeyEntry(entry: String) {
4+
5+
private val components = entry.substringBefore(" #").trim().split('=')
6+
7+
init {
8+
require(components.size == 2 && host.isNotBlank() && accessKey.isNotBlank()) {
9+
"Invalid access key entry format: '${redact(entry)}'. Expected format is 'host=accessKey'."
10+
}
11+
}
12+
13+
val host: String get() = components[0]
14+
val accessKey: String get() = components[1]
15+
}
16+
17+
private const val REDACTED_MAX_LENGTH = 5
18+
19+
private fun redact(entry: String): String =
20+
if (entry.length <= REDACTED_MAX_LENGTH) entry
21+
else "${entry.substring(0, REDACTED_MAX_LENGTH - 1)}[redacted]"

0 commit comments

Comments
 (0)