lakehouse/crates/gateway/src/access_service.rs
root fee094f653 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>
2026-04-24 14:02:01 -05:00

83 lines
2.3 KiB
Rust

use axum::{
Json, Router,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
};
use serde::Deserialize;
use crate::access::{AccessControl, AgentRole};
pub fn router(ac: AccessControl) -> Router {
Router::new()
.route("/roles", get(list_roles))
.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("/check", post(check_access))
.with_state(ac)
}
async fn list_roles(State(ac): State<AccessControl>) -> impl IntoResponse {
Json(ac.list_roles().await)
}
async fn set_role(
State(ac): State<AccessControl>,
Json(role): Json<AgentRole>,
) -> impl IntoResponse {
let name = role.agent_name.clone();
ac.set_role(role).await;
(StatusCode::OK, format!("role set: {name}"))
}
#[derive(Deserialize)]
struct AuditQuery {
limit: Option<usize>,
}
async fn query_audit(
State(ac): State<AccessControl>,
Query(q): Query<AuditQuery>,
) -> impl IntoResponse {
Json(ac.recent_audit(q.limit.unwrap_or(50)).await)
}
#[derive(Deserialize)]
struct CheckRequest {
agent: String,
sensitivity: shared::types::Sensitivity,
}
async fn check_access(
State(ac): State<AccessControl>,
Json(req): Json<CheckRequest>,
) -> impl IntoResponse {
let allowed = ac.can_access(&req.agent, &req.sensitivity).await;
Json(serde_json::json!({
"agent": req.agent,
"sensitivity": req.sensitivity,
"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() }))
}