-
Notifications
You must be signed in to change notification settings - Fork 0
fix(config): reject blank Config Server repository authority #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
seonghobae
wants to merge
13
commits into
develop
Choose a base branch
from
cursor/bc-bd8286b7-f5d0-41f4-b0d0-80d116480a1e-accc
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
dd5a427
test(config): reproduce implicit repository authority
seonghobae 622e882
test(config): refresh repository-authority RED onto live develop
seonghobae c5f3332
fix(config): require explicit repository authority on current develop
seonghobae 50ddcc0
Merge branch 'develop' into repair/config-repository-authority-8f96517
opencode-agent[bot] 43c4049
fix(config): reject blank Config Server repository authority
cursoragent 4df4406
test(config): reproduce repository authority edge cases
seonghobae 3dd4b93
test(config): reproduce pre-Git and mixed-profile failures
seonghobae 379a3b1
test(config): require early authority guard registration
seonghobae 4cf628a
fix(config): normalize repository authority validation
seonghobae d12f91b
fix(config): validate repository authority before Git bean creation
seonghobae 7df95c0
fix(config): register early repository authority guard
seonghobae 6ae22e9
docs(config): correct repository authority evidence and citations
seonghobae 64f0dac
Merge branch 'develop' into cursor/bc-bd8286b7-f5d0-41f4-b0d0-80d1164…
opencode-agent[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.xtrmetl.config; | ||
|
|
||
| import java.net.URI; | ||
| import java.util.Locale; | ||
|
|
||
| /** | ||
| * Decides whether a Config Server Git URI is an explicit deployment-owned authority. | ||
| * | ||
| * <p>Spring Cloud Config 5.0.4 treats a non-null empty URI as valid when | ||
| * {@code cloneOnStart} is false, so a missing or blank {@code CONFIG_REPO_URI} | ||
| * would otherwise start the process and defer Git work to the first request. | ||
| * Call this check during environment preparation, before Config Server creates | ||
| * its Git repository beans.</p> | ||
| * | ||
| * <p>Operators: set {@code CONFIG_REPO_URI} to the reviewed Git URI, then start | ||
| * the default profile. Use the {@code native} profile only as the sole active | ||
| * profile for local fixtures that must not depend on a remote.</p> | ||
| */ | ||
| public final class ConfigServerRepositoryAuthority { | ||
|
|
||
| static final String MISSING_AUTHORITY_MESSAGE = | ||
| "CONFIG_REPO_URI must name a deployment-owned Git repository; " | ||
| + "blank, unresolved, or demo authority is rejected before remote Git access"; | ||
| static final String MIXED_NATIVE_PROFILE_MESSAGE = | ||
| "native profile must be the only active profile when Config Server uses local fixtures"; | ||
|
|
||
| private static final String UNRESOLVED_PLACEHOLDER = "${CONFIG_REPO_URI"; | ||
| private static final String RETIRED_DEMO_HOST = "github.com"; | ||
| private static final String RETIRED_DEMO_PATH = "/your-repo/config-repo"; | ||
|
|
||
| private ConfigServerRepositoryAuthority() { | ||
| } | ||
|
|
||
| /** | ||
| * Rejects repository values that are not an operator-supplied destination. | ||
| * | ||
| * @param repositoryUri bound {@code spring.cloud.config.server.git.uri} value | ||
| * @throws IllegalStateException when the URI is missing, blank, still a | ||
| * placeholder, or the retired demo remote | ||
| */ | ||
| public static void requireExplicitRepository(String repositoryUri) { | ||
| if (repositoryUri == null || repositoryUri.isBlank()) { | ||
| throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); | ||
| } | ||
| String trimmed = repositoryUri.trim(); | ||
| if (trimmed.contains(UNRESOLVED_PLACEHOLDER) || isRetiredDemoRemote(trimmed)) { | ||
| throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Ensures the local-fixture {@code native} profile cannot be composed with | ||
| * another active profile to bypass Git repository authority validation. | ||
| * | ||
| * @param activeProfiles explicitly active Spring profiles | ||
| * @return {@code true} when standalone native mode is active | ||
| * @throws IllegalStateException when native is combined with another profile | ||
| */ | ||
| public static boolean requireSafeProfileComposition(String... activeProfiles) { | ||
| boolean nativeActive = false; | ||
| int activeCount = 0; | ||
| for (String profile : activeProfiles) { | ||
| if (profile == null || profile.isBlank()) { | ||
| continue; | ||
| } | ||
| activeCount++; | ||
| if ("native".equalsIgnoreCase(profile.trim())) { | ||
| nativeActive = true; | ||
| } | ||
| } | ||
| if (nativeActive && activeCount != 1) { | ||
| throw new IllegalStateException(MIXED_NATIVE_PROFILE_MESSAGE); | ||
| } | ||
| return nativeActive; | ||
| } | ||
|
|
||
| private static boolean isRetiredDemoRemote(String repositoryUri) { | ||
| try { | ||
| URI uri = URI.create(repositoryUri); | ||
| if (!RETIRED_DEMO_HOST.equalsIgnoreCase(uri.getHost())) { | ||
| return false; | ||
| } | ||
| String path = uri.getPath(); | ||
| if (path == null) { | ||
| return false; | ||
| } | ||
| String normalizedPath = path.toLowerCase(Locale.ROOT); | ||
| while (normalizedPath.endsWith("/") && normalizedPath.length() > 1) { | ||
| normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1); | ||
| } | ||
| return RETIRED_DEMO_PATH.equals(normalizedPath) | ||
| || (RETIRED_DEMO_PATH + ".git").equals(normalizedPath); | ||
| } catch (IllegalArgumentException ignored) { | ||
| return false; | ||
| } | ||
| } | ||
| } |
63 changes: 63 additions & 0 deletions
63
...main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityEnvironmentPostProcessor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package com.xtrmetl.config; | ||
|
|
||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; | ||
| import org.springframework.boot.env.EnvironmentPostProcessor; | ||
| import org.springframework.core.Ordered; | ||
| import org.springframework.core.env.ConfigurableEnvironment; | ||
|
|
||
| /** | ||
| * Validates Config Server repository authority after config data is loaded and | ||
| * before the application context creates Spring Cloud Config Git components. | ||
| * | ||
| * <p>This boundary intentionally runs before bean initialization so | ||
| * {@code clone-on-start=true} cannot contact an unreviewed or retired remote. | ||
| * Standalone {@code native} mode remains available for reviewed local fixtures, | ||
| * but native cannot be combined with another active profile.</p> | ||
| */ | ||
| public final class ConfigServerRepositoryAuthorityEnvironmentPostProcessor | ||
| implements EnvironmentPostProcessor, Ordered { | ||
|
|
||
| private static final String REPOSITORY_URI_PROPERTY = | ||
| "spring.cloud.config.server.git.uri"; | ||
|
|
||
| /** | ||
| * Runs immediately after Spring Boot has loaded normal ConfigData. | ||
| * | ||
| * @return the processor order | ||
| */ | ||
| @Override | ||
| public int getOrder() { | ||
| return ConfigDataEnvironmentPostProcessor.ORDER + 1; | ||
| } | ||
|
|
||
| /** | ||
| * Enforces profile composition and Git destination authority before context refresh. | ||
| * | ||
| * @param environment prepared Spring environment | ||
| * @param application application being started | ||
| */ | ||
| @Override | ||
| public void postProcessEnvironment( | ||
| ConfigurableEnvironment environment, | ||
| SpringApplication application | ||
| ) { | ||
| boolean nativeOnly = ConfigServerRepositoryAuthority.requireSafeProfileComposition( | ||
| environment.getActiveProfiles() | ||
| ); | ||
| if (nativeOnly) { | ||
| return; | ||
| } | ||
|
|
||
| String repositoryUri; | ||
| try { | ||
| repositoryUri = environment.getProperty(REPOSITORY_URI_PROPERTY); | ||
| } catch (IllegalArgumentException unresolvedPlaceholder) { | ||
| throw new IllegalStateException( | ||
| ConfigServerRepositoryAuthority.MISSING_AUTHORITY_MESSAGE, | ||
| unresolvedPlaceholder | ||
| ); | ||
| } | ||
| ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri); | ||
| } | ||
| } | ||
35 changes: 35 additions & 0 deletions
35
config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.xtrmetl.config; | ||
|
|
||
| import org.springframework.beans.factory.InitializingBean; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Profile; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| /** | ||
| * Fails default-profile startup when Git repository authority is not explicit. | ||
| * | ||
| * <p>The {@code native} profile is excluded so reviewed local fixtures can start | ||
| * without a remote. Do not use {@code native} as a production fallback.</p> | ||
| */ | ||
| @Component | ||
| @Profile("!native") | ||
| public class ConfigServerRepositoryAuthorityValidator implements InitializingBean { | ||
|
|
||
| private final String repositoryUri; | ||
|
|
||
| /** | ||
| * @param repositoryUri bound Git backend URI; empty when unset | ||
| */ | ||
| public ConfigServerRepositoryAuthorityValidator( | ||
| @Value("${spring.cloud.config.server.git.uri:}") String repositoryUri) { | ||
| this.repositoryUri = repositoryUri; | ||
| } | ||
|
|
||
| /** | ||
| * Rejects blank, unresolved, or demo repository authority before Git access. | ||
| */ | ||
| @Override | ||
| public void afterPropertiesSet() { | ||
|
seonghobae marked this conversation as resolved.
|
||
| ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| org.springframework.boot.env.EnvironmentPostProcessor=\ | ||
| com.xtrmetl.config.ConfigServerRepositoryAuthorityEnvironmentPostProcessor |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| package com.xtrmetl.config; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.WebApplicationType; | ||
| import org.springframework.context.ConfigurableApplicationContext; | ||
| import org.springframework.core.env.StandardEnvironment; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assertions.fail; | ||
|
|
||
| /** | ||
| * Starts the Config Server and proves repository/profile authority fails closed | ||
| * before a Git backend can become active. | ||
| * | ||
| * <p>Operators: set {@code CONFIG_REPO_URI} to a deployment-owned Git URI | ||
| * before starting the default profile. Use {@code native} only as the sole | ||
| * active profile for reviewed local fixtures that do not need a remote.</p> | ||
| */ | ||
| class ConfigServerRepositoryAuthorityLiveTest { | ||
|
|
||
| @Test | ||
| void blankRepositoryUriFailsClosedBeforeGitAccess() { | ||
| Exception failure = runExpectingFailure( | ||
| applicationWithoutInheritedConfigRepoUri(), | ||
| "--server.port=0", | ||
| "--eureka.client.enabled=false", | ||
| "--eureka.client.register-with-eureka=false", | ||
| "--eureka.client.fetch-registry=false", | ||
| "--spring.cloud.config.server.git.uri=", | ||
| "--spring.cloud.config.server.git.clone-on-start=true" | ||
| ); | ||
|
|
||
| assertTrue( | ||
| containsAuthorityFailure(failure), | ||
| () -> "Blank authority must fail with the repository-authority message: " + failure | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void unsetRepositoryUriFailsClosedWithoutInheritedEnvironmentAuthority() { | ||
| Exception failure = runExpectingFailure( | ||
| applicationWithoutInheritedConfigRepoUri(), | ||
| "--server.port=0", | ||
| "--eureka.client.enabled=false", | ||
| "--eureka.client.register-with-eureka=false", | ||
| "--eureka.client.fetch-registry=false" | ||
| ); | ||
|
|
||
| assertTrue( | ||
| containsAuthorityFailure(failure), | ||
| () -> "Unset authority must fail with the repository-authority message: " + failure | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void retiredDemoUriFailsBeforeCloneOnStartCanContactGit() { | ||
| Exception failure = runExpectingFailure( | ||
| applicationWithoutInheritedConfigRepoUri(), | ||
| "--server.port=0", | ||
| "--eureka.client.enabled=false", | ||
| "--eureka.client.register-with-eureka=false", | ||
| "--eureka.client.fetch-registry=false", | ||
| "--spring.cloud.config.server.git.uri=https://github.com/your-repo/config-repo.git", | ||
| "--spring.cloud.config.server.git.clone-on-start=true" | ||
| ); | ||
|
|
||
| assertTrue( | ||
| containsAuthorityFailure(failure), | ||
| () -> "Demo URI must be rejected before JGit clone-on-start: " + failure | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void nativeProfileCannotBeCombinedWithAnotherActiveProfile() { | ||
| Exception failure = runExpectingFailure( | ||
| applicationWithoutInheritedConfigRepoUri(), | ||
| "--server.port=0", | ||
| "--eureka.client.enabled=false", | ||
| "--eureka.client.register-with-eureka=false", | ||
| "--eureka.client.fetch-registry=false", | ||
| "--spring.profiles.active=native,default" | ||
| ); | ||
|
|
||
| assertTrue( | ||
| messageChain(failure).contains("native profile must be the only active profile"), | ||
| () -> "Mixed native profile startup must fail closed: " + failure | ||
| ); | ||
| } | ||
|
|
||
| private static SpringApplication applicationWithoutInheritedConfigRepoUri() { | ||
| SpringApplication application = new SpringApplication(ConfigServerApplication.class); | ||
| application.setWebApplicationType(WebApplicationType.SERVLET); | ||
| StandardEnvironment environment = new StandardEnvironment(); | ||
| environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); | ||
| application.setEnvironment(environment); | ||
| return application; | ||
| } | ||
|
|
||
| private static Exception runExpectingFailure(SpringApplication application, String... args) { | ||
| ConfigurableApplicationContext context = null; | ||
| try { | ||
| context = application.run(args); | ||
| fail("Config Server startup was expected to fail closed"); | ||
| throw new AssertionError("unreachable"); | ||
| } catch (Exception ex) { | ||
| return ex; | ||
| } finally { | ||
| if (context != null) { | ||
| context.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static boolean containsAuthorityFailure(Throwable thrown) { | ||
| return messageChain(thrown).contains("CONFIG_REPO_URI must name a deployment-owned Git repository"); | ||
| } | ||
|
|
||
| private static String messageChain(Throwable thrown) { | ||
| assertNotNull(thrown); | ||
| StringBuilder messages = new StringBuilder(); | ||
| for (Throwable current = thrown; current != null; current = current.getCause()) { | ||
| if (current.getMessage() != null) { | ||
| messages.append(current.getMessage()).append('\n'); | ||
| } | ||
| } | ||
| return messages.toString(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Only the top-level git URI is validated
requireExplicitRepositoryinspects onlyspring.cloud.config.server.git.uri. A composite backend or named repos (...git.repos.<name>.uri) with a blank top-level URI would fail closed despite being validly configured. Outside the default single-URI setup this constrains future config shapes.Was this helpful? React with 👍 or 👎 to provide feedback.