Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ workflows, and service-level maintenance.

- Root Maven aggregator: `pom.xml`
- Services: `etl-service/`, `cdc-service/`, `zuul-gateway/`, `eureka-server/`, `config-server/`
- Config Server is independently runnable and is not a default production dependency. The default Git profile must fail closed without an operator-supplied `CONFIG_REPO_URI`; see `docs/doctoring/config-server-repository-authority.md`.
- Shared code: `META-INF/`, common build config in root `pom.xml`
- Operations/docs: `docker/`, `docs/`, `.github/`, `scripts/`

Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ ETL Processing with Retry:
| ETL Service | 8000 | HTTP | Internal |
| CDC Service | 8001 | HTTP | Internal |
| Eureka Server | 8761 | HTTP | Internal |
| Config Server | 8888 | HTTP | Internal |
| Config Server | 8888 | HTTP | Internal; optional module, not a default Compose dependency. Default Git profile requires `CONFIG_REPO_URI` and fails closed on blank, unresolved, or demo authority. Use `native` only for local fixtures. |
| PostgreSQL | 5432 | TCP | Internal |
| Kafka | 9092 | TCP | Internal |
| Zipkin | 9412 | HTTP | Internal |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Config Server default Git profile now rejects unset, blank, unresolved, and demo `CONFIG_REPO_URI` values at startup. Removing the example remote is not enough: Spring Cloud Config 5.0.4 still boots with an empty URI when `cloneOnStart` is false. Set `CONFIG_REPO_URI` to a reviewed Git URI before starting the default profile; use `native` only for local fixtures.
- Production container builds now use digest-pinned Docker base images while retaining readable Maven/Temurin tags, preventing upstream tag movement from silently changing reviewed build inputs.
- Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented.
- Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Contributor and agent guidance for this repository.
- Never commit secrets, credentials, `.env` files, or private keys.
- Do not commit or push unless a human explicitly asks.
- For workflow edits, run local YAML parsing and `actionlint` on edited files.
- Config Server default Git profile requires `CONFIG_REPO_URI` and must fail closed on blank authority. See `docs/doctoring/config-server-repository-authority.md`.

If any guidance here conflicts with `AGENTS.md`, `AGENTS.md` wins.
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;
}
}
}
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);
Comment on lines +52 to +61

Copy link
Copy Markdown

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

requireExplicitRepository inspects only spring.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.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
}
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() {
Comment thread
seonghobae marked this conversation as resolved.
ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri);
}
}
2 changes: 2 additions & 0 deletions config-server/src/main/resources/META-INF/spring.factories
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
com.xtrmetl.config.ConfigServerRepositoryAuthorityEnvironmentPostProcessor
3 changes: 2 additions & 1 deletion config-server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ spring:
config:
server:
git:
uri: ${CONFIG_REPO_URI:https://github.com/your-repo/config-repo.git}
# Set CONFIG_REPO_URI to a reviewed Git URI before starting the default profile.
uri: ${CONFIG_REPO_URI}

eureka:
client:
Expand Down
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();
}
}
Loading
Loading