-
Notifications
You must be signed in to change notification settings - Fork 0
fix(config): reject demo and templated Config Server authority before JGit #328
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
base: develop
Are you sure you want to change the base?
Changes from all commits
dd5a427
622e882
c5f3332
50ddcc0
43c4049
23ac292
f9df0cf
7a85d09
b1fdb4a
e295de4
4785b03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| package com.xtrmetl.config; | ||
|
|
||
| import java.net.URI; | ||
| import java.util.Locale; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** | ||
| * 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 from | ||
| * {@link ConfigServerRepositoryAuthorityEnvironmentPostProcessor} during | ||
| * default-profile startup, before JGit {@code afterPropertiesSet}.</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"; | ||
|
|
||
| static final String REQUEST_TEMPLATE_MESSAGE = | ||
| "CONFIG_REPO_URI must be a concrete repository destination; " | ||
| + "{application}, {profile}, and {label} placeholders are rejected"; | ||
|
|
||
| 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 static final Pattern REQUEST_TEMPLATE = | ||
| Pattern.compile("\\{(application|profile|label)\\}", Pattern.CASE_INSENSITIVE); | ||
| private static final Pattern SCP_DEMO = Pattern.compile( | ||
| "(?i)(?:^|@)github\\.com[:/]your-repo/config-repo(?:\\.git)?/?$" | ||
| ); | ||
|
|
||
| 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 after Unicode | ||
| * trim, still a placeholder, the retired demo | ||
| * remote, or a request-templated Git location | ||
| */ | ||
| public static void requireExplicitRepository(String repositoryUri) { | ||
| if (repositoryUri == null) { | ||
| throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); | ||
| } | ||
| String trimmed = trimAuthority(repositoryUri); | ||
| if (trimmed.isEmpty() | ||
| || trimmed.contains(UNRESOLVED_PLACEHOLDER) | ||
| || isRetiredDemoRemote(trimmed)) { | ||
| throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); | ||
| } | ||
| if (REQUEST_TEMPLATE.matcher(trimmed).find()) { | ||
| throw new IllegalStateException(REQUEST_TEMPLATE_MESSAGE); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Ensures the local-fixture {@code native} profile cannot be composed with | ||
| * another active profile to skip 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; | ||
| } | ||
|
|
||
| /** | ||
| * Removes leading and trailing Unicode space and format padding. | ||
| * | ||
| * <p>{@link String#isBlank()} and {@link String#strip()} miss NBSP and | ||
| * zero-width padding, which would otherwise pass as a non-empty URI.</p> | ||
| * | ||
| * @param repositoryUri raw bound value | ||
| * @return value with ignorable padding removed from both ends | ||
| */ | ||
| static String trimAuthority(String repositoryUri) { | ||
| int start = 0; | ||
| int end = repositoryUri.length(); | ||
| while (start < end && isIgnorablePad(repositoryUri.codePointAt(start))) { | ||
| start += Character.charCount(repositoryUri.codePointAt(start)); | ||
| } | ||
| while (end > start) { | ||
| int codePoint = repositoryUri.codePointBefore(end); | ||
| if (!isIgnorablePad(codePoint)) { | ||
| break; | ||
| } | ||
| end -= Character.charCount(codePoint); | ||
| } | ||
| return repositoryUri.substring(start, end); | ||
| } | ||
|
|
||
| static boolean isRetiredDemoRemote(String repositoryUri) { | ||
| if (SCP_DEMO.matcher(repositoryUri).find()) { | ||
| return true; | ||
| } | ||
| try { | ||
| URI uri = URI.create(repositoryUri); | ||
| String host = uri.getHost(); | ||
| String path = uri.getPath(); | ||
| if (host == null || path == null) { | ||
| return false; | ||
| } | ||
| return RETIRED_DEMO_HOST.equalsIgnoreCase(host) && isRetiredDemoPath(path); | ||
| } catch (IllegalArgumentException ex) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private static boolean isRetiredDemoPath(String path) { | ||
| String normalized = path.toLowerCase(Locale.ROOT); | ||
| if (normalized.endsWith(".git")) { | ||
| normalized = normalized.substring(0, normalized.length() - 4); | ||
| } | ||
| if (normalized.endsWith("/")) { | ||
| normalized = normalized.substring(0, normalized.length() - 1); | ||
|
Comment on lines
+139
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L21-L21 Useful? React with 👍 / 👎. |
||
| } | ||
| return RETIRED_DEMO_PATH.equals(normalized); | ||
| } | ||
|
|
||
| private static boolean isIgnorablePad(int codePoint) { | ||
| return Character.isWhitespace(codePoint) | ||
| || Character.isSpaceChar(codePoint) | ||
| || codePoint == 0x200B | ||
| || codePoint == 0xFEFF; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| 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.annotation.Order; | ||
| import org.springframework.core.env.ConfigurableEnvironment; | ||
| import org.springframework.core.env.MapPropertySource; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Rejects blank, demo, unresolved, or request-templated Git authority before | ||
| * JGit {@code afterPropertiesSet}. | ||
| * | ||
| * <p>Spring Cloud Config 5.0.4 constructs {@code JGitEnvironmentRepository} as | ||
| * an unordered {@code InitializingBean}. A validator bean can therefore run | ||
| * after {@code cloneOnStart=true} has already contacted a remote. This | ||
| * processor runs after config-data load and before context refresh.</p> | ||
| * | ||
| * <p>Operators: set {@code CONFIG_REPO_URI} to a reviewed Git URI, then start | ||
| * the default profile. The {@code native} profile skips the Git URI check only | ||
| * when it is the sole active profile.</p> | ||
| */ | ||
| @Order(ConfigDataEnvironmentPostProcessor.ORDER + 1) | ||
| public class ConfigServerRepositoryAuthorityEnvironmentPostProcessor implements EnvironmentPostProcessor { | ||
|
|
||
| static final String PROPERTY_SOURCE_NAME = "config-server-repository-authority"; | ||
| static final String GIT_URI_PROPERTY = "spring.cloud.config.server.git.uri"; | ||
|
|
||
| /** | ||
| * Fails closed on incompatible profiles or missing Git authority. | ||
| * | ||
| * @param environment configurable application environment | ||
| * @param application Spring application being prepared | ||
| */ | ||
| @Override | ||
| public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { | ||
| if (ConfigServerRepositoryAuthority.requireSafeProfileComposition(environment.getActiveProfiles())) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When profiles are configured through AGENTS.md reference: AGENTS.md:L21-L21 Useful? React with 👍 / 👎. |
||
| return; | ||
| } | ||
| String repositoryUri; | ||
| try { | ||
| repositoryUri = environment.getProperty(GIT_URI_PROPERTY); | ||
| } catch (IllegalArgumentException unresolvedPlaceholder) { | ||
| throw new IllegalStateException( | ||
| ConfigServerRepositoryAuthority.MISSING_AUTHORITY_MESSAGE, | ||
| unresolvedPlaceholder | ||
| ); | ||
| } | ||
| ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri); | ||
|
seonghobae marked this conversation as resolved.
|
||
| String trimmed = ConfigServerRepositoryAuthority.trimAuthority(repositoryUri); | ||
| if (!trimmed.equals(repositoryUri)) { | ||
| environment.getPropertySources().addFirst(new MapPropertySource( | ||
| PROPERTY_SOURCE_NAME, | ||
| Map.of(GIT_URI_PROPERTY, trimmed) | ||
| )); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| 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; | ||
|
|
||
| /** | ||
| * Defense-in-depth fail-closed check after context refresh begins. | ||
| * | ||
| * <p>{@link ConfigServerRepositoryAuthorityEnvironmentPostProcessor} is the | ||
| * authority that runs before JGit. This bean remains so a missing processor | ||
| * registration still stops the default profile. 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() { | ||
| ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| org.springframework.boot.env.EnvironmentPostProcessor=\ | ||
| com.xtrmetl.config.ConfigServerRepositoryAuthorityEnvironmentPostProcessor |
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.
With
spring.profiles.active=NATIVE, this case-insensitive comparison treats native mode as active and makes the environment post-processor skip URI validation, while Spring's profile matching does not treatNATIVEas thenativeprofile. The Git backend and@Profile("!native")validator can therefore be created, and withclone-on-start=trueJGit may contact a blank or retired-demo destination before the unordered validator runs; only skip validation for the exact profile name Spring recognizes.AGENTS.md reference: AGENTS.md:L21-L21
Useful? React with 👍 / 👎.