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
|
||||
- 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
|
||||
- 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`
|
||||
|
||||
## 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`
|
||||
- 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
|
||||
|
||||
## 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.OidcConfigRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.sql.Array;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -13,47 +14,49 @@ import java.util.Optional;
|
||||
public class PostgresOidcConfigRepository implements OidcConfigRepository {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PostgresOidcConfigRepository(JdbcTemplate jdbc) {
|
||||
public PostgresOidcConfigRepository(JdbcTemplate jdbc, ObjectMapper objectMapper) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<OidcConfig> find() {
|
||||
var results = jdbc.query(
|
||||
"SELECT * FROM oidc_config WHERE config_id = 'default'",
|
||||
List<OidcConfig> results = jdbc.query(
|
||||
"SELECT config_val FROM server_config WHERE config_key = 'oidc'",
|
||||
(rs, rowNum) -> {
|
||||
Array arr = rs.getArray("default_roles");
|
||||
String[] roles = arr != null ? (String[]) arr.getArray() : new String[0];
|
||||
return new OidcConfig(
|
||||
rs.getBoolean("enabled"), rs.getString("issuer_uri"),
|
||||
rs.getString("client_id"), rs.getString("client_secret"),
|
||||
rs.getString("roles_claim"), List.of(roles),
|
||||
rs.getBoolean("auto_signup"), rs.getString("display_name_claim"));
|
||||
String json = rs.getString("config_val");
|
||||
try {
|
||||
return objectMapper.readValue(json, OidcConfig.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to deserialize OIDC config", e);
|
||||
}
|
||||
});
|
||||
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
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("""
|
||||
INSERT INTO oidc_config (config_id, enabled, issuer_uri, client_id, client_secret,
|
||||
roles_claim, default_roles, auto_signup, display_name_claim, updated_at)
|
||||
VALUES ('default', ?, ?, ?, ?, ?, ?, ?, ?, now())
|
||||
ON CONFLICT (config_id) DO UPDATE SET
|
||||
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,
|
||||
INSERT INTO server_config (config_key, config_val, updated_at)
|
||||
VALUES ('oidc', ?::jsonb, now())
|
||||
ON CONFLICT (config_key) DO UPDATE SET
|
||||
config_val = EXCLUDED.config_val,
|
||||
updated_at = now()
|
||||
""",
|
||||
config.enabled(), config.issuerUri(), config.clientId(), config.clientSecret(),
|
||||
config.rolesClaim(), config.defaultRoles().toArray(new String[0]),
|
||||
config.autoSignup(), config.displayNameClaim());
|
||||
json);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public Optional<ThresholdConfig> find() {
|
||||
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) -> {
|
||||
String json = rs.getString("config");
|
||||
String json = rs.getString("config_val");
|
||||
try {
|
||||
return objectMapper.readValue(json, ThresholdConfig.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -46,10 +46,10 @@ public class PostgresThresholdRepository implements ThresholdRepository {
|
||||
}
|
||||
|
||||
jdbc.update("""
|
||||
INSERT INTO admin_thresholds (id, config, updated_by, updated_at)
|
||||
VALUES (1, ?::jsonb, ?, now())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
config = EXCLUDED.config,
|
||||
INSERT INTO server_config (config_key, config_val, updated_by, updated_at)
|
||||
VALUES ('thresholds', ?::jsonb, ?, now())
|
||||
ON CONFLICT (config_key) DO UPDATE SET
|
||||
config_val = EXCLUDED.config_val,
|
||||
updated_by = EXCLUDED.updated_by,
|
||||
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.driver-class-name", () -> "org.postgresql.Driver");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ThresholdAdminControllerIT extends AbstractPostgresIT {
|
||||
void setUp() {
|
||||
adminJwt = securityHelper.adminToken();
|
||||
viewerJwt = securityHelper.viewerToken();
|
||||
jdbcTemplate.update("DELETE FROM server_config WHERE config_key = 'thresholds'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user