Skip to content

Commit 252a908

Browse files
runningcodeclaude
andauthored
feat(distribution): Add dedicated distribution auth token property (#1007)
* feat(distribution): Add dedicated distribution auth token property Add distributionAuthToken property to DistributionExtension to decouple distribution authentication from the main org auth token for improved security separation. Changes: - Add distributionAuthToken property to DistributionExtension - Update GenerateDistributionPropertiesTask to use distributionAuthToken from extension - Add fallback to SENTRY_DISTRIBUTION_AUTH_TOKEN environment variable - Update all existing tests to use the new property - Add tests for distributionAuthToken property and environment variable fallback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(distribution): Simplify auth token with convention Simplify the distribution auth token implementation by using Gradle's convention mechanism: - Rename distributionAuthToken to authToken for simplicity - Set convention to System.getenv("SENTRY_DISTRIBUTION_AUTH_TOKEN") - Remove explicit fallback logic in GenerateDistributionPropertiesTask - Update tests to reflect the simpler convention-based approach 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: Add tests to verify distribution auth token isolation Add tests to ensure: - distribution.authToken takes precedence over main authToken - No fallback to main authToken when distribution authToken is not set This ensures proper security separation between distribution and main auth tokens. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e4de3b commit 252a908

4 files changed

Lines changed: 95 additions & 10 deletions

File tree

plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/DistributionExtension.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.sentry.android.gradle.extensions
22

33
import javax.inject.Inject
44
import org.gradle.api.model.ObjectFactory
5+
import org.gradle.api.provider.Property
56
import org.gradle.api.provider.SetProperty
67
import org.jetbrains.annotations.ApiStatus.Experimental
78

@@ -16,4 +17,8 @@ open class DistributionExtension @Inject constructor(objects: ObjectFactory) {
1617
*/
1718
val enabledVariants: SetProperty<String> =
1819
objects.setProperty(String::class.java).convention(emptySet())
20+
21+
/** Auth token used for distribution operations. */
22+
val authToken: Property<String> =
23+
objects.property(String::class.java).convention(System.getenv("SENTRY_DISTRIBUTION_AUTH_TOKEN"))
1924
}

plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/GenerateDistributionPropertiesTask.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ abstract class GenerateDistributionPropertiesTask : PropertiesFileOutputTask() {
117117
}
118118
task.projectSlug.set(projectProvider)
119119

120-
// Auth token only from extension (no fallback)
121-
task.orgAuthToken.set(extension.authToken)
120+
task.orgAuthToken.set(extension.distribution.authToken)
122121

123122
task.buildConfiguration.set(buildConfiguration)
124123
}

plugin-build/src/test/kotlin/io/sentry/android/gradle/extensions/DistributionExtensionTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,23 @@ class DistributionExtensionTest {
2424

2525
assertEquals(setOf("freeDebug", "paidRelease"), extension.enabledVariants.get())
2626
}
27+
28+
@Test
29+
fun `authToken uses environment variable by default`() {
30+
val project = ProjectBuilder.builder().build()
31+
val extension = project.objects.newInstance(DistributionExtension::class.java)
32+
33+
val envToken = System.getenv("SENTRY_DISTRIBUTION_AUTH_TOKEN")
34+
assertEquals(envToken, extension.authToken.orNull)
35+
}
36+
37+
@Test
38+
fun `authToken can be configured`() {
39+
val project = ProjectBuilder.builder().build()
40+
val extension = project.objects.newInstance(DistributionExtension::class.java)
41+
42+
extension.authToken.set("test-token")
43+
44+
assertEquals("test-token", extension.authToken.get())
45+
}
2746
}

plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/GenerateDistributionPropertiesTaskTest.kt

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class GenerateDistributionPropertiesTaskTest {
3131
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
3232
extension.org.set("test-org")
3333
extension.projectName.set("test-project")
34-
extension.authToken.set("test-token")
34+
extension.distribution.authToken.set("test-token")
3535

3636
val task: TaskProvider<GenerateDistributionPropertiesTask> =
3737
GenerateDistributionPropertiesTask.register(
@@ -64,7 +64,7 @@ class GenerateDistributionPropertiesTaskTest {
6464
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
6565
extension.org.set("extension-org")
6666
extension.projectName.set("extension-project")
67-
extension.authToken.set("extension-token")
67+
extension.distribution.authToken.set("extension-token")
6868

6969
val task: TaskProvider<GenerateDistributionPropertiesTask> =
7070
GenerateDistributionPropertiesTask.register(
@@ -89,7 +89,7 @@ class GenerateDistributionPropertiesTaskTest {
8989
// ext properties should take precedence
9090
assertEquals("ext-org", props.getProperty(ORG_SLUG_PROPERTY))
9191
assertEquals("ext-project", props.getProperty(PROJECT_SLUG_PROPERTY))
92-
// auth token should still come from extension as it's not in ext
92+
// auth token should still come from distribution extension
9393
assertEquals("extension-token", props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
9494
}
9595

@@ -132,16 +132,17 @@ class GenerateDistributionPropertiesTaskTest {
132132

133133
assertEquals("props-org", props.getProperty(ORG_SLUG_PROPERTY))
134134
assertEquals("props-project", props.getProperty(PROJECT_SLUG_PROPERTY))
135-
// Auth token should not be present (no fallback)
136-
assertNull(props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
135+
// Distribution auth token should match environment variable (if set)
136+
val envToken = System.getenv("SENTRY_DISTRIBUTION_AUTH_TOKEN")
137+
assertEquals(envToken, props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
137138
}
138139

139140
@Test
140141
fun `extension properties override sentry properties file`() {
141142
val project = createProject()
142143
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
143144
extension.org.set("extension-org")
144-
extension.authToken.set("extension-token")
145+
extension.distribution.authToken.set("extension-token")
145146

146147
// Create a sentry.properties file
147148
val sentryPropertiesFile = File(project.projectDir, "sentry.properties")
@@ -178,10 +179,69 @@ class GenerateDistributionPropertiesTaskTest {
178179
assertEquals("extension-org", props.getProperty(ORG_SLUG_PROPERTY))
179180
// Project should fall back to properties file
180181
assertEquals("props-project", props.getProperty(PROJECT_SLUG_PROPERTY))
181-
// Auth token comes from extension only
182+
// Distribution auth token comes from distribution extension
182183
assertEquals("extension-token", props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
183184
}
184185

186+
@Test
187+
fun `distribution authToken takes precedence over main authToken`() {
188+
val project = createProject()
189+
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
190+
extension.authToken.set("main-auth-token")
191+
extension.distribution.authToken.set("distribution-auth-token")
192+
193+
val task: TaskProvider<GenerateDistributionPropertiesTask> =
194+
GenerateDistributionPropertiesTask.register(
195+
project,
196+
extension,
197+
null,
198+
project.layout.buildDirectory.dir("dummy/folder/"),
199+
"test",
200+
"debug",
201+
)
202+
203+
val outputDir = File(project.buildDir, "dummy/folder/")
204+
outputDir.mkdirs()
205+
206+
task.get().generateProperties()
207+
208+
val expectedFile = File(project.buildDir, "dummy/folder/sentry-distribution.properties")
209+
val props = PropertiesUtil.load(expectedFile)
210+
211+
// Distribution auth token should be used, not main auth token
212+
assertEquals("distribution-auth-token", props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
213+
}
214+
215+
@Test
216+
fun `does not fall back to main authToken when distribution authToken is not set`() {
217+
val project = createProject()
218+
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
219+
extension.authToken.set("main-auth-token")
220+
// Explicitly set distribution auth token to null to override the env var convention
221+
extension.distribution.authToken.set(null as String?)
222+
223+
val task: TaskProvider<GenerateDistributionPropertiesTask> =
224+
GenerateDistributionPropertiesTask.register(
225+
project,
226+
extension,
227+
null,
228+
project.layout.buildDirectory.dir("dummy/folder/"),
229+
"test",
230+
"debug",
231+
)
232+
233+
val outputDir = File(project.buildDir, "dummy/folder/")
234+
outputDir.mkdirs()
235+
236+
task.get().generateProperties()
237+
238+
val expectedFile = File(project.buildDir, "dummy/folder/sentry-distribution.properties")
239+
val props = PropertiesUtil.load(expectedFile)
240+
241+
// Distribution auth token should not fall back to main auth token
242+
assertNull(props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
243+
}
244+
185245
@Test
186246
fun `handles missing values gracefully`() {
187247
val project = createProject()
@@ -208,7 +268,9 @@ class GenerateDistributionPropertiesTaskTest {
208268
// Should not write null values
209269
assertNull(props.getProperty(ORG_SLUG_PROPERTY))
210270
assertNull(props.getProperty(PROJECT_SLUG_PROPERTY))
211-
assertNull(props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
271+
// Distribution auth token should match environment variable (if set)
272+
val envToken = System.getenv("SENTRY_DISTRIBUTION_AUTH_TOKEN")
273+
assertEquals(envToken, props.getProperty(DISTRIBUTION_AUTH_TOKEN_PROPERTY))
212274
// Build configuration should always be present
213275
assertEquals("debug", props.getProperty(BUILD_CONFIGURATION_PROPERTY))
214276
}

0 commit comments

Comments
 (0)