use super::*;
#[derive(Clone, Debug, Deserialize)]
struct ConsoleApiLoginPayload {
principal_id: String,
shared_secret: String,
#[serde(default = "default_console_identity_provider")]
identity_provider: String,
}
#[derive(Clone, Debug, Deserialize)]
struct ConsoleApiLegalHoldPayload {
enabled: bool,
#[serde(default)]
reason: String,
}
fn default_console_identity_provider() -> String {
"custom-shared-secret".to_string()
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct ConsoleApiRequest {
pub method: String,
pub path: String,
pub access_key_id: Option<String>,
pub query: BTreeMap<String, String>,
pub body: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ConsoleApiResponse {
pub status: u16,
pub content_type: String,
pub request_id: String,
pub body: serde_json::Value,
}
impl BucketWarden {
pub fn handle_console_api(
&mut self,
request: ConsoleApiRequest,
) -> Result<ConsoleApiResponse, RuntimeError> {
let request_id = self.next_console_api_request_id();
match self.dispatch_console_api(&request, &request_id) {
Ok(body) => Ok(ConsoleApiResponse::json(200, request_id, body)),
Err(error) => {
let envelope = self.console_api_error_envelope(&error, Some(&request_id));
Ok(ConsoleApiResponse::json(
envelope.status,
request_id,
serde_json::to_value(envelope).map_err(RuntimeError::SnapshotSerialize)?,
))
}
}
}
fn dispatch_console_api(
&mut self,
request: &ConsoleApiRequest,
_request_id: &str,
) -> Result<serde_json::Value, RuntimeError> {
let method = request.method.as_str();
let owned_path = canonical_console_api_path(request.path.trim_end_matches('/'));
let path = owned_path.as_str();
if method == "POST" && path == "/ui/api/login" {
let login = serde_json::from_value::<ConsoleApiLoginPayload>(
request.body.clone().unwrap_or_default(),
)
.map_err(RuntimeError::SnapshotDeserialize)?;
if login.identity_provider != "custom-shared-secret" {
return Err(RuntimeError::InvalidListParameter {
name: "identity_provider".to_string(),
value: login.identity_provider,
});
}
return json(self.console_api_login(ConsoleApiLoginRequest {
principal_id: login.principal_id,
shared_secret: login.shared_secret,
})?);
}
if method == "GET" && path == "/ui/api/identity-providers" {
return json(self.console_api_identity_providers());
}
let access_key_id = request.access_key_id.as_deref().ok_or_else(|| {
RuntimeError::InvalidToken("missing console API access key".to_string())
})?;
match (method, path) {
("GET", "/ui/api/current-user") => json(self.console_api_current_user(access_key_id)?),
("POST", "/ui/api/logout") => {
self.console_api_logout(access_key_id)?;
json(serde_json::json!({"logged_out": true}))
}
("GET", "/ui/api/overview") => json(self.console_api_overview(access_key_id)?),
("GET", "/ui/api/reports") => json(self.console_api_reports_dashboard(access_key_id)?),
("GET", "/ui/api/reports/health") => {
json(self.console_api_health_report(access_key_id)?)
}
("GET", "/ui/api/reports/config") => {
Ok(self.console_api_redacted_configuration_report(access_key_id)?)
}
("GET", "/ui/api/reports/storage") => {
json(self.console_api_storage_report(access_key_id)?)
}
("GET", "/ui/api/reports/governance") => {
Ok(self.console_api_governance_report(access_key_id)?)
}
("GET", "/ui/api/reports/incident") => {
json(self.console_api_incident_report(access_key_id)?)
}
("GET", "/ui/api/buckets") => {
json(self.console_api_bucket_list(access_key_id, list_query(request))?)
}
("GET", "/ui/api/audit") => {
json(self.console_api_audit_events(access_key_id, audit_query(request))?)
}
("GET", "/ui/api/evidence") => {
json(self.console_api_evidence_list(access_key_id, list_query(request))?)
}
("GET", "/ui/api/evidence-export") => {
json(self.console_api_evidence_export(access_key_id)?)
}
("GET", "/ui/api/admin") => json(self.console_api_admin_summary(access_key_id)?),
("GET", "/ui/api/preferences") => {
json(self.console_api_preferences_read(access_key_id)?)
}
("PUT", "/ui/api/preferences") => {
let preferences = serde_json::from_value::<ConsoleApiPreferences>(
request.body.clone().unwrap_or_default(),
)
.map_err(RuntimeError::SnapshotDeserialize)?;
json(self.console_api_preferences_write(access_key_id, preferences)?)
}
_ => self.dispatch_resource_route(access_key_id, method, path, request),
}
}
fn dispatch_resource_route(
&mut self,
access_key_id: &str,
method: &str,
path: &str,
request: &ConsoleApiRequest,
) -> Result<serde_json::Value, RuntimeError> {
let parts = path
.split('/')
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
match parts.as_slice() {
["ui", "api", "buckets", bucket] if method == "GET" => {
json(self.console_api_bucket_detail(access_key_id, bucket)?)
}
["ui", "api", "buckets", bucket, "objects"] if method == "GET" => {
json(self.console_api_object_list(access_key_id, bucket, list_query(request))?)
}
["ui", "api", "admin", "users", principal_id] if method == "GET" => {
json(self.console_api_user_detail(access_key_id, principal_id)?)
}
["ui", "api", "object-governance"] if method == "GET" => {
let (bucket, key, version_id) = object_query(request)?;
json(self.console_api_object_governance_summary(
access_key_id,
&bucket,
&key,
version_id.as_deref(),
)?)
}
["ui", "api", "object-lock-retention"] if method == "GET" => {
let (bucket, key, version_id) = object_query(request)?;
json(self.console_api_object_lock_retention(
access_key_id,
&bucket,
&key,
version_id.as_deref(),
)?)
}
["ui", "api", "legal-hold"] if method == "GET" => {
let (bucket, key, version_id) = object_query(request)?;
json(self.console_api_legal_hold(
access_key_id,
&bucket,
&key,
version_id.as_deref(),
)?)
}
["ui", "api", "legal-hold"] if method == "POST" => {
let payload = serde_json::from_value::<ConsoleApiLegalHoldPayload>(
request.body.clone().unwrap_or_default(),
)
.map_err(RuntimeError::SnapshotDeserialize)?;
if payload.reason.trim().is_empty() {
return Err(RuntimeError::InvalidListParameter {
name: "reason".to_string(),
value: String::new(),
});
}
let (bucket, key, version_id) = object_query(request)?;
json(self.console_api_put_legal_hold(
access_key_id,
&bucket,
&key,
version_id.as_deref(),
payload.enabled,
payload.reason,
)?)
}
["ui", "api", "object-versions"] if method == "GET" => {
let (bucket, key, _) = object_query(request)?;
json(self.console_api_object_version_history(
access_key_id,
&bucket,
&key,
list_query(request),
)?)
}
_ => Err(RuntimeError::InvalidToken(format!(
"unsupported console API route {method} {path}"
))),
}
}
}
fn canonical_console_api_path(path: &str) -> String {
if path == "/api/v1" {
return "/ui/api".to_string();
}
path.strip_prefix("/api/v1/")
.map(|suffix| format!("/ui/api/{suffix}"))
.unwrap_or_else(|| path.to_string())
}
impl ConsoleApiResponse {
fn json(status: u16, request_id: String, body: serde_json::Value) -> Self {
Self {
status,
content_type: "application/json".to_string(),
request_id,
body,
}
}
}
fn json(value: impl Serialize) -> Result<serde_json::Value, RuntimeError> {
serde_json::to_value(value).map_err(RuntimeError::SnapshotSerialize)
}
fn list_query(request: &ConsoleApiRequest) -> ConsoleApiListQuery {
ConsoleApiListQuery {
limit: parse_usize(&request.query, "limit"),
offset: parse_usize(&request.query, "offset"),
prefix: request.query.get("prefix").cloned(),
q: request.query.get("q").cloned(),
sort: request.query.get("sort").cloned(),
}
}
fn audit_query(request: &ConsoleApiRequest) -> ConsoleApiAuditQuery {
ConsoleApiAuditQuery {
limit: parse_usize(&request.query, "limit"),
offset: parse_usize(&request.query, "offset"),
q: request.query.get("q").cloned(),
sort: request.query.get("sort").cloned(),
subject: request.query.get("subject").cloned(),
action: request.query.get("action").cloned(),
resource: request.query.get("resource").cloned(),
outcome: request.query.get("outcome").cloned(),
min_sequence: parse_u64(&request.query, "min_sequence"),
max_sequence: parse_u64(&request.query, "max_sequence"),
}
}
fn object_query(
request: &ConsoleApiRequest,
) -> Result<(String, String, Option<String>), RuntimeError> {
let bucket = request
.query
.get("bucket")
.cloned()
.ok_or_else(|| missing_query("bucket"))?;
let key = request
.query
.get("key")
.cloned()
.ok_or_else(|| missing_query("key"))?;
Ok((bucket, key, request.query.get("versionId").cloned()))
}
fn parse_usize(query: &BTreeMap<String, String>, key: &str) -> Option<usize> {
query.get(key).and_then(|value| value.parse().ok())
}
fn parse_u64(query: &BTreeMap<String, String>, key: &str) -> Option<u64> {
query.get(key).and_then(|value| value.parse().ok())
}
fn missing_query(key: &str) -> RuntimeError {
RuntimeError::InvalidListParameter {
name: key.to_string(),
value: String::new(),
}
}