diff --git a/Cargo.toml b/Cargo.toml index 2ce000d97ce693761931ee5b5584fa54798584f9..48bba047245c762c08145a98223833669b6fedac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,8 @@ time = "0.3.17" openidconnect = "2.5.0" url = "2.3.1" futures = "0.3.26" +utoipa = { version = "3", features = ["actix_extras"] } +utoipa-swagger-ui = { version ="3.1.3", features = ["actix-web"]} [build-dependencies] tonic-build = "0.8.4" diff --git a/README.md b/README.md index 10c888ec986afc104410ffe5cd1a08139f7f3a57..66adbf4a1f86148019b34194e2f67fe122ac38d4 100644 --- a/README.md +++ b/README.md @@ -134,5 +134,8 @@ curl -k --header "Authorization:XmUICsVV48EjfkWYv3ch1eutRJOQh7mp3bRfmQDL" -v htt ```shell RUST_BACKTRACE=full RUST_LOG=info ./target/debug/client --config add --key-id default-pgp --file-type rpm --key-type pgp .data/simple.rpm ``` +## OpenAPI Documentation +Signatrust supports online openAPI documentation, once control server starts, navigate to `localhost:8080/swagger-ui/` and check the document. note you need to add correct `Authorization` +header to try the APIs. # Contribute diff --git a/src/presentation/handler/control/datakey_handler.rs b/src/presentation/handler/control/datakey_handler.rs index c8b4c9b2c21926187e838312cdf5de22fb042681..9f34ed271ac1249837b34d9896624512f6024973 100644 --- a/src/presentation/handler/control/datakey_handler.rs +++ b/src/presentation/handler/control/datakey_handler.rs @@ -26,13 +26,112 @@ use crate::application::datakey::KeyService; use crate::domain::datakey::entity::DataKey; use super::model::user::dto::UserIdentity; - +/// Create new key +/// +/// This will generate either a pgp private/public key pairs or a x509 private/public/cert keys. +/// ## Generate pgp key +/// To generate a pgp key the required parameters in `attributes` are: +/// 1. **digest_algorithm**: the digest algorithm used for pgp, for example: sha2_256 +/// 2. **email**: email address used for identify the pgp key, +/// 3. **key_length**: the private key length, for example, 2048, +/// 4. **key_type**: the algorithm of private key, for example, rsa or dsa. +/// ### Request body example: +/// ```json +/// { +/// "name": "test-pgp", +/// "email": "tommylikehu@gmail.com", +/// "description": "hello world", +/// "key_type": "pgp", +/// "user": "tommylike", +/// "attributes": { +/// "digest_algorithm": "sha2_256", +/// "key_type": "rsa", +/// "key_length": "2048", +/// "email": "test@openeuler.org", +/// }, +/// "create_at": "2023-04-12 22:10:57+08:00", +/// "expire_at": "2024-05-12 22:10:57+08:00" +/// } +/// ``` +/// +/// ## Generate x509 key +/// To generate a x509 key the required parameters in `attributes` are: +/// 1. **digest_algorithm**: the digest algorithm used for x509 key, for example: sha2_256 +/// 2. **key_length**: the private key length, for example, 2048, +/// 3. **key_type**: the algorithm of private key, for example, rsa or dsa. +/// 4. **common_name**: common name (commonName, CN), used for certificate. +/// 5. **country_name**: country (countryName, C), used for certificate. +/// 6. **locality**: locality (locality, L), used for certificate. +/// 7. **organization**: organization (organizationName, O), used for certificate. +/// 8. **organizational_unit**: organizational unit (organizationalUnitName, OU), used for certificate. +/// 9. **province_name**: state or province name (stateOrProvinceName, ST), used for certificate. +/// ### Request body example: +/// ```json +/// { +/// "name": "test-x509", +/// "email": "tommylikehu@gmail.com", +/// "description": "hello world", +/// "key_type": "x509", +/// "user": "tommylike", +/// "attributes": { +/// "digest_algorithm": "sha2_256", +/// "key_type": "rsa", +/// "key_length": "2048", +/// "common_name": "common name", +/// "organizational_unit": "organizational_unit", +/// "organization": "organization", +/// "locality": "locality", +/// "province_name": "province_name", +/// "country_name": "country_name" +/// }, +/// "create_at": "2023-04-12 22:10:57+08:00", +/// "expire_at": "2024-05-12 22:10:57+08:00" +/// } +/// ``` +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys -d '{}' +/// ``` +#[utoipa::path( + post, + path = "/api/v1/keys", + request_body = DataKeyDTO, + security( + ("Authorization" = []) + ), + responses( + (status = 201, description = "Key successfully imported", body = DataKeyDTO), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn create_data_key(user: UserIdentity, key_service: web::Data, datakey: web::Json,) -> Result { datakey.validate()?; let mut key = DataKey::convert_from(datakey.0, user)?; Ok(HttpResponse::Created().json(DataKeyDTO::try_from(key_service.into_inner().create(&mut key).await?)?)) } +/// Get all available keys from database. +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl https://domain:port/api/v1/keys/ +/// ``` +#[utoipa::path( + get, + path = "/api/v1/keys/", + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "List available keys", body = [DataKeyDTO]), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn list_data_key(_user: UserIdentity, key_service: web::Data) -> Result { let keys = key_service.into_inner().get_all().await?; let mut results = vec![]; @@ -42,32 +141,173 @@ async fn list_data_key(_user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { let key = key_service.into_inner().get_one(id.parse::()?).await?; Ok(HttpResponse::Ok().json(DataKeyDTO::try_from(key)?)) } +/// Delete specific key by id from database +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X DELETE https://domain:port/api/v1/keys/{id} +/// ``` +#[utoipa::path( + delete, + path = "/api/v1/keys/{id}", + params( + ("id" = i32, Path, description = "Key id"), + ), + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "Key successfully deleted"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn delete_data_key(_user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { key_service.into_inner().delete_one(id.parse::()?).await?; Ok(HttpResponse::Ok()) } +/// Export content of specific key +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id}/export +/// ``` +#[utoipa::path( + post, + path = "/api/v1/keys/{id}/export", + params( + ("id" = i32, Path, description = "Key id"), + ), + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "Key successfully exported", body = ExportKey), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn export_data_key(_user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { Ok(HttpResponse::Ok().json(ExportKey::try_from(key_service.export_one(id.parse::()?).await?)?)) } +/// Enable specific key +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id}/enable +/// ``` +#[utoipa::path( + post, + path = "/api/v1/keys/{id}/enable", + params( + ("id" = i32, Path, description = "Key id"), + ), + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "Key successfully enabled"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn enable_data_key(_user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { key_service.enable(id.parse::()?).await?; Ok(HttpResponse::Ok()) } +/// Disable specific key +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id}/disable +/// ``` +#[utoipa::path( + post, + path = "/api/v1/keys/{id}/disable", + params( + ("id" = i32, Path, description = "Key id"), + ), + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "Key successfully disabled"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn disable_data_key(_user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { key_service.disable(id.parse::()?).await?; Ok(HttpResponse::Ok()) } +/// Import key +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/import +/// ``` +#[utoipa::path( + post, + path = "/api/v1/keys/import", + request_body = DataKeyDTO, + security( + ("Authorization" = []) + ), + responses( + (status = 201, description = "Key successfully imported", body = DataKeyDTO), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn import_data_key(_user: UserIdentity) -> Result { - Ok(HttpResponse::Ok()) + Ok(HttpResponse::Created()) } diff --git a/src/presentation/handler/control/model/datakey/dto.rs b/src/presentation/handler/control/model/datakey/dto.rs index 59f1009898f99e4370ee4a301b16b208b0ea216b..63b920a763c85610513fb30e430575f9b6c0513f 100644 --- a/src/presentation/handler/control/model/datakey/dto.rs +++ b/src/presentation/handler/control/model/datakey/dto.rs @@ -9,9 +9,10 @@ use validator::{Validate, ValidationError}; use std::collections::HashMap; use crate::util::error::Error; use serde::{Deserialize, Serialize}; +use utoipa::{ToSchema}; use crate::presentation::handler::control::model::user::dto::UserIdentity; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct ExportKey { pub public_key: String, pub certificate: String, @@ -28,25 +29,35 @@ impl TryFrom for ExportKey { } } -#[derive(Debug, Validate, Deserialize, Serialize)] +#[derive(Debug, Validate, Deserialize, Serialize, ToSchema)] pub struct DataKeyDTO { + /// Key ID, leave empty when creating #[serde(skip_deserializing)] pub id: i32, + /// Key Name, should be identical, length between 4 and 20 #[validate(length(min = 4, max = 20))] pub name: String, #[serde(skip_deserializing)] + /// User email, will be removed pub email: String, + /// Description, length between 0 and 100 #[validate(length(min = 0, max = 100))] pub description: String, + /// User ID, leave empty when creating #[serde(skip_deserializing)] pub user: i32, + /// Attributes in map #[serde(serialize_with = "sorted_map")] pub attributes: HashMap, + /// Key type current support pgp and x509 pub key_type: String, + /// Create utc time, format: 2023-04-08 13:36:35.328324 UTC #[validate(custom = "validate_utc_time")] pub create_at: String, + /// Expire utc time, format: 2023-04-08 13:36:35.328324 UTC #[validate(custom = "validate_utc_time")] pub expire_at: String, + /// Key state, leave empty when creating #[serde(skip_deserializing)] pub key_state: String, } diff --git a/src/presentation/handler/control/model/token/dto.rs b/src/presentation/handler/control/model/token/dto.rs index 0439631fa9c7a56a0dc5bbf64fffddce4e57615e..ad79876262e6c21e57bebe4801e393a069c79094 100644 --- a/src/presentation/handler/control/model/token/dto.rs +++ b/src/presentation/handler/control/model/token/dto.rs @@ -6,9 +6,10 @@ use serde::{Deserialize, Serialize}; use std::convert::From; use chrono::{DateTime, Utc}; use crate::domain::token::entity::Token; +use utoipa::{ToSchema}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct TokenDTO { #[serde(skip_deserializing)] pub id: i32, diff --git a/src/presentation/handler/control/model/user/dto.rs b/src/presentation/handler/control/model/user/dto.rs index 952efcb09be38788f637f51b5ee5ec8fb74bee08..2398b1e64ec0528aac58a7f4b0f851e2f2c63c1c 100644 --- a/src/presentation/handler/control/model/user/dto.rs +++ b/src/presentation/handler/control/model/user/dto.rs @@ -9,8 +9,9 @@ use serde::{Deserialize, Serialize}; use std::convert::From; use crate::application::user::UserService; use crate::domain::user::entity::User; +use utoipa::{ToSchema}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct UserIdentity { pub email: String, pub id: i32, diff --git a/src/presentation/handler/control/user_handler.rs b/src/presentation/handler/control/user_handler.rs index 133af8f81892ed5b17c70022d7de37362217e523..3c77dc8ea24d7dfa0278e6d4ac17c21dc02e445f 100644 --- a/src/presentation/handler/control/user_handler.rs +++ b/src/presentation/handler/control/user_handler.rs @@ -28,19 +28,79 @@ struct Code { pub code: String, } +/// Start the login OIDC login process +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl https://domain:port/api/v1/user/login +/// ``` +#[utoipa::path( + get, + path = "/api/v1/user/login", + responses( + (status = 302, description = "Redirect to login url"), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn login(user_service: web::Data) -> Result { Ok(HttpResponse::Found().insert_header(("Location", user_service.into_inner().get_login_url().await?.as_str())).finish()) } +/// Get login user information +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl https://domain:port/api/v1/user/ +/// ``` +#[utoipa::path( + get, + path = "/api/v1/user/", + responses( + (status = 200, description = "get login user information", body = UserIdentity), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn info(id: UserIdentity) -> Result { Ok(HttpResponse::Ok().json(id)) } +/// Logout current user +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/user/logout +/// ``` +#[utoipa::path( + post, + path = "/api/v1/user/logout", + responses( + (status = 204, description = "logout successfully"), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn logout(id: Identity) -> Result { id.logout(); Ok( HttpResponse::NoContent().finish()) } +/// Callback API for OIDC provider +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X GET https://domain:port/api/v1/user/callback +/// ``` +#[utoipa::path( + get, + path = "/api/v1/user/callback", + responses( + (status = 302, description = "logout succeed, redirect to index"), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn callback(req: HttpRequest, user_service: web::Data, code: web::Query) -> Result { let user_entity:UserIdentity = UserIdentity::from(user_service.into_inner().validate_user(&code.code).await?); match Identity::login(&req.extensions(), serde_json::to_string(&user_entity)?) { @@ -53,11 +113,48 @@ async fn callback(req: HttpRequest, user_service: web::Data, co } } +/// Generate new token for current user +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/user/api_keys +/// ``` +#[utoipa::path( + post, + path = "/api/v1/user/api_keys", + security( + ("Authorization" = []) + ), + responses( + (status = 201, description = "logout successfully", body = TokenDTO), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn new_token(user: UserIdentity, user_service: web::Data, token: web::Json) -> Result { let token = user_service.into_inner().generate_token(&user, token.0).await?; - Ok(HttpResponse::Ok().json(TokenDTO::from(token))) + Ok(HttpResponse::Created().json(TokenDTO::from(token))) } +/// List all tokens for current user +/// +/// **NOTE**: only the token hash will be responsed. +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X GET https://domain:port/api/v1/user/api_keys +/// ``` +#[utoipa::path( + get, + path = "/api/v1/user/api_keys", + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "logout successfully", body = [TokenDTO]), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] async fn list_token(user: UserIdentity, user_service: web::Data) -> Result { let token = user_service.into_inner().get_token(&user).await?; let mut results = vec![]; diff --git a/src/presentation/server/control_server.rs b/src/presentation/server/control_server.rs index a7911fa6730012b123d85b3998eadbb2b23dedcd..dac6005078f94a87ae31cbf29c2d12e4d9f59654 100644 --- a/src/presentation/server/control_server.rs +++ b/src/presentation/server/control_server.rs @@ -15,6 +15,11 @@ */ use std::net::SocketAddr; +use utoipa::{ + openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, + Modify, OpenApi, +}; +use utoipa_swagger_ui::SwaggerUi; use std::sync::{Arc, RwLock}; use actix_web::{App, HttpServer, middleware, web, cookie::Key}; use config::Config; @@ -49,6 +54,48 @@ pub struct ControlServer { } +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.as_mut().unwrap(); + components.add_security_scheme( + "Authorization", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("Authorization"))), + ) + } +} + +#[derive(OpenApi)] +#[openapi( + paths( + crate::presentation::handler::control::datakey_handler::list_data_key, + crate::presentation::handler::control::datakey_handler::show_data_key, + crate::presentation::handler::control::datakey_handler::create_data_key, + crate::presentation::handler::control::datakey_handler::delete_data_key, + crate::presentation::handler::control::datakey_handler::export_data_key, + crate::presentation::handler::control::datakey_handler::enable_data_key, + crate::presentation::handler::control::datakey_handler::disable_data_key, + crate::presentation::handler::control::datakey_handler::import_data_key, + + crate::presentation::handler::control::user_handler::login, + crate::presentation::handler::control::user_handler::callback, + crate::presentation::handler::control::user_handler::info, + crate::presentation::handler::control::user_handler::logout, + crate::presentation::handler::control::user_handler::new_token, + crate::presentation::handler::control::user_handler::list_token, + ), + components( + schemas(crate::presentation::handler::control::model::datakey::dto::DataKeyDTO, + crate::presentation::handler::control::model::datakey::dto::ExportKey, + crate::presentation::handler::control::model::token::dto::TokenDTO, + crate::presentation::handler::control::model::user::dto::UserIdentity, + crate::util::error::ErrorMessage) + ), + modifiers(&SecurityAddon) +)] +struct ControlApiDoc; + impl ControlServer { pub async fn new(server_config: Arc>) -> Result { let database = server_config.read()?.get_table("database")?; @@ -120,6 +167,8 @@ impl ControlServer { .unwrap(), ); + let openapi = ControlApiDoc::openapi(); + let http_server = HttpServer::new(move || { App::new() .wrap(middleware::Logger::default()) @@ -145,6 +194,10 @@ impl ControlServer { .service(web::scope("/api/v1") .service(user_handler::get_scope()) .service(datakey_handler::get_scope())) + //open api document + .service( + SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-doc/openapi.json", openapi.clone()), + ) }); if self.server_config .read()? diff --git a/src/util/error.rs b/src/util/error.rs index f219fb74517ca788ac57de8ffb36f6daa9c5579d..2ab0dcc5f68d06b46769a28de24883aae2002ec4 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -40,6 +40,7 @@ use openidconnect::url::ParseError as OIDCParseError; use openidconnect::ConfigurationError; use openidconnect::UserInfoError; use anyhow::Error as AnyhowError; +use utoipa::{ToSchema}; pub type Result = std::result::Result; @@ -114,7 +115,7 @@ pub enum Error { KOAlreadySignedError, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, ToSchema)] pub struct ErrorMessage { detail: String }