210 lines
7.0 KiB
Rust
210 lines
7.0 KiB
Rust
use crate::handlers;
|
||
use crate::models::{
|
||
CreateRoleRequest, CreateTenantRequest, CreateUserRequest, LoginRequest, LoginResponse, Role,
|
||
RoleResponse, Tenant, TenantEnabledAppsResponse, TenantResponse,
|
||
UpdateTenantEnabledAppsRequest, UpdateTenantRequest, UpdateTenantStatusRequest,
|
||
UpdateUserRequest, UpdateUserRolesRequest, User, UserResponse,
|
||
};
|
||
use serde_json::Value;
|
||
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
||
use utoipa::{Modify, OpenApi};
|
||
|
||
struct SecurityAddon;
|
||
|
||
#[derive(Clone)]
|
||
struct DocsEnvConfig {
|
||
default_tenant_id: Option<String>,
|
||
default_token: Option<String>,
|
||
}
|
||
|
||
impl DocsEnvConfig {
|
||
fn from_env() -> Self {
|
||
let require_tenant = std::env::var("IAM_DOCS_REQUIRE_TENANT_ID")
|
||
.map(|v| v == "1")
|
||
.unwrap_or(false);
|
||
let require_token = std::env::var("IAM_DOCS_REQUIRE_TOKEN")
|
||
.map(|v| v == "1")
|
||
.unwrap_or(false);
|
||
|
||
let default_tenant_id = std::env::var("IAM_DOCS_DEFAULT_TENANT_ID").ok();
|
||
let default_token = std::env::var("IAM_DOCS_DEFAULT_TOKEN").ok();
|
||
|
||
if require_tenant && default_tenant_id.is_none() {
|
||
panic!("IAM_DOCS_REQUIRE_TENANT_ID=1 but IAM_DOCS_DEFAULT_TENANT_ID is not set");
|
||
}
|
||
if require_token && default_token.is_none() {
|
||
panic!("IAM_DOCS_REQUIRE_TOKEN=1 but IAM_DOCS_DEFAULT_TOKEN is not set");
|
||
}
|
||
|
||
Self {
|
||
default_tenant_id,
|
||
default_token,
|
||
}
|
||
}
|
||
|
||
fn tenant_example(&self) -> String {
|
||
self.default_tenant_id
|
||
.clone()
|
||
.unwrap_or_else(|| "11111111-1111-1111-1111-111111111111".to_string())
|
||
}
|
||
|
||
fn authorization_example(&self) -> String {
|
||
let Some(token) = self.default_token.clone() else {
|
||
return "Bearer <access_token>".to_string();
|
||
};
|
||
if token.to_ascii_lowercase().starts_with("bearer ") {
|
||
token
|
||
} else {
|
||
format!("Bearer {}", token)
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Modify for SecurityAddon {
|
||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||
let components = openapi
|
||
.components
|
||
.get_or_insert_with(utoipa::openapi::Components::new);
|
||
components.add_security_scheme(
|
||
"bearer_auth",
|
||
SecurityScheme::Http(
|
||
HttpBuilder::new()
|
||
.scheme(HttpAuthScheme::Bearer)
|
||
.bearer_format("JWT")
|
||
.build(),
|
||
),
|
||
);
|
||
|
||
let cfg = DocsEnvConfig::from_env();
|
||
apply_header_parameter_examples(openapi, &cfg);
|
||
}
|
||
}
|
||
|
||
fn apply_header_parameter_examples(openapi: &mut utoipa::openapi::OpenApi, cfg: &DocsEnvConfig) {
|
||
use utoipa::openapi::path::ParameterBuilder;
|
||
use utoipa::openapi::path::ParameterIn;
|
||
|
||
let tenant_value = cfg.tenant_example();
|
||
let auth_value = cfg.authorization_example();
|
||
|
||
for (_path, item) in openapi.paths.paths.iter_mut() {
|
||
let operations = [
|
||
item.get.as_mut(),
|
||
item.post.as_mut(),
|
||
item.put.as_mut(),
|
||
item.patch.as_mut(),
|
||
item.delete.as_mut(),
|
||
];
|
||
|
||
for op in operations.into_iter().flatten() {
|
||
let Some(params) = op.parameters.as_mut() else {
|
||
continue;
|
||
};
|
||
for param in params.iter_mut() {
|
||
if param.parameter_in == ParameterIn::Header
|
||
&& param.name.eq_ignore_ascii_case("X-Tenant-ID")
|
||
{
|
||
let builder: ParameterBuilder = std::mem::take(param).into();
|
||
*param = builder
|
||
.example(Some(Value::String(tenant_value.clone())))
|
||
.build();
|
||
}
|
||
if param.parameter_in == ParameterIn::Header
|
||
&& param.name.eq_ignore_ascii_case("Authorization")
|
||
{
|
||
let builder: ParameterBuilder = std::mem::take(param).into();
|
||
*param = builder
|
||
.example(Some(Value::String(auth_value.clone())))
|
||
.build();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(OpenApi)]
|
||
#[openapi(
|
||
modifiers(&SecurityAddon),
|
||
info(
|
||
title = "IAM Service API",
|
||
version = "0.1.0",
|
||
description = include_str!("../docs/SCALAR_GUIDE.md")
|
||
),
|
||
|
||
paths(
|
||
handlers::auth::register_handler,
|
||
handlers::auth::login_handler,
|
||
handlers::authorization::my_permissions_handler,
|
||
handlers::platform::get_tenant_enabled_apps_handler,
|
||
handlers::platform::set_tenant_enabled_apps_handler,
|
||
handlers::tenant::create_tenant_handler,
|
||
handlers::tenant::get_tenant_handler,
|
||
handlers::tenant::update_tenant_handler,
|
||
handlers::tenant::update_tenant_status_handler,
|
||
handlers::tenant::delete_tenant_handler,
|
||
handlers::role::create_role_handler,
|
||
handlers::role::list_roles_handler,
|
||
handlers::user::list_users_handler,
|
||
handlers::user::get_user_handler,
|
||
handlers::user::update_user_handler,
|
||
handlers::user::delete_user_handler,
|
||
handlers::user::list_user_roles_handler,
|
||
handlers::user::set_user_roles_handler,
|
||
// Add other handlers here as you implement them
|
||
),
|
||
components(
|
||
schemas(
|
||
User,
|
||
UserResponse,
|
||
CreateUserRequest,
|
||
UpdateUserRequest,
|
||
LoginRequest,
|
||
LoginResponse,
|
||
Role,
|
||
CreateRoleRequest,
|
||
RoleResponse,
|
||
Tenant,
|
||
TenantResponse,
|
||
CreateTenantRequest,
|
||
UpdateTenantRequest,
|
||
UpdateTenantStatusRequest,
|
||
UpdateTenantEnabledAppsRequest,
|
||
TenantEnabledAppsResponse,
|
||
UpdateUserRolesRequest
|
||
)
|
||
),
|
||
tags(
|
||
(name = "Auth", description = "认证:注册/登录/令牌"),
|
||
(name = "Tenant", description = "租户:创建/查询/更新/状态/删除"),
|
||
(name = "User", description = "用户:查询/列表/更新/删除(需权限)"),
|
||
(name = "Role", description = "角色:创建/列表(需权限)"),
|
||
(name = "Me", description = "当前用户:权限自查等"),
|
||
(name = "Policy", description = "策略:预留(ABAC/策略引擎后续扩展)")
|
||
)
|
||
)]
|
||
pub struct ApiDoc;
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::ApiDoc;
|
||
use utoipa::OpenApi;
|
||
|
||
#[test]
|
||
fn openapi_schema_contains_defaults() {
|
||
let doc = ApiDoc::openapi();
|
||
let json = serde_json::to_value(&doc).unwrap();
|
||
|
||
let token_type_default = json
|
||
.pointer("/components/schemas/LoginResponse/properties/token_type/default")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or_default();
|
||
assert_eq!(token_type_default, "Bearer");
|
||
|
||
let tenant_status_default = json
|
||
.pointer("/components/schemas/Tenant/properties/status/default")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or_default();
|
||
assert_eq!(tenant_status_default, "active");
|
||
}
|
||
}
|