use axum::{extract::State, http::StatusCode, routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use super::middleware::AuthenticatedPrincipal;
use super::principal::{AuthMethod, PrincipalType};
use super::{Action, Authorizer, Resource};
use crate::app::AppState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthContextResponse {
pub principal: PrincipalInfo,
pub capabilities: Capabilities,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub features: HashMap<String, bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrincipalInfo {
pub id: String,
pub name: String,
pub principal_type: String,
pub tenant_id: String,
pub roles: Vec<String>,
pub auth_method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Capabilities {
pub catalog: ActionSet,
pub namespaces: ActionSet,
pub tables: ActionSet,
pub is_admin: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActionSet {
pub list: bool,
pub read: bool,
pub create: bool,
pub update: bool,
pub delete: bool,
pub manage: bool,
}
impl ActionSet {
pub fn all() -> Self {
Self {
list: true,
read: true,
create: true,
update: true,
delete: true,
manage: true,
}
}
pub fn read_only() -> Self {
Self {
list: true,
read: true,
..Default::default()
}
}
}
pub async fn get_auth_context(
State(app_state): State<AppState>,
AuthenticatedPrincipal(principal): AuthenticatedPrincipal,
) -> Result<Json<AuthContextResponse>, (StatusCode, &'static str)> {
let principal_type = match principal.principal_type() {
PrincipalType::User => "user",
PrincipalType::Service => "service",
PrincipalType::ApiKey => "api_key",
PrincipalType::System => "system",
PrincipalType::Anonymous => "anonymous",
};
let auth_method = match principal.auth_method() {
AuthMethod::ApiKey => "api_key",
AuthMethod::Bearer => "jwt",
AuthMethod::MutualTls => "mtls",
AuthMethod::Basic => "basic",
AuthMethod::Internal => "internal",
AuthMethod::None => "none",
};
let expires_at = principal.expires_at().map(|dt| dt.to_rfc3339());
let principal_info = PrincipalInfo {
id: principal.id().to_string(),
name: principal.name().to_string(),
principal_type: principal_type.to_string(),
tenant_id: principal.tenant_id().to_string(),
roles: principal.roles().iter().cloned().collect(),
auth_method: auth_method.to_string(),
expires_at,
};
let capabilities = evaluate_capabilities(&principal, &app_state).await;
let features = HashMap::new();
Ok(Json(AuthContextResponse {
principal: principal_info,
capabilities,
features,
}))
}
async fn evaluate_capabilities(principal: &super::Principal, app_state: &AppState) -> Capabilities {
let tenant_id = principal.tenant_id();
let is_admin = principal.is_system()
|| principal.has_role("admin")
|| principal.has_role("system")
|| principal.has_role("catalog-admin");
if is_admin {
return Capabilities {
catalog: ActionSet::all(),
namespaces: ActionSet::all(),
tables: ActionSet::all(),
is_admin: true,
};
}
let catalog = evaluate_resource_capabilities(
principal,
&app_state.authorizer,
Resource::catalog(tenant_id),
)
.await;
let namespaces = evaluate_resource_capabilities(
principal,
&app_state.authorizer,
Resource::namespace(tenant_id, vec!["*"]),
)
.await;
let tables = evaluate_resource_capabilities(
principal,
&app_state.authorizer,
Resource::table(tenant_id, vec!["*"], "*"),
)
.await;
Capabilities {
catalog,
namespaces,
tables,
is_admin: false,
}
}
async fn evaluate_resource_capabilities(
principal: &super::Principal,
authorizer: &Arc<dyn Authorizer>,
resource: Resource,
) -> ActionSet {
use super::AuthzContext;
let mut action_set = ActionSet::default();
let ctx = AuthzContext::new(principal.clone(), resource.clone(), Action::List);
if authorizer.authorize(&ctx).await.is_allowed() {
action_set.list = true;
}
let ctx = AuthzContext::new(principal.clone(), resource.clone(), Action::Read);
if authorizer.authorize(&ctx).await.is_allowed() {
action_set.read = true;
}
let ctx = AuthzContext::new(principal.clone(), resource.clone(), Action::Create);
if authorizer.authorize(&ctx).await.is_allowed() {
action_set.create = true;
}
let ctx = AuthzContext::new(principal.clone(), resource.clone(), Action::Update);
if authorizer.authorize(&ctx).await.is_allowed() {
action_set.update = true;
}
let ctx = AuthzContext::new(principal.clone(), resource.clone(), Action::Delete);
if authorizer.authorize(&ctx).await.is_allowed() {
action_set.delete = true;
}
let ctx = AuthzContext::new(principal.clone(), resource.clone(), Action::Manage);
if authorizer.authorize(&ctx).await.is_allowed() {
action_set.manage = true;
}
action_set
}
pub fn create_routes(app_state: AppState) -> Router {
Router::new()
.route("/auth/context", get(get_auth_context))
.with_state(app_state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_set_all() {
let actions = ActionSet::all();
assert!(actions.list);
assert!(actions.read);
assert!(actions.create);
assert!(actions.update);
assert!(actions.delete);
assert!(actions.manage);
}
#[test]
fn test_action_set_read_only() {
let actions = ActionSet::read_only();
assert!(actions.list);
assert!(actions.read);
assert!(!actions.create);
assert!(!actions.update);
assert!(!actions.delete);
assert!(!actions.manage);
}
#[test]
fn test_action_set_default() {
let actions = ActionSet::default();
assert!(!actions.list);
assert!(!actions.read);
assert!(!actions.create);
assert!(!actions.update);
assert!(!actions.delete);
assert!(!actions.manage);
}
#[test]
fn test_principal_info_serialization() {
let info = PrincipalInfo {
id: "user123".to_string(),
name: "Test User".to_string(),
principal_type: "user".to_string(),
tenant_id: "tenant1".to_string(),
roles: vec!["reader".to_string(), "writer".to_string()],
auth_method: "api_key".to_string(),
expires_at: None,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("user123"));
assert!(json.contains("Test User"));
assert!(json.contains("reader"));
}
#[test]
fn test_auth_context_response_serialization() {
let response = AuthContextResponse {
principal: PrincipalInfo {
id: "user123".to_string(),
name: "Test User".to_string(),
principal_type: "user".to_string(),
tenant_id: "tenant1".to_string(),
roles: vec!["admin".to_string()],
auth_method: "jwt".to_string(),
expires_at: Some("2026-01-25T12:00:00Z".to_string()),
},
capabilities: Capabilities {
catalog: ActionSet::all(),
namespaces: ActionSet::all(),
tables: ActionSet::all(),
is_admin: true,
},
features: HashMap::new(),
};
let json = serde_json::to_string_pretty(&response).unwrap();
assert!(json.contains("user123"));
assert!(json.contains("is_admin"));
assert!(json.contains("expires_at"));
}
}