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` before JGit runs; `native` must be the only active profile for local fixtures. 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, request-templated, or demo authority before JGit. Use `native` only as the sole active profile 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, request-templated, and retired-demo `CONFIG_REPO_URI` values in an `EnvironmentPostProcessor` before JGit `afterPropertiesSet`. 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 as the sole active profile 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, unresolved, request-templated, or demo authority before JGit. `native` must be the only active profile for local fixtures. 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,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())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match the native profile with Spring's case semantics

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 treat NATIVE as the native profile. The Git backend and @Profile("!native") validator can therefore be created, and with clone-on-start=true JGit 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 👍 / 👎.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Strip trailing slash before matching the demo path

When CONFIG_REPO_URI is https://github.com/your-repo/config-repo.git/, the .git check runs while the path still ends in /; removing the slash afterward leaves /your-repo/config-repo.git, which does not equal the retired path. The processor therefore accepts this equivalent demo destination, and clone-on-start=true can contact GitHub instead of failing before JGit; normalize the trailing slash before stripping .git.

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())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include effective default profiles in the native-only check

When profiles are configured through spring.profiles.default and no explicit active profile is set, Spring uses those defaults for @Profile matching but getActiveProfiles() remains empty. Consequently, spring.profiles.default=native,prod plus any accepted Git URI passes this check while Spring can still select the native backend, bypassing the requirement that native be the sole effective profile; inspect default profiles as well when the active set is empty.

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);
Comment thread
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);
}
}
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
Loading
Loading