//! REST HTTP API handlers for IAM. use axum::{ extract::{Path, Query, State}, http::{HeaderMap, StatusCode}, routing::{get, post}, Json, Router, }; use iam_client::client::{IamClient, IamClientConfig}; use iam_types::{Organization, Principal, PrincipalKind, PrincipalRef, Project, Scope}; use serde::{Deserialize, Serialize}; /// REST API state #[derive(Clone)] pub struct RestApiState { pub server_addr: String, pub tls_enabled: bool, pub admin_token: Option, } /// 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(), } } } fn iam_client_config(state: &RestApiState) -> IamClientConfig { let mut config = IamClientConfig::new(&state.server_addr); if let Some(token) = state.admin_token.as_deref() { config = config.with_admin_token(token); } if !state.tls_enabled { config = config.without_tls(); } config } /// 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(), } } } #[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 } #[derive(Debug, Serialize)] pub struct TokenResponse { pub token: String, pub expires_at: String, } #[derive(Debug, Deserialize)] pub struct VerifyRequest { pub token: String, } #[derive(Debug, Serialize)] pub struct VerifyResponse { pub valid: bool, pub principal_id: Option, pub principal_name: Option, pub roles: Option>, } #[derive(Debug, Deserialize)] pub struct CreateUserRequest { pub id: String, pub name: String, } #[derive(Debug, Serialize)] pub struct UserResponse { pub id: String, pub name: String, pub kind: String, pub org_id: Option, pub project_id: Option, pub enabled: bool, } impl From for UserResponse { fn from(p: Principal) -> Self { Self { id: p.id, name: p.name, kind: format!("{:?}", p.kind), org_id: p.org_id, project_id: p.project_id, enabled: p.enabled, } } } #[derive(Debug, Serialize)] pub struct UsersResponse { pub users: Vec, } #[derive(Debug, Deserialize)] pub struct CreateOrganizationRequest { pub id: String, pub name: String, #[serde(default)] pub description: String, } #[derive(Debug, Deserialize)] pub struct UpdateOrganizationRequest { pub name: Option, pub description: Option, pub enabled: Option, } #[derive(Debug, Serialize)] pub struct OrganizationResponse { pub id: String, pub name: String, pub description: String, pub enabled: bool, } impl From for OrganizationResponse { fn from(org: Organization) -> Self { Self { id: org.id, name: org.name, description: org.description, enabled: org.enabled, } } } #[derive(Debug, Serialize)] pub struct OrganizationsResponse { pub organizations: Vec, } #[derive(Debug, Deserialize)] pub struct CreateProjectRequest { pub id: String, pub org_id: String, pub name: String, #[serde(default)] pub description: String, } #[derive(Debug, Deserialize)] pub struct UpdateProjectRequest { pub name: Option, pub description: Option, pub enabled: Option, } #[derive(Debug, Deserialize)] pub struct ProjectsQuery { pub org_id: Option, } #[derive(Debug, Serialize)] pub struct ProjectResponse { pub id: String, pub org_id: String, pub name: String, pub description: String, pub enabled: bool, } impl From for ProjectResponse { fn from(project: Project) -> Self { Self { id: project.id, org_id: project.org_id, name: project.name, description: project.description, enabled: project.enabled, } } } #[derive(Debug, Serialize)] pub struct ProjectsResponse { pub projects: Vec, } 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/users/{id}", get(get_user)) .route( "/api/v1/orgs", get(list_organizations).post(create_organization), ) .route( "/api/v1/orgs/{org_id}", get(get_organization) .patch(update_organization) .delete(delete_organization), ) .route("/api/v1/projects", get(list_projects).post(create_project)) .route( "/api/v1/orgs/{org_id}/projects/{project_id}", get(get_project) .patch(update_project) .delete(delete_project), ) .route("/health", get(health_check)) .with_state(state) } async fn health_check() -> (StatusCode, Json>) { ( StatusCode::OK, Json(SuccessResponse::new( serde_json::json!({ "status": "healthy" }), )), ) } fn require_admin( state: &RestApiState, headers: &HeaderMap, ) -> Result<(), (StatusCode, Json)> { let Some(token) = state.admin_token.as_deref() else { return Ok(()); }; if let Some(value) = headers.get("x-iam-admin-token") { if let Ok(raw) = value.to_str() { if raw.trim() == token { return Ok(()); } } } if let Some(value) = headers.get("authorization") { if let Ok(raw) = value.to_str() { let raw = raw.trim(); if let Some(rest) = raw .strip_prefix("Bearer ") .or_else(|| raw.strip_prefix("bearer ")) { if rest.trim() == token { return Ok(()); } } } } Err(error_response( StatusCode::UNAUTHORIZED, "ADMIN_TOKEN_REQUIRED", "missing or invalid IAM admin token", )) } async fn connect_client( state: &RestApiState, ) -> Result)> { IamClient::connect(iam_client_config(state)) .await .map_err(|e| { error_response( StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e), ) }) } async fn issue_token( headers: HeaderMap, State(state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; if !allow_insecure_rest_token_issue() { return Err(error_response( StatusCode::FORBIDDEN, "TOKEN_ISSUE_DISABLED", "token issuance is disabled; enable IAM_REST_ALLOW_INSECURE_TOKEN=true for dev", )); } let TokenRequest { username, password: _password, ttl_seconds, } = req; let client = connect_client(&state).await?; let principal_ref = PrincipalRef::new(PrincipalKind::User, &username); let principal = match client.get_principal(&principal_ref).await.map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "PRINCIPAL_LOOKUP_FAILED", &e.to_string(), ) })? { Some(principal) => principal, None => { return Err(error_response( StatusCode::NOT_FOUND, "PRINCIPAL_NOT_FOUND", "principal not found", )) } }; if !principal.enabled { return Err(error_response( StatusCode::FORBIDDEN, "PRINCIPAL_DISABLED", "principal is disabled", )); } 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(), }))) } fn allow_insecure_rest_token_issue() -> bool { std::env::var("IAM_REST_ALLOW_INSECURE_TOKEN") .or_else(|_| std::env::var("PHOTON_IAM_REST_ALLOW_INSECURE_TOKEN")) .ok() .map(|value| { matches!( value.trim().to_lowercase().as_str(), "1" | "true" | "yes" | "y" | "on" ) }) .unwrap_or(false) } async fn verify_token( State(state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { let client = connect_client(&state).await?; 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, }))), } } async fn create_user( headers: HeaderMap, State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json>), (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; 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))), )) } async fn list_users( headers: HeaderMap, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; 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 }))) } async fn get_user( headers: HeaderMap, Path(id): Path, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let principal = client .get_principal(&PrincipalRef::user(id)) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "USER_LOOKUP_FAILED", &e.to_string(), ) })? .ok_or_else(|| error_response(StatusCode::NOT_FOUND, "USER_NOT_FOUND", "user not found"))?; Ok(Json(SuccessResponse::new(UserResponse::from(principal)))) } async fn create_organization( headers: HeaderMap, State(state): State, Json(req): Json, ) -> Result< (StatusCode, Json>), (StatusCode, Json), > { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let org = client .create_organization(&req.id, &req.name, &req.description) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "ORG_CREATE_FAILED", &e.to_string(), ) })?; Ok((StatusCode::CREATED, Json(SuccessResponse::new(org.into())))) } async fn list_organizations( headers: HeaderMap, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let organizations = client.list_organizations().await.map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "ORG_LIST_FAILED", &e.to_string(), ) })?; Ok(Json(SuccessResponse::new(OrganizationsResponse { organizations: organizations.into_iter().map(Into::into).collect(), }))) } async fn get_organization( headers: HeaderMap, Path(org_id): Path, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let organization = client .get_organization(&org_id) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "ORG_LOOKUP_FAILED", &e.to_string(), ) })? .ok_or_else(|| { error_response( StatusCode::NOT_FOUND, "ORG_NOT_FOUND", "organization not found", ) })?; Ok(Json(SuccessResponse::new(organization.into()))) } async fn update_organization( headers: HeaderMap, Path(org_id): Path, State(state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let organization = client .update_organization( &org_id, req.name.as_deref(), req.description.as_deref(), req.enabled, ) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "ORG_UPDATE_FAILED", &e.to_string(), ) })?; Ok(Json(SuccessResponse::new(organization.into()))) } async fn delete_organization( headers: HeaderMap, Path(org_id): Path, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let deleted = client.delete_organization(&org_id).await.map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "ORG_DELETE_FAILED", &e.to_string(), ) })?; Ok(Json(SuccessResponse::new( serde_json::json!({ "deleted": deleted }), ))) } async fn create_project( headers: HeaderMap, State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json>), (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let project = client .create_project(&req.org_id, &req.id, &req.name, &req.description) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "PROJECT_CREATE_FAILED", &e.to_string(), ) })?; Ok(( StatusCode::CREATED, Json(SuccessResponse::new(project.into())), )) } async fn list_projects( headers: HeaderMap, Query(query): Query, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let projects = client .list_projects(query.org_id.as_deref()) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "PROJECT_LIST_FAILED", &e.to_string(), ) })?; Ok(Json(SuccessResponse::new(ProjectsResponse { projects: projects.into_iter().map(Into::into).collect(), }))) } async fn get_project( headers: HeaderMap, Path((org_id, project_id)): Path<(String, String)>, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let project = client .get_project(&org_id, &project_id) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "PROJECT_LOOKUP_FAILED", &e.to_string(), ) })? .ok_or_else(|| { error_response( StatusCode::NOT_FOUND, "PROJECT_NOT_FOUND", "project not found", ) })?; Ok(Json(SuccessResponse::new(project.into()))) } async fn update_project( headers: HeaderMap, Path((org_id, project_id)): Path<(String, String)>, State(state): State, Json(req): Json, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let project = client .update_project( &org_id, &project_id, req.name.as_deref(), req.description.as_deref(), req.enabled, ) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "PROJECT_UPDATE_FAILED", &e.to_string(), ) })?; Ok(Json(SuccessResponse::new(project.into()))) } async fn delete_project( headers: HeaderMap, Path((org_id, project_id)): Path<(String, String)>, State(state): State, ) -> Result>, (StatusCode, Json)> { require_admin(&state, &headers)?; let client = connect_client(&state).await?; let deleted = client .delete_project(&org_id, &project_id) .await .map_err(|e| { error_response( StatusCode::INTERNAL_SERVER_ERROR, "PROJECT_DELETE_FAILED", &e.to_string(), ) })?; Ok(Json(SuccessResponse::new( serde_json::json!({ "deleted": deleted }), ))) } 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(), }), ) }