gateway/access: wire get_role + is_enabled into HTTP routes

Two of the four #[allow(dead_code)] methods in access.rs were dead
because nothing exposed them externally. access.rs itself is fine —
list_roles, set_role, can_access all have live callers. But get_role
and is_enabled were shaped as public API with no surface to call
them through.

Fix adds two small routes under /access (where the rest of the
access surface lives):

  GET /access/roles/{agent}
    Calls AccessControl::get_role(agent). Returns 404 with a clear
    message when the agent isn't registered so clients distinguish
    "unknown agent" from "access denied." Part of P13-001
    (ops tooling needs per-agent role introspection).

  GET /access/enabled
    Calls AccessControl::is_enabled(). Returns {"enabled": bool}.
    Dashboards + ops tooling poll this to confirm auth posture of
    the running gateway — distinct from /health which answers
    "is the process up" vs "is access enforcement on."

#[allow(dead_code)] removed from both methods — they have live
callers now via these routes, the linter will enforce that going
forward.

Still #[allow(dead_code)] on access.rs: masked_fields + log_query.
Both need cross-crate wiring:
  - masked_fields wants the agent's role + query response columns,
    called in response shaping (queryd returning to gateway path)
  - log_query wants post-execution audit, called after every SQL
    execution on the gateway boundary
Both are P13-001 phase 2 work — need AgentIdentity plumbed through
the /query nested router before the call sites make sense. Flagged
for follow-up.

Workspace warnings still at 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-24 14:02:01 -05:00
parent 91a38dc20b
commit fee094f653
2 changed files with 25 additions and 4 deletions

View File

@ -93,8 +93,7 @@ impl AccessControl {
self.roles.write().await.insert(role.agent_name.clone(), role); self.roles.write().await.insert(role.agent_name.clone(), role);
} }
/// Get an agent's role. /// Get an agent's role. Called by `GET /access/roles/{agent}`.
#[allow(dead_code)]
pub async fn get_role(&self, agent: &str) -> Option<AgentRole> { pub async fn get_role(&self, agent: &str) -> Option<AgentRole> {
self.roles.read().await.get(agent).cloned() self.roles.read().await.get(agent).cloned()
} }
@ -152,7 +151,9 @@ impl AccessControl {
log[start..].iter().rev().cloned().collect() log[start..].iter().rev().cloned().collect()
} }
#[allow(dead_code)] /// Reports whether access-control enforcement is active.
/// Called by `GET /access/enabled` — ops tooling / dashboards poll
/// this to confirm the auth posture of the running gateway.
pub fn is_enabled(&self) -> bool { pub fn is_enabled(&self) -> bool {
self.enabled self.enabled
} }

View File

@ -1,6 +1,6 @@
use axum::{ use axum::{
Json, Router, Json, Router,
extract::{Query, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
routing::{get, post}, routing::{get, post},
@ -13,6 +13,12 @@ pub fn router(ac: AccessControl) -> Router {
Router::new() Router::new()
.route("/roles", get(list_roles)) .route("/roles", get(list_roles))
.route("/roles", post(set_role)) .route("/roles", post(set_role))
// Scrum iter 11 / P13-001 finding: get_role was #[allow(dead_code)]
// because nothing called it — dead until exposed. Route activates it.
// Returns 404 when the agent isn't registered so clients can
// distinguish "missing role" from "access denied."
.route("/roles/{agent}", get(get_role))
.route("/enabled", get(enabled_status))
.route("/audit", get(query_audit)) .route("/audit", get(query_audit))
.route("/check", post(check_access)) .route("/check", post(check_access))
.with_state(ac) .with_state(ac)
@ -60,3 +66,17 @@ async fn check_access(
"allowed": allowed, "allowed": allowed,
})) }))
} }
async fn get_role(
State(ac): State<AccessControl>,
Path(agent): Path<String>,
) -> impl IntoResponse {
match ac.get_role(&agent).await {
Some(role) => Ok(Json(role)),
None => Err((StatusCode::NOT_FOUND, format!("no role registered for agent '{agent}'"))),
}
}
async fn enabled_status(State(ac): State<AccessControl>) -> impl IntoResponse {
Json(serde_json::json!({ "enabled": ac.is_enabled() }))
}