Skip to content
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

- Retired abandoned local-auth tables from the default clean-install PostgreSQL bootstrap while preserving the historical `users` / `roles` / `user_roles` schema only as an explicit deprecated compatibility artifact outside the default init path; existing volumes remain untouched and `docs/data/legacy-auth-bootstrap-retirement.md` records consumer uncertainty, rollback, and forward-recovery boundaries.
- 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
34 changes: 34 additions & 0 deletions docker/postgres/compat/legacy_auth_tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- DEPRECATED COMPATIBILITY: historical local-auth bootstrap only.
--
-- This file is intentionally outside docker/postgres/init and is never executed by the default
-- mightyETL clean-install path. Use it only when an existing integration has independently proven
-- that it still depends on the abandoned local users/roles schema. It does not re-enable a shipped
-- /auth/signup or /auth/signin API, and it must not be used as evidence that mightyETL provides a
-- production authentication service.
--
-- Existing PostgreSQL volumes that already contain these tables require no action from this file.
-- Before removing the compatibility artifact entirely, inventory private/external consumers and
-- preserve an explicit rollback/export path.

CREATE TABLE IF NOT EXISTS roles (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL
);

CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL
);

CREATE TABLE IF NOT EXISTS user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (role_id) REFERENCES roles (id)
);

INSERT INTO roles (name)
VALUES ('ROLE_USER'), ('ROLE_ADMIN')
ON CONFLICT (name) DO NOTHING;
29 changes: 6 additions & 23 deletions docker/postgres/init/01_schema.sql
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
-- Local docker-compose schema bootstrap (primary + replica)

CREATE TABLE IF NOT EXISTS roles (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL
);

CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL
);

CREATE TABLE IF NOT EXISTS user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (role_id) REFERENCES roles (id)
);

INSERT INTO roles (name)
VALUES ('ROLE_USER'), ('ROLE_ADMIN')
ON CONFLICT (name) DO NOTHING;
--
-- This default clean-install path intentionally contains only the ETL target schema that
-- protected mightyETL actually uses. Historical local-auth tables were never backed by a shipped
-- authentication API and no longer run implicitly on new installations. Existing PostgreSQL
-- volumes are not modified by this bootstrap change. See docker/postgres/compat/legacy_auth_tables.sql
-- for the explicit deprecated compatibility artifact.

