photoncloud-monorepo/iam/crates/iam-server/src/rest.rs
centra 3eeb303dcb feat: Batch commit for T039.S3 deployment
Includes all pending changes needed for nixos-anywhere:
- fiberlb: L7 policy, rule, certificate types
- deployer: New service for cluster management
- nix-nos: Generic network modules
- Various service updates and fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 04:34:51 +09:00

382 lines
10 KiB
Rust

//! 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<serde_json::Value>,
}
#[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<T> {
pub data: T,
pub meta: ResponseMeta,
}
impl<T> SuccessResponse<T> {
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<String>,
pub principal_name: Option<String>,
pub roles: Option<Vec<String>>,
}
/// 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<Principal> 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<UserResponse>,
}
/// 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<ProjectResponse>,
}
/// 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<SuccessResponse<serde_json::Value>>) {
(
StatusCode::OK,
Json(SuccessResponse::new(
serde_json::json!({ "status": "healthy" }),
)),
)
}
/// POST /api/v1/auth/token - Issue token
async fn issue_token(
State(state): State<RestApiState>,
Json(req): Json<TokenRequest>,
) -> Result<Json<SuccessResponse<TokenResponse>>, (StatusCode, Json<ErrorResponse>)> {
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<RestApiState>,
Json(req): Json<VerifyRequest>,
) -> Result<Json<SuccessResponse<VerifyResponse>>, (StatusCode, Json<ErrorResponse>)> {
// 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<RestApiState>,
Json(req): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<SuccessResponse<UserResponse>>), (StatusCode, Json<ErrorResponse>)> {
// 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<RestApiState>,
) -> Result<Json<SuccessResponse<UsersResponse>>, (StatusCode, Json<ErrorResponse>)> {
// 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<UserResponse> = 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<RestApiState>,
) -> Result<Json<SuccessResponse<ProjectsResponse>>, (StatusCode, Json<ErrorResponse>)> {
// 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<RestApiState>,
Json(req): Json<CreateProjectRequest>,
) -> Result<(StatusCode, Json<SuccessResponse<ProjectResponse>>), (StatusCode, Json<ErrorResponse>)>
{
// 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<ErrorResponse>) {
(
status,
Json(ErrorResponse {
error: ErrorDetail {
code: code.to_string(),
message: message.to_string(),
details: None,
},
meta: ResponseMeta::new(),
}),
)
}