mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-21 01:08:41 +02:00
feat(capabilities): honest secret-presence downgrade + admin features view [operator §4]
Make /capabilities fail-closed and add the admin features surface. - Secret-presence downgrade (§4.2): a chain whose flag is on but whose source is unconfigured (btc/ltc no electrum URL, xmr/wow no LWS admin key, grin no owner secret) now reports enabled:false — so /capabilities never advertises a chain that would 404, and a missing-secret downgrade is indistinguishable from a deliberately-off chain. grin_relay requires serviceable Grin; nostr_identity requires PUBLIC_API_URL. effective_capabilities() is the single explicit mapper. - contract_version stamp on the public document (clients soft-notice a higher value, ignore unknown additive keys). - GET /admin/features (admin_guard): the resolved effective state PLUS admin-only downgrade reasons (the public surface hides the reason, per §4.3). - Golden test: the public capabilities JSON, built from a fully-secret-seeded config, contains no secret value (asserts no "integration-test-" substring). openapi regenerated (contract_version). Admin features view stays off the public OpenAPI. Deferred (documented): runtime mutation — PUT /admin/features + a DB feature- override table + resolve() precedence + Arc hot-swap + CLI `features set`. Flags are env/config-driven for v1; runtime tuning (and its fleet propagation, itself a deferred LB decision) is a later addition.
This commit is contained in:
@@ -651,6 +651,93 @@ pub async fn admin_keys_rotate(
|
||||
}))
|
||||
}
|
||||
|
||||
// ── features (read-only view) ────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Downgrade {
|
||||
pub feature: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AdminFeaturesResponse {
|
||||
pub effective: crate::api::capabilities::CapabilitiesResponse,
|
||||
/// Flags that are ON in config but serve as disabled (missing secret/URL).
|
||||
/// Exposed only to the admin — the public capabilities hides the reason.
|
||||
pub downgrades: Vec<Downgrade>,
|
||||
}
|
||||
|
||||
/// Admin-only view of the resolved feature state + why anything is downgraded.
|
||||
/// (Runtime mutation — PUT /admin/features + DB override — is deferred; flags are
|
||||
/// env/config-driven for now.)
|
||||
#[instrument(skip(state, headers))]
|
||||
pub async fn admin_features(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<AdminFeaturesResponse>, AppError> {
|
||||
admin_guard(&state, &headers).await?;
|
||||
let cfg = &state.config;
|
||||
let effective = crate::api::capabilities::effective_capabilities(cfg);
|
||||
|
||||
let mut downgrades = Vec::new();
|
||||
let mut note = |on: bool, eff: bool, feature: &str, reason: &str| {
|
||||
if on && !eff {
|
||||
downgrades.push(Downgrade {
|
||||
feature: feature.into(),
|
||||
reason: reason.into(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let c = &cfg.features.chains;
|
||||
note(
|
||||
c.btc,
|
||||
effective.chains.btc.enabled,
|
||||
"btc",
|
||||
"BTC_ELECTRUM_URL/fallbacks unset",
|
||||
);
|
||||
note(
|
||||
c.ltc,
|
||||
effective.chains.ltc.enabled,
|
||||
"ltc",
|
||||
"LTC_ELECTRUM_URL/fallbacks unset",
|
||||
);
|
||||
note(
|
||||
c.xmr,
|
||||
effective.chains.xmr.enabled,
|
||||
"xmr",
|
||||
"XMR_LWS_ADMIN_KEY unset",
|
||||
);
|
||||
note(
|
||||
c.wow,
|
||||
effective.chains.wow.enabled,
|
||||
"wow",
|
||||
"WOW_LWS_ADMIN_KEY unset",
|
||||
);
|
||||
note(
|
||||
c.grin,
|
||||
effective.chains.grin.enabled,
|
||||
"grin",
|
||||
"GRIN_OWNER_API_SECRET unset",
|
||||
);
|
||||
note(
|
||||
cfg.features.grin_relay,
|
||||
effective.features.grin_relay,
|
||||
"grin_relay",
|
||||
"Grin chain not serviceable",
|
||||
);
|
||||
note(
|
||||
cfg.features.nostr_identity,
|
||||
effective.features.nostr_identity,
|
||||
"nostr_identity",
|
||||
"PUBLIC_API_URL unset",
|
||||
);
|
||||
|
||||
Ok(Json(AdminFeaturesResponse {
|
||||
effective,
|
||||
downgrades,
|
||||
}))
|
||||
}
|
||||
|
||||
// ── router ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Admin-plane routes. Mounted by [`crate::admin_router`] on the loopback socket.
|
||||
@@ -661,6 +748,7 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
.route("/admin/auth/refresh", post(admin_refresh))
|
||||
.route("/admin/auth/logout", post(admin_logout))
|
||||
.route("/admin/me", get(admin_me))
|
||||
.route("/admin/features", get(admin_features))
|
||||
.route("/admin/keys", post(admin_keys_add).get(admin_keys_list))
|
||||
.route("/admin/keys/:id", delete(admin_keys_revoke))
|
||||
.route("/admin/keys/:id/rotate", post(admin_keys_rotate))
|
||||
|
||||
Reference in New Issue
Block a user