CREATE TABLE IF NOT EXISTS processed_data (
id BIGSERIAL PRIMARY KEY,
Expand Down
69 changes: 69 additions & 0 deletions docs/data/legacy-auth-bootstrap-retirement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Legacy Local-Auth Bootstrap Retirement

**Implementation status:** `active_pr` #155
**Protected baseline:** `develop@622e5e6c3d534f230c390f10e3832efadfc01825`

This runbook defines the bounded compatibility change that stops new default Docker PostgreSQL installations from recreating the abandoned local-auth persistence objects `users`, `roles`, and `user_roles`. It does not claim that those historical objects have been removed from existing PostgreSQL volumes and it does not introduce a replacement authentication API.

## Clean-install boundary

The default Docker initialization path mounts only `docker/postgres/init/` into `/docker-entrypoint-initdb.d`. On this PR, `docker/postgres/init/01_schema.sql` retains the supported `processed_data` target table but no longer creates or seeds `users`, `roles`, or `user_roles`.

A brand-new PostgreSQL data directory therefore starts without the abandoned local-auth tables. This is a clean-install compatibility change, not an in-place migration.

The historical schema is preserved only as the explicit opt-in artifact `docker/postgres/compat/legacy_auth_tables.sql`. That file is deliberately outside the default init directory and must never be mounted into `/docker-entrypoint-initdb.d` by the shipped Compose configuration.

## Existing-volume boundary

Existing PostgreSQL volumes are intentionally untouched by this slice. Docker's PostgreSQL initialization scripts run only for an empty data directory, so a previously initialized volume can continue to contain `users`, `roles`, and `user_roles` after an application upgrade.

That asymmetry is deliberate. Repository and public-code searches found no shipped runtime SQL consumer or local sign-up/sign-in controller, but that evidence does not prove absence of private or external consumers. A destructive DROP or rename would therefore exceed the evidence available to this writer.

Operators must inventory downstream SQL, BI, migration, backup, export, and private integration consumers before deciding that the historical objects can be removed from an existing database.

## Rollback and forward recovery

### Roll back a new clean installation

If a deployment proves that it still requires the historical local-auth objects, apply `docker/postgres/compat/legacy_auth_tables.sql` explicitly to that database under an authorized database identity. Do not move or symlink the compatibility script into the default init directory merely to make all future installations recreate legacy state.

The compatibility script is idempotent for its table creation and seed role names. Its use must be change-controlled because it recreates nonconforming historical database names and should be treated as temporary compatibility debt rather than the target architecture.

### Recover an existing volume

This PR does not drop or rename existing rows, keys, or foreign keys. Rolling back application/container code therefore requires no database reverse migration for an already initialized volume.

If a future migration removes or renames the historical objects, that later change must provide its own data-preserving upgrade and rollback or forward-recovery rehearsal. Do not use this clean-install PR as evidence for that future destructive migration.

### Failure handling

Failure to apply the optional compatibility script is an operator-visible database error and must not be interpreted as successful legacy support. The application must not silently create the historical schema at runtime as a fallback.

## External-consumer uncertainty

The current repository contains no supported local `/auth/signup` or `/auth/signin` implementation that requires these tables. Public organization search did not identify another repository consumer. Those observations narrow the likely blast radius, but public-code search does not prove absence of private or external consumers.

Consequently:

- the default product stops advertising the schema through new clean installations;
- the compatibility artifact remains explicit and opt-in;
- existing volumes are not modified;
- complete retirement of the compatibility artifact requires stronger consumer evidence and a separately reviewed migration decision.

## Security and data handling

The compatibility tables may contain authentication identifiers and historical password material. Their continued presence in an old volume does not make them a supported identity store. Operators should restrict database access, encrypt storage and transport where applicable, avoid copying their contents into telemetry, and apply retention/deletion policy based on the actual deployment purpose and legal obligations.

No raw usernames, password values, table contents, SQL result data, or private-consumer identifiers are required in ordinary mightyETL telemetry for this retirement decision.

## Verification

The PR-level contract verifies that:

- `docker/postgres/init/01_schema.sql` no longer creates or seeds `users`, `roles`, or `user_roles`;
- the default Compose path still mounts only `docker/postgres/init/`;
- `docker/postgres/compat/legacy_auth_tables.sql` remains outside that default path;
- this runbook records the clean-install, existing-volume, rollback/recovery, and external-consumer boundaries;
- `CHANGELOG.md` records the compatibility-impacting clean-install change.

A future complete migration of existing volumes requires a real PostgreSQL upgrade/rollback rehearsal with representative rows and foreign keys before protected merge.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.xtrmetl.etl.documentation;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Guards retirement of the abandoned local-auth bootstrap from the default PostgreSQL schema while
* retaining an explicit opt-in compatibility artifact for installations that still require it.
*/
class LegacyAuthBootstrapRetirementTest {

private static final Path PROJECT_ROOT = projectRoot();

@Test
void defaultBootstrapDoesNotCreateAbandonedLocalAuthObjects() throws IOException {
String bootstrap = read("docker/postgres/init/01_schema.sql");

assertFalse(bootstrap.contains("CREATE TABLE IF NOT EXISTS roles"));
assertFalse(bootstrap.contains("CREATE TABLE IF NOT EXISTS users"));
assertFalse(bootstrap.contains("CREATE TABLE IF NOT EXISTS user_roles"));
assertFalse(bootstrap.contains("INSERT INTO roles"));
assertTrue(bootstrap.contains("CREATE TABLE IF NOT EXISTS processed_data"));
}

@Test
void legacyCompatibilityIsExplicitAndNeverRunsFromDefaultInitDirectory() throws IOException {
String compatibility = read("docker/postgres/compat/legacy_auth_tables.sql");
String compose = read("docker-compose.yml");

assertTrue(compatibility.contains("DEPRECATED COMPATIBILITY"));
assertTrue(compatibility.contains("CREATE TABLE IF NOT EXISTS roles"));
assertTrue(compatibility.contains("CREATE TABLE IF NOT EXISTS users"));
assertTrue(compatibility.contains("CREATE TABLE IF NOT EXISTS user_roles"));
assertTrue(compatibility.contains("INSERT INTO roles"));

assertTrue(compose.contains("./docker/postgres/init:/docker-entrypoint-initdb.d:ro"));
assertFalse(compose.contains("./docker/postgres/compat:/docker-entrypoint-initdb.d"));
assertFalse(compose.contains("legacy_auth_tables.sql:/docker-entrypoint-initdb.d"));

assertFalse(
PROJECT_ROOT.resolve("docker/postgres/compat").normalize()
.startsWith(PROJECT_ROOT.resolve("docker/postgres/init").normalize())
);
}

@Test
void retirementDocumentsCleanInstallExistingVolumeAndRecoveryBoundaries() throws IOException {
String operations = read("docs/data/legacy-auth-bootstrap-retirement.md");
String changelog = read("CHANGELOG.md");

for (String heading : new String[] {
"## Clean-install boundary",
"## Existing-volume boundary",
"## Rollback and forward recovery",
"## External-consumer uncertainty"
}) {
assertTrue(operations.contains(heading), "Retirement operations doc misses " + heading);
}
for (String legacyObject : new String[] {"`users`", "`roles`", "`user_roles`"}) {
assertTrue(operations.contains(legacyObject), "Operations doc misses " + legacyObject);
}
assertTrue(operations.contains("docker/postgres/compat/legacy_auth_tables.sql"));
assertTrue(operations.contains("does not prove absence of private or external consumers"));
assertTrue(
changelog.contains("Retired abandoned local-auth tables from the default clean-install PostgreSQL bootstrap"),
"CHANGELOG must record the compatibility-impacting clean-install change"
);
}

private static String read(String relativePath) throws IOException {
return Files.readString(PROJECT_ROOT.resolve(relativePath), StandardCharsets.UTF_8)
.replace("\r\n", "\n")
.replace("\r", "\n");
}

private static Path projectRoot() {
Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
Path lastPomParent = null;
while (current != null) {
if (Files.exists(current.resolve(".git"))) {
return current;
}
if (Files.exists(current.resolve("pom.xml"))) {
lastPomParent = current;
}
current = current.getParent();
}
if (lastPomParent != null) {
return lastPomParent;
}
throw new IllegalStateException("Could not find project root");
}
}
Loading