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>
382 lines
10 KiB
Rust
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(),
|
|
}),
|
|
)
|
|
}
|