refactor: consolidate oidc_config and admin_thresholds into generic server_config table
All checks were successful
CI / build (push) Successful in 1m19s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 42s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 34s
CI / build (pull_request) Successful in 1m23s
CI / cleanup-branch (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
All checks were successful
CI / build (push) Successful in 1m19s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 42s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 34s
CI / build (pull_request) Successful in 1m23s
CI / cleanup-branch (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
Single JSONB key-value table replaces two singleton config tables, making future config types trivial to add. Also fixes pre-existing IT failures: Flyway URL not overridden by Testcontainers, threshold test ordering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
|
|||||||
- Maintains agent instance registry with states: LIVE → STALE → DEAD
|
- Maintains agent instance registry with states: LIVE → STALE → DEAD
|
||||||
- Storage: PostgreSQL (TimescaleDB) for structured data, OpenSearch for full-text search
|
- Storage: PostgreSQL (TimescaleDB) for structured data, OpenSearch for full-text search
|
||||||
- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing, bootstrap token for registration
|
- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing, bootstrap token for registration
|
||||||
- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API, stored in database (`oidc_config` table)
|
- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API, stored in database (`server_config` table)
|
||||||
- User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users`
|
- User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users`
|
||||||
|
|
||||||
## CI/CD & Deployment
|
## CI/CD & Deployment
|
||||||
@@ -56,3 +56,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
|
|||||||
- Secrets managed in CI deploy step (idempotent `--dry-run=client | kubectl apply`): `cameleer-auth`, `postgres-credentials`, `opensearch-credentials`
|
- Secrets managed in CI deploy step (idempotent `--dry-run=client | kubectl apply`): `cameleer-auth`, `postgres-credentials`, `opensearch-credentials`
|
||||||
- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready`, OpenSearch uses `/_cluster/health`
|
- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready`, OpenSearch uses `/_cluster/health`
|
||||||
- Docker build uses buildx registry cache + `--provenance=false` for Gitea compatibility
|
- Docker build uses buildx registry cache + `--provenance=false` for Gitea compatibility
|
||||||
|
|
||||||
|
## Disabled Skills
|
||||||
|
|
||||||
|
- Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands.
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package com.cameleer3.server.app.storage;
|
|||||||
|
|
||||||
import com.cameleer3.server.core.security.OidcConfig;
|
import com.cameleer3.server.core.security.OidcConfig;
|
||||||
import com.cameleer3.server.core.security.OidcConfigRepository;
|
import com.cameleer3.server.core.security.OidcConfigRepository;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.sql.Array;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -13,47 +14,49 @@ import java.util.Optional;
|
|||||||
public class PostgresOidcConfigRepository implements OidcConfigRepository {
|
public class PostgresOidcConfigRepository implements OidcConfigRepository {
|
||||||
|
|
||||||
private final JdbcTemplate jdbc;
|
private final JdbcTemplate jdbc;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public PostgresOidcConfigRepository(JdbcTemplate jdbc) {
|
public PostgresOidcConfigRepository(JdbcTemplate jdbc, ObjectMapper objectMapper) {
|
||||||
this.jdbc = jdbc;
|
this.jdbc = jdbc;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<OidcConfig> find() {
|
public Optional<OidcConfig> find() {
|
||||||
var results = jdbc.query(
|
List<OidcConfig> results = jdbc.query(
|
||||||
"SELECT * FROM oidc_config WHERE config_id = 'default'",
|
"SELECT config_val FROM server_config WHERE config_key = 'oidc'",
|
||||||
(rs, rowNum) -> {
|
(rs, rowNum) -> {
|
||||||
Array arr = rs.getArray("default_roles");
|
String json = rs.getString("config_val");
|
||||||
String[] roles = arr != null ? (String[]) arr.getArray() : new String[0];
|
try {
|
||||||
return new OidcConfig(
|
return objectMapper.readValue(json, OidcConfig.class);
|
||||||
rs.getBoolean("enabled"), rs.getString("issuer_uri"),
|
} catch (JsonProcessingException e) {
|
||||||
rs.getString("client_id"), rs.getString("client_secret"),
|
throw new RuntimeException("Failed to deserialize OIDC config", e);
|
||||||
rs.getString("roles_claim"), List.of(roles),
|
}
|
||||||
rs.getBoolean("auto_signup"), rs.getString("display_name_claim"));
|
|
||||||
});
|
});
|
||||||
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(OidcConfig config) {
|
public void save(OidcConfig config) {
|
||||||
|
String json;
|
||||||
|
try {
|
||||||
|
json = objectMapper.writeValueAsString(config);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException("Failed to serialize OIDC config", e);
|
||||||
|
}
|
||||||
|
|
||||||
jdbc.update("""
|
jdbc.update("""
|
||||||
INSERT INTO oidc_config (config_id, enabled, issuer_uri, client_id, client_secret,
|
INSERT INTO server_config (config_key, config_val, updated_at)
|
||||||
roles_claim, default_roles, auto_signup, display_name_claim, updated_at)
|
VALUES ('oidc', ?::jsonb, now())
|
||||||
VALUES ('default', ?, ?, ?, ?, ?, ?, ?, ?, now())
|
ON CONFLICT (config_key) DO UPDATE SET
|
||||||
ON CONFLICT (config_id) DO UPDATE SET
|
config_val = EXCLUDED.config_val,
|
||||||
enabled = EXCLUDED.enabled, issuer_uri = EXCLUDED.issuer_uri,
|
|
||||||
client_id = EXCLUDED.client_id, client_secret = EXCLUDED.client_secret,
|
|
||||||
roles_claim = EXCLUDED.roles_claim, default_roles = EXCLUDED.default_roles,
|
|
||||||
auto_signup = EXCLUDED.auto_signup, display_name_claim = EXCLUDED.display_name_claim,
|
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
""",
|
""",
|
||||||
config.enabled(), config.issuerUri(), config.clientId(), config.clientSecret(),
|
json);
|
||||||
config.rolesClaim(), config.defaultRoles().toArray(new String[0]),
|
|
||||||
config.autoSignup(), config.displayNameClaim());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete() {
|
public void delete() {
|
||||||
jdbc.update("DELETE FROM oidc_config WHERE config_id = 'default'");
|
jdbc.update("DELETE FROM server_config WHERE config_key = 'oidc'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ public class PostgresThresholdRepository implements ThresholdRepository {
|
|||||||
@Override
|
@Override
|
||||||
public Optional<ThresholdConfig> find() {
|
public Optional<ThresholdConfig> find() {
|
||||||
List<ThresholdConfig> results = jdbc.query(
|
List<ThresholdConfig> results = jdbc.query(
|
||||||
"SELECT config FROM admin_thresholds WHERE id = 1",
|
"SELECT config_val FROM server_config WHERE config_key = 'thresholds'",
|
||||||
(rs, rowNum) -> {
|
(rs, rowNum) -> {
|
||||||
String json = rs.getString("config");
|
String json = rs.getString("config_val");
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(json, ThresholdConfig.class);
|
return objectMapper.readValue(json, ThresholdConfig.class);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
@@ -46,10 +46,10 @@ public class PostgresThresholdRepository implements ThresholdRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jdbc.update("""
|
jdbc.update("""
|
||||||
INSERT INTO admin_thresholds (id, config, updated_by, updated_at)
|
INSERT INTO server_config (config_key, config_val, updated_by, updated_at)
|
||||||
VALUES (1, ?::jsonb, ?, now())
|
VALUES ('thresholds', ?::jsonb, ?, now())
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (config_key) DO UPDATE SET
|
||||||
config = EXCLUDED.config,
|
config_val = EXCLUDED.config_val,
|
||||||
updated_by = EXCLUDED.updated_by,
|
updated_by = EXCLUDED.updated_by,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
""",
|
""",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- Consolidate oidc_config + admin_thresholds → server_config
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
CREATE TABLE server_config (
|
||||||
|
config_key TEXT PRIMARY KEY,
|
||||||
|
config_val JSONB NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_by TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrate existing oidc_config row (if any)
|
||||||
|
INSERT INTO server_config (config_key, config_val, updated_at)
|
||||||
|
SELECT 'oidc',
|
||||||
|
jsonb_build_object(
|
||||||
|
'enabled', enabled,
|
||||||
|
'issuerUri', issuer_uri,
|
||||||
|
'clientId', client_id,
|
||||||
|
'clientSecret', client_secret,
|
||||||
|
'rolesClaim', roles_claim,
|
||||||
|
'defaultRoles', to_jsonb(default_roles),
|
||||||
|
'autoSignup', auto_signup,
|
||||||
|
'displayNameClaim', display_name_claim
|
||||||
|
),
|
||||||
|
updated_at
|
||||||
|
FROM oidc_config
|
||||||
|
WHERE config_id = 'default';
|
||||||
|
|
||||||
|
-- Migrate existing admin_thresholds row (if any)
|
||||||
|
INSERT INTO server_config (config_key, config_val, updated_at, updated_by)
|
||||||
|
SELECT 'thresholds', config, updated_at, updated_by
|
||||||
|
FROM admin_thresholds
|
||||||
|
WHERE id = 1;
|
||||||
|
|
||||||
|
DROP TABLE oidc_config;
|
||||||
|
DROP TABLE admin_thresholds;
|
||||||
@@ -42,6 +42,9 @@ public abstract class AbstractPostgresIT {
|
|||||||
registry.add("spring.datasource.password", postgres::getPassword);
|
registry.add("spring.datasource.password", postgres::getPassword);
|
||||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||||
registry.add("spring.flyway.enabled", () -> "true");
|
registry.add("spring.flyway.enabled", () -> "true");
|
||||||
|
registry.add("spring.flyway.url", postgres::getJdbcUrl);
|
||||||
|
registry.add("spring.flyway.user", postgres::getUsername);
|
||||||
|
registry.add("spring.flyway.password", postgres::getPassword);
|
||||||
registry.add("opensearch.url", opensearch::getHttpHostAddress);
|
registry.add("opensearch.url", opensearch::getHttpHostAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class ThresholdAdminControllerIT extends AbstractPostgresIT {
|
|||||||
void setUp() {
|
void setUp() {
|
||||||
adminJwt = securityHelper.adminToken();
|
adminJwt = securityHelper.adminToken();
|
||||||
viewerJwt = securityHelper.viewerToken();
|
viewerJwt = securityHelper.viewerToken();
|
||||||
|
jdbcTemplate.update("DELETE FROM server_config WHERE config_key = 'thresholds'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user