mod auth;
mod cedar;
mod config;
mod entity_config;
mod entity_handlers;
mod models;
mod openapi_gen;
mod pdt;
use axum::{routing::get, Json, Router};
use pep::oidc_resource_server::ResourceServerClient;
use std::sync::Arc;
use tower_http::cors::CorsLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use utoipa_swagger_ui::SwaggerUi;
#[derive(Clone)]
pub struct AppState {
pub config: Arc<config::Config>,
pub pdt: Arc<pdt::PdtClient>,
pub authorizer: Option<Arc<pep::cedar::CedarAuthorizer>>,
pub entity_configs: Vec<Arc<entity_config::EntityConfig>>,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with(tracing_subscriber::fmt::layer())
.init();
let config = config::Config::load().await?;
tracing::info!("Loaded config for Fame");
let entity_config_dir =
std::env::var("FAME_ENTITY_DIR").unwrap_or_else(|_| config.entity_dir.clone());
let entity_configs_raw = entity_config::load_entity_configs(&entity_config_dir);
let entity_configs: Vec<Arc<entity_config::EntityConfig>> =
entity_configs_raw.into_iter().map(Arc::new).collect();
tracing::info!(
"Loaded {} entity configs from {}",
entity_configs.len(),
entity_config_dir
);
let pdt_client = pdt::PdtClient::new(&config.pdt_url);
let state = AppState {
config: Arc::new(config.clone()),
pdt: Arc::new(pdt_client),
authorizer: None,
entity_configs: entity_configs.clone(),
};
let authorizer = if config.cedar.enabled {
let cedar_config: pep::cedar::CedarConfig = config.cedar.clone().into();
match pep::cedar::CedarAuthorizer::new_with_policy_store(cedar_config).await {
Ok(a) => {
tracing::info!("Cedar authorizer initialized (embedded policies loaded)");
Some(Arc::new(a))
}
Err(e) => {
tracing::error!(
"FATAL: Cedar is enabled but failed to initialize: {}. \
Refusing to start fail-open — fix the policy/schema and restart.",
e
);
return Err(anyhow::anyhow!(
"Cedar enabled but initialization failed: {}",
e
));
}
}
} else {
tracing::warn!(
"Cedar authorization disabled — every authz check is skipped (fail-open). \
Only acceptable for local development."
);
None
};
let auth_client = ResourceServerClient::new();
let auth_layer = auth::middleware::AuthLayer::new(config.auth.clone(), auth_client);
tracing::info!("Auth layer initialized (enabled: {})", config.auth.enabled);
let state = AppState {
authorizer,
..state
};
let public_routes = Router::new().route("/health", get(health));
let mut protected_routes = Router::new()
.route(
"/api/v1/config/entities",
get(entity_handlers::list_entity_configs),
);
protected_routes = entity_handlers::register_entity_routes(protected_routes, &entity_configs);
let protected_routes = protected_routes.layer(auth_layer);
let merged_openapi = openapi_gen::generate_merged_openapi(&entity_configs);
let app = Router::new()
.merge(public_routes)
.merge(protected_routes)
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", merged_openapi))
.with_state(state)
.layer(CorsLayer::permissive());
let addr = format!("{}:{}", config.host, config.port);
tracing::info!("Fame starting on {}", addr);
tracing::info!("Swagger UI: http://{}/swagger-ui", addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn health() -> Json<serde_json::Value> {
Json(serde_json::json!({
"status": "ok",
"service": "fame",
"version": env!("CARGO_PKG_VERSION"),
}))
}