//! REST HTTP API handlers for IAM //! //! Implements REST endpoints as specified in T050.S4: //! - POST /api/v1/auth/token - Issue token //! - POST /api/v1/auth/verify - Verify token //! - GET /api/v1/users - List users //! - POST /api/v1/users - Create user //! - GET /api/v1/projects - List projects //! - POST /api/v1/projects - Create project //! - GET /health - Health check use axum::{ extract::State, http::StatusCode, routing::{get, post}, Json, Router, }; use iam_client::client::{IamClient, IamClientConfig}; use iam_types::{Principal, PrincipalKind, Scope}; use serde::{Deserialize, Serialize}; /// REST API state #[derive(Clone)] pub struct RestApiState { pub server_addr: String, } /// Standard REST error response #[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: ErrorDetail, pub meta: ResponseMeta, } #[derive(Debug, Serialize)] pub struct ErrorDetail { pub code: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, } #[derive(Debug, Serialize)] pub struct ResponseMeta { pub request_id: String, pub timestamp: String, } impl ResponseMeta { fn new() -> Self { Self { request_id: uuid::Uuid::new_v4().to_string(), timestamp: chrono::Utc::now().to_rfc3339(), } } } /// Standard REST success response #[derive(Debug, Serialize)] pub struct SuccessResponse { pub data: T, pub meta: ResponseMeta, } impl SuccessResponse { fn new(data: T) -> Self { Self { data, meta: ResponseMeta::new(), } } } /// Token issuance request #[derive(Debug, Deserialize)] pub struct TokenRequest { pub username: String, pub password: String, #[serde(default = "default_ttl")] pub ttl_seconds: u64, } fn default_ttl() -> u64 { 3600 // 1 hour } /// Token response #[derive(Debug, Serialize)] pub struct TokenResponse { pub token: String, pub expires_at: String, } /// Token verification request #[derive(Debug, Deserialize)] pub struct VerifyRequest { pub token: String, } /// Token verification response #[derive(Debug, Serialize)] pub struct VerifyResponse { pub valid: bool, pub principal_id: Option, pub principal_name: Option, pub roles: Option>, } /// User creation request #[derive(Debug, Deserialize)] pub struct CreateUserRequest { pub id: String, pub name: String, } /// User response #[derive(Debug, Serialize)] pub struct UserResponse { pub id: String, pub name: String, pub kind: String, } impl From for UserResponse { fn from(p: Principal) -> Self { Self { id: p.id, name: p.name, kind: format!("{:?}", p.kind), } } } /// Users list response #[derive(Debug, Serialize)] pub struct UsersResponse { pub users: Vec, } /// Project creation request (placeholder) #[derive(Debug, Deserialize)] pub struct CreateProjectRequest { pub id: String, pub name: String, } /// Project response (placeholder) #[derive(Debug, Serialize)] pub struct ProjectResponse { pub id: String, pub name: String, } /// Projects list response (placeholder) #[derive(Debug, Serialize)] pub struct ProjectsResponse { pub projects: Vec, } /// Build the REST API router pub fn build_router(state: RestApiState) -> Router { Router::new() .route("/api/v1/auth/token", post(issue_token)) .route("/api/v1/auth/verify", post(verify_token)) .route("/api/v1/users", get(list_users).post(create_user)) .route("/api/v1/projects", get(list_projects).post(create_project)) .route("/health", get(health_check)) .with_state(state) } /// Health check endpoint async fn health_check() -> (StatusCode, Json>) { ( StatusCode::OK, Json(SuccessResponse::new( serde_json::json!({ "status": "healthy" }), )), ) } /// POST /api/v1/auth/token - Issue token async fn issue_token( State(state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { let TokenRequest { username, password: _password, ttl_seconds, } = req; // Connect to IAM server let config = IamClientConfig::new(&state.server_addr).without_tls(); let client = IamClient::connect(config).await.map_err(|e| { error_response( StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e), ) })?; // For demo purposes, create a user principal // In production, this would authenticate against a user store let principal = Principal { id: username.clone(), kind: PrincipalKind::User, name: username.clone(), org_id: None, project_id: None, email: None, oidc_sub: None, node_id: None, metadata: Default::default(), created_at: 0, updated_at: 0, enabled: true, }; // Issue token let token = client .issue_token(&principal, vec![], Scope::System, ttl_seconds) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "TOKEN_ISSUE_FAILED", &e.to_string(), ) })?; let expires_at = chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds as i64); Ok(Json(SuccessResponse::new(TokenResponse { token, expires_at: expires_at.to_rfc3339(), }))) } /// POST /api/v1/auth/verify - Verify token async fn verify_token( State(state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { // Connect to IAM server let config = IamClientConfig::new(&state.server_addr).without_tls(); let client = IamClient::connect(config).await.map_err(|e| { error_response( StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e), ) })?; // Validate token let result = client.validate_token(&req.token).await; match result { Ok(claims) => Ok(Json(SuccessResponse::new(VerifyResponse { valid: true, principal_id: Some(claims.principal_id), principal_name: Some(claims.principal_name), roles: Some(claims.roles), }))), Err(_) => Ok(Json(SuccessResponse::new(VerifyResponse { valid: false, principal_id: None, principal_name: None, roles: None, }))), } } /// POST /api/v1/users - Create user async fn create_user( State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json>), (StatusCode, Json)> { // Connect to IAM server let config = IamClientConfig::new(&state.server_addr).without_tls(); let client = IamClient::connect(config).await.map_err(|e| { error_response( StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e), ) })?; // Create user let principal = client.create_user(&req.id, &req.name).await.map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "USER_CREATE_FAILED", &e.to_string(), ) })?; Ok(( StatusCode::CREATED, Json(SuccessResponse::new(UserResponse::from(principal))), )) } /// GET /api/v1/users - List users async fn list_users( State(state): State, ) -> Result>, (StatusCode, Json)> { // Connect to IAM server let config = IamClientConfig::new(&state.server_addr).without_tls(); let client = IamClient::connect(config).await.map_err(|e| { error_response( StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e), ) })?; // List users let principals = client.list_users().await.map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "USER_LIST_FAILED", &e.to_string(), ) })?; let users: Vec = principals.into_iter().map(UserResponse::from).collect(); Ok(Json(SuccessResponse::new(UsersResponse { users }))) } /// GET /api/v1/projects - List projects (placeholder) async fn list_projects( State(_state): State, ) -> Result>, (StatusCode, Json)> { // Project management not yet implemented in IAM // Return placeholder response Ok(Json(SuccessResponse::new(ProjectsResponse { projects: vec![ProjectResponse { id: "(placeholder)".to_string(), name: "Project management via REST not yet implemented - use gRPC IamAdminService for scope/binding management".to_string(), }], }))) } /// POST /api/v1/projects - Create project (placeholder) async fn create_project( State(_state): State, Json(req): Json, ) -> Result<(StatusCode, Json>), (StatusCode, Json)> { // Project management not yet implemented in IAM // Return placeholder response Ok(( StatusCode::NOT_IMPLEMENTED, Json(SuccessResponse::new(ProjectResponse { id: req.id, name: format!( "Project '{}' - management via REST not yet implemented", req.name ), })), )) } /// Helper to create error response fn error_response( status: StatusCode, code: &str, message: &str, ) -> (StatusCode, Json) { ( status, Json(ErrorResponse { error: ErrorDetail { code: code.to_string(), message: message.to_string(), details: None, }, meta: ResponseMeta::new(), }), ) }