diff --git a/src/server.rs b/src/server.rs index 7cb6f0a..eb4b68b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -171,7 +171,15 @@ pub async fn run( ) .service( web::scope("/utils") - .route("/action", web::post().to(handlers::utils_handle_action)), + .route("/action", web::post().to(handlers::utils_handle_action)) + .service( + web::resource([ + "/{area}/{resource}", + "/{area}/{resource}/{resource_id}", + "/{area}/{resource}/{resource_id}/{resource_operation}", + ]) + .to(handlers::utils_action), + ), ) .service( web::scope("/ui").route("/state", web::get().to(handlers::ui_state_get)), diff --git a/src/server/handlers.rs b/src/server/handlers.rs index 1ad9c20..88f8cd5 100644 --- a/src/server/handlers.rs +++ b/src/server/handlers.rs @@ -18,6 +18,7 @@ mod ui_state_get; mod user_data_get; mod user_data_set; mod user_get; +mod utils_action; mod utils_handle_action; mod webhooks_responders; @@ -45,6 +46,7 @@ pub use self::{ user_data_get::user_data_get, user_data_set::user_data_set, user_get::user_get, + utils_action::utils_action, utils_handle_action::utils_handle_action, webhooks_responders::webhooks_responders, }; diff --git a/src/server/handlers/utils_action.rs b/src/server/handlers/utils_action.rs new file mode 100644 index 0000000..9011199 --- /dev/null +++ b/src/server/handlers/utils_action.rs @@ -0,0 +1,752 @@ +use crate::{ + api::Api, + error::Error as SecutilsError, + network::{DnsResolver, EmailTransport}, + server::AppState, + users::{User, UserShare}, + utils::{ + certificates_handle_action, UtilsAction, UtilsActionParams, UtilsResource, + UtilsResourceOperation, + }, +}; +use actix_http::Method; +use actix_web::{web, HttpRequest, HttpResponse}; +use serde_json::Value as JsonValue; +use uuid::Uuid; + +fn extract_resource(req: &HttpRequest) -> Option { + let match_info = req.match_info(); + let (Some(area), Some(resource)) = (match_info.get("area"), match_info.get("resource")) else { + return None; + }; + + UtilsResource::try_from((area, resource)).ok() +} + +fn extract_action(req: &HttpRequest, resource: &UtilsResource) -> Option { + let match_info = req.match_info(); + let (Ok(resource_id), Ok(resource_operation)) = ( + match_info + .get("resource_id") + .map(Uuid::parse_str) + .transpose(), + match_info + .get("resource_operation") + .map(|operation| UtilsResourceOperation::try_from((resource, operation))) + .transpose(), + ) else { + return None; + }; + + match (req.method(), resource_id, resource_operation) { + // Resource collection based actions. + (&Method::GET, None, None) => Some(UtilsAction::List), + (&Method::POST, None, None) => Some(UtilsAction::Create), + // Resource based actions. + (&Method::GET, Some(resource_id), None) => Some(UtilsAction::Get { resource_id }), + (&Method::PUT, Some(resource_id), None) => Some(UtilsAction::Update { resource_id }), + (&Method::DELETE, Some(resource_id), None) => Some(UtilsAction::Delete { resource_id }), + (&Method::POST, Some(resource_id), Some(operation)) => Some(UtilsAction::Execute { + resource_id, + operation, + }), + // Unsupported actions. + _ => None, + } +} + +async fn extract_user( + api: &Api, + user: Option, + user_share: Option, + action: &UtilsAction, + resource: &UtilsResource, +) -> anyhow::Result> { + match (user, user_share) { + // If user is authenticated, and action is not targeting a shared resource, act on behalf of + // the currently authenticated user. + (user, None) if user.is_some() => Ok(user), + + // If user is authenticated, and action is targeting a shared resource that belongs to the + // user, act on behalf of the currently authenticated user. + (Some(user), Some(user_share)) if user.id == user_share.user_id => Ok(Some(user)), + + // If action is targeting a shared resource that doesn't belong to currently authenticated + // user or user isn't authenticated, act on behalf of the shared resource owner assuming + // action is authorized to be performed on a shared resource. + (_, Some(user_share)) if user_share.is_action_authorized(action, resource) => { + api.users().get(user_share.user_id).await + } + + _ => Ok(None), + } +} + +pub async fn utils_action( + state: web::Data, + user: Option, + user_share: Option, + req: HttpRequest, + body_params: Option>, +) -> Result { + // First, extract resource. + let Some(resource) = extract_resource(&req) else { + return Ok(HttpResponse::NotFound().finish()); + }; + + // Next, extract action + let Some(action) = extract_action(&req, &resource) else { + return Ok(HttpResponse::NotFound().finish()); + }; + + // Fail, if action requires params, but params aren't provided, or vice versa. + if body_params.is_some() != action.requires_params() { + return Ok(HttpResponse::BadRequest().finish()); + } + + let Some(user) = extract_user(&state.api, user, user_share, &action, &resource).await? else { + return Err(SecutilsError::access_forbidden()); + }; + + let params = body_params.map(|body| UtilsActionParams::json(body.into_inner())); + let action_result = match resource { + UtilsResource::CertificatesTemplates | UtilsResource::CertificatesPrivateKeys => { + certificates_handle_action(user, &state.api, action, resource, params).await? + } + }; + + Ok(if let Some(result) = action_result.into_inner() { + HttpResponse::Ok().json(result) + } else { + match action { + UtilsAction::List | UtilsAction::Get { .. } => HttpResponse::NotFound().finish(), + UtilsAction::Create + | UtilsAction::Update { .. } + | UtilsAction::Delete { .. } + | UtilsAction::Execute { .. } => HttpResponse::NoContent().finish(), + } + }) +} + +#[cfg(test)] +mod tests { + use super::{extract_action, extract_resource, extract_user, utils_action}; + use crate::{ + tests::{mock_api, mock_app_state, mock_user, mock_user_with_id}, + users::{SharedResource, UserShare, UserShareId}, + utils::{ + PrivateKeyAlgorithm, PrivateKeysCreateParams, UtilsAction, UtilsResource, + UtilsResourceOperation, + }, + }; + use actix_http::body::MessageBody; + use actix_web::{http::Method, test::TestRequest, web}; + use insta::assert_debug_snapshot; + use serde_json::json; + use time::OffsetDateTime; + use uuid::uuid; + + #[test] + fn can_extract_resource() { + for (area, resource) in [ + (None, None), + (Some("certificates"), None), + (None, Some("private_keys")), + (Some("certificates"), Some("unknown")), + (Some("unknown"), Some("private_keys")), + ] { + let request = TestRequest::with_uri("https://secutils.dev/api/utils"); + let request = if let Some(area) = area { + request.param("area", area) + } else { + request + }; + let request = if let Some(resource) = resource { + request.param("resource", resource) + } else { + request + }; + + assert!(extract_resource(&request.to_http_request()).is_none()); + } + + assert_eq!( + extract_resource( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .param("area", "certificates") + .param("resource", "private_keys") + .to_http_request(), + ), + Some(UtilsResource::CertificatesPrivateKeys) + ); + assert_eq!( + extract_resource( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .param("area", "certificates") + .param("resource", "templates") + .to_http_request(), + ), + Some(UtilsResource::CertificatesTemplates) + ); + } + + #[test] + fn ignores_invalid_actions() { + let resource_id = uuid!("00000000-0000-0000-0000-000000000000"); + for resource in [ + UtilsResource::CertificatesPrivateKeys, + UtilsResource::CertificatesTemplates, + ] { + assert!(extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::PUT) + .to_http_request(), + &resource, + ) + .is_none()); + + assert!(extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("resource_id", resource_id.to_string()) + .to_http_request(), + &resource, + ) + .is_none()); + } + } + + #[test] + fn can_extract_common_actions() { + let resource_id = uuid!("00000000-0000-0000-0000-000000000000"); + for resource in [ + UtilsResource::CertificatesPrivateKeys, + UtilsResource::CertificatesTemplates, + ] { + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::GET) + .to_http_request(), + &resource, + ), + Some(UtilsAction::List) + ); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .to_http_request(), + &resource, + ), + Some(UtilsAction::Create) + ); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::GET) + .param("resource_id", resource_id.to_string()) + .to_http_request(), + &resource, + ), + Some(UtilsAction::Get { resource_id }) + ); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::PUT) + .param("resource_id", resource_id.to_string()) + .to_http_request(), + &resource, + ), + Some(UtilsAction::Update { resource_id }) + ); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::DELETE) + .param("resource_id", resource_id.to_string()) + .to_http_request(), + &resource, + ), + Some(UtilsAction::Delete { resource_id }) + ); + } + } + + #[test] + fn can_extract_certificates_templates_actions() { + let resource = UtilsResource::CertificatesTemplates; + let resource_id = uuid!("00000000-0000-0000-0000-000000000000"); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("resource_id", resource_id.to_string()) + .param("resource_operation", "generate") + .to_http_request(), + &resource, + ), + Some(UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesTemplateGenerate + }) + ); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("resource_id", resource_id.to_string()) + .param("resource_operation", "share") + .to_http_request(), + &resource, + ), + Some(UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesTemplateShare + }) + ); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("resource_id", resource_id.to_string()) + .param("resource_operation", "unshare") + .to_http_request(), + &resource, + ), + Some(UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesTemplateUnshare + }) + ); + } + + #[test] + fn can_extract_certificates_private_keys_action() { + let resource = UtilsResource::CertificatesPrivateKeys; + let resource_id = uuid!("00000000-0000-0000-0000-000000000000"); + + assert_eq!( + extract_action( + &TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("resource_id", resource_id.to_string()) + .param("resource_operation", "export") + .to_http_request(), + &resource, + ), + Some(UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesPrivateKeyExport + }) + ); + } + + #[actix_rt::test] + async fn can_extract_user() -> anyhow::Result<()> { + let resource_id = uuid!("00000000-0000-0000-0000-000000000001"); + let resource = UtilsResource::CertificatesTemplates; + let action = UtilsAction::Get { resource_id }; + + let api = mock_api().await?; + + // Insert user into the database. + let user = mock_user_with_id(1)?; + let users = api.users(); + users.upsert(&user).await?; + + // No user information. + assert!(extract_user(&api, None, None, &action, &resource) + .await? + .is_none()); + + // Only current user is provided. + let extracted_user = + extract_user(&api, Some(user.clone()), None, &action, &resource).await?; + assert_eq!(extracted_user.unwrap().id, user.id); + + // Both current user and user share that belongs to that user were provided. + let extracted_user = extract_user( + &api, + Some(user.clone()), + Some(UserShare { + id: UserShareId::new(), + user_id: user.id, + resource: SharedResource::CertificateTemplate { + template_id: resource_id, + }, + created_at: OffsetDateTime::now_utc(), + }), + &action, + &resource, + ) + .await?; + assert_eq!(extracted_user.unwrap().id, user.id); + + // Both current user and user share that doesn't belong to that user were provided. + let another_user = mock_user_with_id(2)?; + users.upsert(&another_user).await?; + let extracted_user = extract_user( + &api, + Some(user.clone()), + Some(UserShare { + id: UserShareId::new(), + user_id: another_user.id, + resource: SharedResource::CertificateTemplate { + template_id: resource_id, + }, + created_at: OffsetDateTime::now_utc(), + }), + &action, + &resource, + ) + .await?; + assert_eq!(extracted_user.unwrap().id, another_user.id); + + // Anonymous user. + let extracted_user = extract_user( + &api, + None, + Some(UserShare { + id: UserShareId::new(), + user_id: another_user.id, + resource: SharedResource::CertificateTemplate { + template_id: resource_id, + }, + created_at: OffsetDateTime::now_utc(), + }), + &action, + &resource, + ) + .await?; + assert_eq!(extracted_user.unwrap().id, another_user.id); + + // Current user isn't authorized. + let another_user = mock_user_with_id(2)?; + users.upsert(&another_user).await?; + let extracted_user = extract_user( + &api, + Some(user.clone()), + Some(UserShare { + id: UserShareId::new(), + user_id: another_user.id, + resource: SharedResource::CertificateTemplate { + template_id: resource_id, + }, + created_at: OffsetDateTime::now_utc(), + }), + &UtilsAction::Create, + &resource, + ) + .await?; + assert!(extracted_user.is_none()); + + // Anonymous user is not authorized. + let extracted_user = extract_user( + &api, + None, + Some(UserShare { + id: UserShareId::new(), + user_id: another_user.id, + resource: SharedResource::CertificateTemplate { + template_id: resource_id, + }, + created_at: OffsetDateTime::now_utc(), + }), + &UtilsAction::Create, + &resource, + ) + .await?; + assert!(extracted_user.is_none()); + + Ok(()) + } + + #[actix_rt::test] + async fn fail_if_resource_is_not_valid() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::GET) + .param("area", "certificates") + .param("resource", "unknown") + .to_http_request(); + assert_debug_snapshot!( + utils_action(web::Data::new(app_state), Some(user), None, request, None).await, + @r###" + Ok( + HttpResponse { + error: None, + res: + Response HTTP/1.1 404 Not Found + headers: + body: Sized(0) + , + }, + ) + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn fail_if_action_is_not_valid() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::DELETE) + .param("area", "certificates") + .param("resource", "private_keys") + .to_http_request(); + assert_debug_snapshot!( + utils_action(web::Data::new(app_state), Some(user), None, request, None).await, + @r###" + Ok( + HttpResponse { + error: None, + res: + Response HTTP/1.1 404 Not Found + headers: + body: Sized(0) + , + }, + ) + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn fail_if_action_requires_body_but_not_provided() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("area", "certificates") + .param("resource", "private_keys") + .to_http_request(); + assert_debug_snapshot!( + utils_action(web::Data::new(app_state), Some(user), None, request, None).await, + @r###" + Ok( + HttpResponse { + error: None, + res: + Response HTTP/1.1 400 Bad Request + headers: + body: Sized(0) + , + }, + ) + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn fail_if_user_is_not_authenticated() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::GET) + .param("area", "certificates") + .param("resource", "private_keys") + .to_http_request(); + assert_debug_snapshot!( + utils_action(web::Data::new(app_state), None, None, request, None).await, + @r###" + Err( + "Access Forbidden", + ) + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn fail_if_action_parameters_are_invalid() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("area", "certificates") + .param("resource", "private_keys") + .to_http_request(); + let body = web::Json(json!({})); + assert_debug_snapshot!( + utils_action(web::Data::new(app_state), Some(user), None, request, Some(body)).await, + @r###" + Err( + Error { + context: "Invalid action parameters.", + source: Error("missing field `keyName`", line: 0, column: 0), + }, + ) + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn can_return_json_value() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::POST) + .param("area", "certificates") + .param("resource", "private_keys") + .to_http_request(); + let body = web::Json(json!({ "keyName": "pk", "alg": { "keyType": "ed25519" } })); + let response = utils_action( + web::Data::new(app_state), + Some(user), + None, + request, + Some(body), + ) + .await?; + assert_eq!(response.status(), 200); + assert_debug_snapshot!(response.headers(), @r###" + HeaderMap { + inner: { + "content-type": Value { + inner: [ + "application/json", + ], + }, + }, + } + "###); + + assert!(!response.into_body().try_into_bytes().unwrap().is_empty()); + + Ok(()) + } + + #[actix_rt::test] + async fn can_return_no_value() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let certificates = app_state.api.certificates(); + let private_key = certificates + .create_private_key( + user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::PUT) + .param("area", "certificates") + .param("resource", "private_keys") + .param("resource_id", private_key.id.to_string()) + .to_http_request(); + let body = web::Json(json!({ "keyName": "pk-new" })); + assert_debug_snapshot!( + utils_action( + web::Data::new(app_state), + Some(user), + None, + request, + Some(body), + ) + .await, + @r###" + Ok( + HttpResponse { + error: None, + res: + Response HTTP/1.1 204 No Content + headers: + body: Sized(0) + , + }, + ) + "### + ); + + Ok(()) + } + + #[actix_rt::test] + async fn can_return_not_found() -> anyhow::Result<()> { + let app_state = mock_app_state().await?; + + let user = mock_user()?; + let users = app_state.api.users(); + users.upsert(&user).await?; + + let non_existent_id = uuid!("00000000-0000-0000-0000-000000000001"); + let request = TestRequest::with_uri("https://secutils.dev/api/utils") + .method(Method::GET) + .param("area", "certificates") + .param("resource", "private_keys") + .param("resource_id", non_existent_id.to_string()) + .to_http_request(); + assert_debug_snapshot!( + utils_action( + web::Data::new(app_state), + Some(user), + None, + request, + None, + ) + .await, + @r###" + Ok( + HttpResponse { + error: None, + res: + Response HTTP/1.1 404 Not Found + headers: + body: Sized(0) + , + }, + ) + "### + ); + + Ok(()) + } +} diff --git a/src/server/handlers/utils_handle_action.rs b/src/server/handlers/utils_handle_action.rs index a18d18a..7338f41 100644 --- a/src/server/handlers/utils_handle_action.rs +++ b/src/server/handlers/utils_handle_action.rs @@ -2,14 +2,14 @@ use crate::{ error::Error as SecutilsError, server::AppState, users::{User, UserShare}, - utils::UtilsAction, + utils::UtilsLegacyAction, }; use actix_web::{web, HttpResponse}; use serde::Deserialize; #[derive(Deserialize)] pub struct BodyParams { - action: UtilsAction, + action: UtilsLegacyAction, } pub async fn utils_handle_action( @@ -33,7 +33,7 @@ pub async fn utils_handle_action( // If action is targeting a shared resource that doesn't belong to currently authenticated // user or user isn't authenticated, act on behalf of the shared resource owner assuming // action is authorized to be performed on a shared resource. - (_, Some(user_share)) if user_share.is_action_authorized(&action) => { + (_, Some(user_share)) if user_share.is_legacy_action_authorized(&action) => { // If user isn't found forbid any actions on the shared resource. if let Some(user) = state.api.users().get(user_share.user_id).await? { user diff --git a/src/utils.rs b/src/utils.rs index 1268491..d8d35b7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,22 +4,33 @@ mod database_ext; mod user_share_ext; mod util; mod utils_action; +mod utils_action_params; mod utils_action_result; mod utils_action_validation; +mod utils_legacy_action; +mod utils_legacy_action_result; +mod utils_resource; +mod utils_resource_operation; mod web_scraping; mod web_security; mod webhooks; pub use self::{ certificates::{ - CertificateAttributes, CertificateTemplate, CertificatesApi, ExportFormat, - ExtendedKeyUsage, KeyUsage, PrivateKey, PrivateKeyAlgorithm, PrivateKeyEllipticCurve, - PrivateKeySize, SignatureAlgorithm, UtilsCertificatesAction, UtilsCertificatesActionResult, - Version, + certificates_handle_action, CertificateAttributes, CertificateTemplate, CertificatesApi, + ExportFormat, ExtendedKeyUsage, KeyUsage, PrivateKey, PrivateKeyAlgorithm, + PrivateKeyEllipticCurve, PrivateKeySize, PrivateKeysCreateParams, PrivateKeysExportParams, + PrivateKeysUpdateParams, SignatureAlgorithm, TemplatesCreateParams, + TemplatesGenerateParams, TemplatesUpdateParams, Version, }, util::Util, utils_action::UtilsAction, + utils_action_params::UtilsActionParams, utils_action_result::UtilsActionResult, + utils_legacy_action::UtilsLegacyAction, + utils_legacy_action_result::UtilsLegacyActionResult, + utils_resource::UtilsResource, + utils_resource_operation::UtilsResourceOperation, web_scraping::{ UtilsWebScrapingAction, UtilsWebScrapingActionResult, WebPageResource, WebPageResourceContent, WebPageResourceContentData, WebPageResourceDiffStatus, diff --git a/src/utils/certificates.rs b/src/utils/certificates.rs index 54f6230..ae7ecc4 100644 --- a/src/utils/certificates.rs +++ b/src/utils/certificates.rs @@ -2,23 +2,824 @@ mod certificate_templates; mod database_ext; mod export_format; mod private_keys; -mod utils_certificates_action; -mod utils_certificates_action_result; mod x509; mod api_ext; +use crate::{ + api::Api, + error::Error as SecutilsError, + network::{DnsResolver, EmailTransport}, + users::{ClientUserShare, SharedResource, User}, + utils::{ + UtilsAction, UtilsActionParams, UtilsActionResult, UtilsResource, UtilsResourceOperation, + }, +}; +use serde::de::DeserializeOwned; +use serde_json::json; + pub use self::{ - api_ext::CertificatesApi, + api_ext::{ + CertificatesApi, PrivateKeysCreateParams, PrivateKeysExportParams, PrivateKeysUpdateParams, + TemplatesCreateParams, TemplatesGenerateParams, TemplatesUpdateParams, + }, certificate_templates::{CertificateAttributes, CertificateTemplate}, export_format::ExportFormat, private_keys::{PrivateKey, PrivateKeyAlgorithm, PrivateKeyEllipticCurve, PrivateKeySize}, - utils_certificates_action::UtilsCertificatesAction, - utils_certificates_action_result::UtilsCertificatesActionResult, x509::{ExtendedKeyUsage, KeyUsage, SignatureAlgorithm, Version}, }; +fn extract_params(params: Option) -> anyhow::Result { + params + .ok_or_else(|| SecutilsError::client("Missing required action parameters."))? + .into_inner() +} + +pub async fn certificates_handle_action( + user: User, + api: &Api, + action: UtilsAction, + resource: UtilsResource, + params: Option, +) -> anyhow::Result { + let certificates = api.certificates(); + match (resource, action) { + (UtilsResource::CertificatesPrivateKeys, UtilsAction::List) => { + UtilsActionResult::json(certificates.get_private_keys(user.id).await?) + } + (UtilsResource::CertificatesPrivateKeys, UtilsAction::Get { resource_id }) => { + if let Some(private_key) = certificates.get_private_key(user.id, resource_id).await? { + UtilsActionResult::json(private_key) + } else { + Ok(UtilsActionResult::empty()) + } + } + (UtilsResource::CertificatesPrivateKeys, UtilsAction::Create) => UtilsActionResult::json( + certificates + .create_private_key(user.id, extract_params(params)?) + .await?, + ), + (UtilsResource::CertificatesPrivateKeys, UtilsAction::Update { resource_id }) => { + certificates + .update_private_key(user.id, resource_id, extract_params(params)?) + .await?; + Ok(UtilsActionResult::empty()) + } + (UtilsResource::CertificatesPrivateKeys, UtilsAction::Delete { resource_id }) => { + certificates + .remove_private_key(user.id, resource_id) + .await?; + Ok(UtilsActionResult::empty()) + } + ( + UtilsResource::CertificatesPrivateKeys, + UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesPrivateKeyExport, + }, + ) => UtilsActionResult::json( + certificates + .export_private_key(user.id, resource_id, extract_params(params)?) + .await?, + ), + // Certificate templates. + (UtilsResource::CertificatesTemplates, UtilsAction::List) => { + UtilsActionResult::json(certificates.get_certificate_templates(user.id).await?) + } + (UtilsResource::CertificatesTemplates, UtilsAction::Get { resource_id }) => { + let users = api.users(); + let Some(template) = certificates + .get_certificate_template(user.id, resource_id) + .await? + else { + return Ok(UtilsActionResult::empty()); + }; + + UtilsActionResult::json(json!({ + "template": template, + "user_share": users + .get_user_share_by_resource( + user.id, + &SharedResource::certificate_template(resource_id), + ) + .await? + .map(ClientUserShare::from), + })) + } + (UtilsResource::CertificatesTemplates, UtilsAction::Create) => UtilsActionResult::json( + certificates + .create_certificate_template(user.id, extract_params(params)?) + .await?, + ), + (UtilsResource::CertificatesTemplates, UtilsAction::Update { resource_id }) => { + certificates + .update_certificate_template(user.id, resource_id, extract_params(params)?) + .await?; + Ok(UtilsActionResult::empty()) + } + (UtilsResource::CertificatesTemplates, UtilsAction::Delete { resource_id }) => { + certificates + .remove_certificate_template(user.id, resource_id) + .await?; + Ok(UtilsActionResult::empty()) + } + ( + UtilsResource::CertificatesTemplates, + UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesTemplateGenerate, + }, + ) => UtilsActionResult::json( + certificates + .generate_self_signed_certificate(user.id, resource_id, extract_params(params)?) + .await?, + ), + ( + UtilsResource::CertificatesTemplates, + UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesTemplateShare, + }, + ) => UtilsActionResult::json( + certificates + .share_certificate_template(user.id, resource_id) + .await + .map(ClientUserShare::from)?, + ), + ( + UtilsResource::CertificatesTemplates, + UtilsAction::Execute { + resource_id, + operation: UtilsResourceOperation::CertificatesTemplateUnshare, + }, + ) => UtilsActionResult::json( + certificates + .unshare_certificate_template(user.id, resource_id) + .await + .map(|user_share| user_share.map(ClientUserShare::from))?, + ), + + _ => Err(SecutilsError::client("Invalid resource or action.").into()), + } +} + #[cfg(test)] pub mod tests { pub use super::certificate_templates::tests::*; + use super::certificates_handle_action; + use crate::{ + tests::{mock_api, mock_user}, + users::{SharedResource, UserShareId}, + utils::{ + CertificateAttributes, CertificateTemplate, ExtendedKeyUsage, KeyUsage, PrivateKey, + PrivateKeyAlgorithm, PrivateKeySize, PrivateKeysCreateParams, SignatureAlgorithm, + TemplatesCreateParams, UtilsAction, UtilsActionParams, UtilsResource, + UtilsResourceOperation, Version, + }, + }; + use serde::Deserialize; + use serde_json::json; + use time::OffsetDateTime; + use uuid::uuid; + + fn get_mock_certificate_attributes() -> anyhow::Result { + Ok(CertificateAttributes { + common_name: Some("my-common-name".to_string()), + country: Some("DE".to_string()), + state_or_province: Some("BE".to_string()), + locality: None, + organization: None, + organizational_unit: None, + key_algorithm: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size1024, + }, + signature_algorithm: SignatureAlgorithm::Sha256, + not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, + not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, + version: Version::One, + is_ca: true, + key_usage: Some([KeyUsage::KeyAgreement].into_iter().collect()), + extended_key_usage: Some([ExtendedKeyUsage::EmailProtection].into_iter().collect()), + }) + } + + #[actix_rt::test] + async fn can_list_private_keys() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk-2".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + + let serialized_private_keys = certificates_handle_action( + mock_user, + &api, + UtilsAction::List, + UtilsResource::CertificatesPrivateKeys, + None, + ) + .await?; + + let private_keys = serde_json::from_value::>( + serialized_private_keys.into_inner().unwrap(), + )?; + assert_eq!(private_keys.len(), 2); + + Ok(()) + } + + #[actix_rt::test] + async fn can_retrieve_private_key() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let private_key_original = certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + + let serialized_private_key = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Get { + resource_id: private_key_original.id, + }, + UtilsResource::CertificatesPrivateKeys, + None, + ) + .await?; + + let private_key = + serde_json::from_value::(serialized_private_key.into_inner().unwrap())?; + assert_eq!(private_key_original.id, private_key.id); + assert_eq!(private_key_original.name, private_key.name); + + let empty_result = certificates_handle_action( + mock_user, + &api, + UtilsAction::Get { + resource_id: uuid!("00000000-0000-0000-0000-000000000000"), + }, + UtilsResource::CertificatesPrivateKeys, + None, + ) + .await?; + assert!(empty_result.into_inner().is_none()); + + Ok(()) + } + + #[actix_rt::test] + async fn can_create_private_key() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let serialized_private_key = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Create, + UtilsResource::CertificatesPrivateKeys, + Some(UtilsActionParams::json(json!({ + "keyName": "pk", + "alg": { "keyType": "ed25519" }, + }))), + ) + .await?; + let private_key = + serde_json::from_value::(serialized_private_key.into_inner().unwrap())?; + assert_eq!(private_key.name, "pk"); + assert!(matches!(private_key.alg, PrivateKeyAlgorithm::Ed25519)); + + let private_key = api + .certificates() + .get_private_key(mock_user.id, private_key.id) + .await? + .unwrap(); + assert_eq!(private_key.name, "pk"); + + Ok(()) + } + + #[actix_rt::test] + async fn can_update_private_key() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let private_key_original = certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + + let empty_result = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Update { + resource_id: private_key_original.id, + }, + UtilsResource::CertificatesPrivateKeys, + Some(UtilsActionParams::json(json!({ + "keyName": "pk-new", + }))), + ) + .await?; + assert!(empty_result.into_inner().is_none()); + + let private_key = api + .certificates() + .get_private_key(mock_user.id, private_key_original.id) + .await? + .unwrap(); + assert_eq!(private_key.name, "pk-new"); + + Ok(()) + } + + #[actix_rt::test] + async fn can_delete_private_key() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let private_key = certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + + let empty_result = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Delete { + resource_id: private_key.id, + }, + UtilsResource::CertificatesPrivateKeys, + None, + ) + .await?; + assert!(empty_result.into_inner().is_none()); + + assert!(api + .certificates() + .get_private_key(mock_user.id, private_key.id) + .await? + .is_none()); + + Ok(()) + } + + #[actix_rt::test] + async fn can_export_private_key() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let private_key_original = certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + + let export_result = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Execute { + resource_id: private_key_original.id, + operation: UtilsResourceOperation::CertificatesPrivateKeyExport, + }, + UtilsResource::CertificatesPrivateKeys, + Some(UtilsActionParams::json(json!({ + "format": "pem", + }))), + ) + .await?; + + let export_result = serde_json::from_value::>(export_result.into_inner().unwrap())?; + assert_eq!(export_result.len(), 119); + + Ok(()) + } + + #[actix_rt::test] + async fn can_list_templates() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct-2".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + + let serialized_templates = certificates_handle_action( + mock_user, + &api, + UtilsAction::List, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + + let templates = serde_json::from_value::>( + serialized_templates.into_inner().unwrap(), + )?; + assert_eq!(templates.len(), 2); + + Ok(()) + } + + #[actix_rt::test] + async fn can_retrieve_template() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let template_original = certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + + let serialized_template = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Get { + resource_id: template_original.id, + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + + #[derive(Deserialize)] + struct UserShareWrapper { + id: UserShareId, + } + + #[derive(Deserialize)] + struct TemplateWrapper { + template: CertificateTemplate, + user_share: Option, + } + + let template = + serde_json::from_value::(serialized_template.into_inner().unwrap())?; + assert_eq!(template_original.id, template.template.id); + assert_eq!(template_original.name, template.template.name); + assert!(template.user_share.is_none()); + + // Share template. + let user_share = certificates + .share_certificate_template(mock_user.id, template_original.id) + .await?; + let serialized_template = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Get { + resource_id: template_original.id, + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + let template = + serde_json::from_value::(serialized_template.into_inner().unwrap())?; + assert_eq!(template_original.id, template.template.id); + assert_eq!(template_original.name, template.template.name); + assert_eq!(template.user_share.unwrap().id, user_share.id); + + let empty_result = certificates_handle_action( + mock_user, + &api, + UtilsAction::Get { + resource_id: uuid!("00000000-0000-0000-0000-000000000000"), + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + assert!(empty_result.into_inner().is_none()); + + Ok(()) + } + + #[actix_rt::test] + async fn can_create_template() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let serialized_template = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Create, + UtilsResource::CertificatesTemplates, + Some(UtilsActionParams::json(json!({ + "templateName": "ct", + "attributes": serde_json::to_value(get_mock_certificate_attributes()?)?, + }))), + ) + .await?; + let template = serde_json::from_value::( + serialized_template.into_inner().unwrap(), + )?; + assert_eq!(template.name, "ct"); + assert_eq!(template.attributes, get_mock_certificate_attributes()?); + + let template = api + .certificates() + .get_certificate_template(mock_user.id, template.id) + .await? + .unwrap(); + assert_eq!(template.name, "ct"); + + Ok(()) + } + + #[actix_rt::test] + async fn can_update_template() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let template_original = certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + + let empty_result = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Update { + resource_id: template_original.id, + }, + UtilsResource::CertificatesTemplates, + Some(UtilsActionParams::json(json!({ + "templateName": "ct-new", + }))), + ) + .await?; + assert!(empty_result.into_inner().is_none()); + + let template = api + .certificates() + .get_certificate_template(mock_user.id, template_original.id) + .await? + .unwrap(); + assert_eq!(template.name, "ct-new"); + + Ok(()) + } + + #[actix_rt::test] + async fn can_delete_template() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let template = certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + + let empty_result = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Delete { + resource_id: template.id, + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + assert!(empty_result.into_inner().is_none()); + + assert!(api + .certificates() + .get_certificate_template(mock_user.id, template.id) + .await? + .is_none()); + + Ok(()) + } + + #[actix_rt::test] + async fn can_generate_key_pair() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let template_original = certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + + let generate_result = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Execute { + resource_id: template_original.id, + operation: UtilsResourceOperation::CertificatesTemplateGenerate, + }, + UtilsResource::CertificatesTemplates, + Some(UtilsActionParams::json(json!({ + "format": "pem", + }))), + ) + .await?; + + let generate_result = + serde_json::from_value::>(generate_result.into_inner().unwrap())?; + assert!(generate_result.len() > 1000); + + Ok(()) + } + + #[actix_rt::test] + async fn can_share_and_unshare_template() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = api.certificates(); + let template = certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + + let serialized_user_share = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Execute { + resource_id: template.id, + operation: UtilsResourceOperation::CertificatesTemplateShare, + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + + #[derive(Deserialize)] + struct UserShareWrapper { + id: UserShareId, + } + + let UserShareWrapper { id: user_share_id } = serde_json::from_value::( + serialized_user_share.into_inner().unwrap(), + )?; + assert_eq!( + api.users() + .get_user_share(user_share_id) + .await? + .unwrap() + .resource, + SharedResource::CertificateTemplate { + template_id: template.id + } + ); + + let serialized_user_share = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Execute { + resource_id: template.id, + operation: UtilsResourceOperation::CertificatesTemplateUnshare, + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + + let UserShareWrapper { + id: user_unshare_id, + } = serde_json::from_value::( + serialized_user_share.into_inner().unwrap(), + )?; + assert_eq!(user_unshare_id, user_share_id); + assert!(api.users().get_user_share(user_share_id).await?.is_none()); + + let serialized_user_share = certificates_handle_action( + mock_user.clone(), + &api, + UtilsAction::Execute { + resource_id: template.id, + operation: UtilsResourceOperation::CertificatesTemplateUnshare, + }, + UtilsResource::CertificatesTemplates, + None, + ) + .await?; + + let user_unshare = serde_json::from_value::>( + serialized_user_share.into_inner().unwrap(), + )?; + assert!(user_unshare.is_none()); + assert!(api.users().get_user_share(user_share_id).await?.is_none()); + + Ok(()) + } } diff --git a/src/utils/certificates/api_ext.rs b/src/utils/certificates/api_ext.rs index 400b44e..899996e 100644 --- a/src/utils/certificates/api_ext.rs +++ b/src/utils/certificates/api_ext.rs @@ -1,11 +1,26 @@ +mod private_keys_create_params; +mod private_keys_export_params; +mod private_keys_update_params; +mod templates_create_params; +mod templates_generate_params; +mod templates_update_params; + +pub use self::{ + private_keys_create_params::PrivateKeysCreateParams, + private_keys_export_params::PrivateKeysExportParams, + private_keys_update_params::PrivateKeysUpdateParams, + templates_create_params::TemplatesCreateParams, + templates_generate_params::TemplatesGenerateParams, + templates_update_params::TemplatesUpdateParams, +}; use crate::{ api::Api, error::Error as SecutilsError, network::{DnsResolver, EmailTransport}, users::{SharedResource, UserId, UserShare}, utils::{ - CertificateAttributes, CertificateTemplate, ExportFormat, ExtendedKeyUsage, KeyUsage, - PrivateKey, PrivateKeyAlgorithm, SignatureAlgorithm, + utils_action_validation::MAX_UTILS_ENTITY_NAME_LENGTH, CertificateTemplate, ExportFormat, + ExtendedKeyUsage, KeyUsage, PrivateKey, PrivateKeyAlgorithm, SignatureAlgorithm, }, }; use anyhow::{anyhow, bail}; @@ -59,16 +74,19 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { pub async fn create_private_key( &self, user_id: UserId, - name: impl Into, - alg: PrivateKeyAlgorithm, - passphrase: Option<&str>, + params: PrivateKeysCreateParams, ) -> anyhow::Result { + Self::assert_private_key_name(¶ms.key_name)?; + let private_key = PrivateKey { id: Uuid::now_v7(), - name: name.into(), - alg, - pkcs8: Self::export_private_key_to_pkcs8(Self::generate_private_key(alg)?, passphrase)?, - encrypted: passphrase.is_some(), + name: params.key_name, + alg: params.alg, + pkcs8: Self::export_private_key_to_pkcs8( + Self::generate_private_key(params.alg)?, + params.passphrase.as_deref(), + )?, + encrypted: params.passphrase.is_some(), // Preserve timestamp only up to seconds. created_at: OffsetDateTime::from_unix_timestamp( OffsetDateTime::now_utc().unix_timestamp(), @@ -89,10 +107,22 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { &self, user_id: UserId, id: Uuid, - name: Option<&str>, - passphrase: Option<&str>, - new_passphrase: Option<&str>, + params: PrivateKeysUpdateParams, ) -> anyhow::Result<()> { + let includes_new_passphrase = + params.passphrase.is_some() || params.new_passphrase.is_some(); + if params.key_name.is_none() && !includes_new_passphrase { + bail!(SecutilsError::client(format!( + "Either new name or passphrase should be provided ({id})." + ))); + } + + if includes_new_passphrase && params.passphrase == params.new_passphrase { + bail!(SecutilsError::client(format!( + "New private key passphrase should be different from the current passphrase ({id})." + ))); + } + let Some(private_key) = self.get_private_key(user_id, id).await? else { bail!(SecutilsError::client(format!( "Private key ('{id}') is not found." @@ -100,25 +130,30 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { }; // If name update is needed, extract it from parameters. - let name = if let Some(name) = name { + let name = if let Some(name) = params.key_name { + Self::assert_private_key_name(&name)?; name.to_string() } else { private_key.name }; // If passphrase update is needed, try to decrypt private key using the provided passphrase. - let (pkcs8, encrypted) = if passphrase != new_passphrase { - let pkcs8_private_key = - Self::import_private_key_from_pkcs8(&private_key.pkcs8, passphrase).map_err( - |err| { - SecutilsError::client_with_root_cause(anyhow!(err).context(format!( - "Unable to decrypt private key ('{id}') with the provided passphrase." - ))) - }, - )?; + let (pkcs8, encrypted) = if params.passphrase != params.new_passphrase { + let pkcs8_private_key = Self::import_private_key_from_pkcs8( + &private_key.pkcs8, + params.passphrase.as_deref(), + ) + .map_err(|err| { + SecutilsError::client_with_root_cause(anyhow!(err).context(format!( + "Unable to decrypt private key ('{id}') with the provided passphrase." + ))) + })?; ( - Self::export_private_key_to_pkcs8(pkcs8_private_key, new_passphrase)?, - new_passphrase.is_some(), + Self::export_private_key_to_pkcs8( + pkcs8_private_key, + params.new_passphrase.as_deref(), + )?, + params.new_passphrase.is_some(), ) } else { (private_key.pkcs8, private_key.encrypted) @@ -153,9 +188,7 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { &self, user_id: UserId, id: Uuid, - format: ExportFormat, - passphrase: Option<&str>, - export_passphrase: Option<&str>, + params: PrivateKeysExportParams, ) -> anyhow::Result> { let Some(private_key) = self.get_private_key(user_id, id).await? else { bail!(SecutilsError::client(format!( @@ -164,30 +197,34 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { }; // Try to decrypt private key using the provided passphrase. - let pkcs8_private_key = Self::import_private_key_from_pkcs8(&private_key.pkcs8, passphrase) - .map_err(|err| { + let pkcs8_private_key = + Self::import_private_key_from_pkcs8(&private_key.pkcs8, params.passphrase.as_deref()) + .map_err(|err| { SecutilsError::client_with_root_cause(anyhow!(err).context(format!( "Unable to decrypt private key ('{id}') with the provided passphrase." ))) })?; - let export_result = match format { - ExportFormat::Pem => { - Self::export_private_key_to_pem(pkcs8_private_key, export_passphrase) - } - ExportFormat::Pkcs8 => { - Self::export_private_key_to_pkcs8(pkcs8_private_key, export_passphrase) - } + let export_result = match params.format { + ExportFormat::Pem => Self::export_private_key_to_pem( + pkcs8_private_key, + params.export_passphrase.as_deref(), + ), + ExportFormat::Pkcs8 => Self::export_private_key_to_pkcs8( + pkcs8_private_key, + params.export_passphrase.as_deref(), + ), ExportFormat::Pkcs12 => Self::export_private_key_to_pkcs12( &private_key.name, &pkcs8_private_key, - export_passphrase, + params.export_passphrase.as_deref(), ), }; export_result.map_err(|err| { SecutilsError::client_with_root_cause(anyhow!(err).context(format!( - "Unable to export private key ('{id}') to the specified format ('{format:?}')." + "Unable to export private key ('{id}') to the specified format ('{:?}').", + params.format ))) .into() }) @@ -215,13 +252,14 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { pub async fn create_certificate_template( &self, user_id: UserId, - name: impl Into, - attributes: CertificateAttributes, + params: TemplatesCreateParams, ) -> anyhow::Result { + Self::assert_certificate_template_name(¶ms.template_name)?; + let certificate_template = CertificateTemplate { id: Uuid::now_v7(), - name: name.into(), - attributes, + name: params.template_name, + attributes: params.attributes, // Preserve timestamp only up to seconds. created_at: OffsetDateTime::from_unix_timestamp( OffsetDateTime::now_utc().unix_timestamp(), @@ -242,9 +280,16 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { &self, user_id: UserId, id: Uuid, - name: Option, - attributes: Option, + params: TemplatesUpdateParams, ) -> anyhow::Result<()> { + if let Some(name) = ¶ms.template_name { + Self::assert_certificate_template_name(name)?; + } else if params.attributes.is_none() { + bail!(SecutilsError::client(format!( + "Either new name or attributes should be provided ({id})." + ))); + } + let Some(certificate_template) = self.get_certificate_template(user_id, id).await? else { bail!(SecutilsError::client(format!( "Certificate template ('{id}') is not found." @@ -257,12 +302,12 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { .update_certificate_template( user_id, &CertificateTemplate { - name: if let Some(name) = name { + name: if let Some(name) = params.template_name { name } else { certificate_template.name }, - attributes: if let Some(attributes) = attributes { + attributes: if let Some(attributes) = params.attributes { attributes } else { certificate_template.attributes @@ -292,8 +337,7 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { &self, user_id: UserId, template_id: Uuid, - format: ExportFormat, - passphrase: Option<&str>, + params: TemplatesGenerateParams, ) -> anyhow::Result> { let Some(certificate_template) = self.get_certificate_template(user_id, template_id).await? @@ -319,16 +363,20 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { )?; let certificate = certificate_builder.build(); - Ok(match format { - ExportFormat::Pem => { - Self::export_key_pair_to_pem_archive(certificate, private_key, passphrase)? + Ok(match params.format { + ExportFormat::Pem => Self::export_key_pair_to_pem_archive( + certificate, + private_key, + params.passphrase.as_deref(), + )?, + ExportFormat::Pkcs8 => { + Self::export_private_key_to_pkcs8(private_key, params.passphrase.as_deref())? } - ExportFormat::Pkcs8 => Self::export_private_key_to_pkcs8(private_key, passphrase)?, ExportFormat::Pkcs12 => Self::export_key_pair_to_pkcs12( &certificate_template.name, &private_key, &certificate, - passphrase, + params.passphrase.as_deref(), )?, }) } @@ -686,6 +734,38 @@ impl<'a, DR: DnsResolver, ET: EmailTransport> CertificatesApi<'a, DR, ET> { Ok(()) } + + fn assert_private_key_name(name: &str) -> Result<(), SecutilsError> { + if name.is_empty() { + return Err(SecutilsError::client("Private key name cannot be empty.")); + } + + if name.len() > MAX_UTILS_ENTITY_NAME_LENGTH { + return Err(SecutilsError::client(format!( + "Private key name cannot be longer than {} characters.", + MAX_UTILS_ENTITY_NAME_LENGTH + ))); + } + + Ok(()) + } + + fn assert_certificate_template_name(name: &str) -> Result<(), SecutilsError> { + if name.is_empty() { + return Err(SecutilsError::client( + "Certificate template name cannot be empty.", + )); + } + + if name.len() > MAX_UTILS_ENTITY_NAME_LENGTH { + return Err(SecutilsError::client(format!( + "Certificate template name cannot be longer than {} characters.", + MAX_UTILS_ENTITY_NAME_LENGTH + ))); + } + + Ok(()) + } } impl Api { @@ -701,8 +781,9 @@ mod tests { tests::{mock_api, mock_user, MockResolver}, utils::{ CertificateAttributes, CertificatesApi, ExportFormat, ExtendedKeyUsage, KeyUsage, - PrivateKeyAlgorithm, PrivateKeyEllipticCurve, PrivateKeySize, SignatureAlgorithm, - Version, + PrivateKeyAlgorithm, PrivateKeyEllipticCurve, PrivateKeySize, PrivateKeysCreateParams, + PrivateKeysExportParams, PrivateKeysUpdateParams, SignatureAlgorithm, + TemplatesCreateParams, TemplatesGenerateParams, TemplatesUpdateParams, Version, }, }; use insta::assert_debug_snapshot; @@ -762,7 +843,14 @@ mod tests { (PrivateKeyAlgorithm::Ed25519, 256), ] { let private_key = certificates - .create_private_key(mock_user.id, format!("pk-{:?}-{:?}", alg, pass), alg, pass) + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: format!("pk-{:?}-{:?}", alg, pass), + alg, + passphrase: pass.map(|p| p.to_string()), + }, + ) .await?; assert_eq!(private_key.alg, alg); @@ -778,6 +866,53 @@ mod tests { Ok(()) } + #[actix_rt::test] + async fn fails_to_create_private_key_if_name_is_invalid() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = CertificatesApi::new(&api); + assert_debug_snapshot!( + certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await, + @r###" + Err( + "Private key name cannot be empty.", + ) + "### + ); + + assert_debug_snapshot!( + certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "a".repeat(101), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await, + @r###" + Err( + "Private key name cannot be longer than 100 characters.", + ) + "### + ); + + Ok(()) + } + #[actix_rt::test] async fn can_change_private_key_passphrase() -> anyhow::Result<()> { let api = mock_api().await?; @@ -787,7 +922,14 @@ mod tests { let certificates = CertificatesApi::new(&api); let private_key = certificates - .create_private_key(mock_user.id, "pk", PrivateKeyAlgorithm::Ed25519, None) + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) .await?; // Decrypting without password should succeed. @@ -801,7 +943,15 @@ mod tests { // Set passphrase. certificates - .update_private_key(mock_user.id, private_key.id, None, None, Some("pass")) + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: None, + passphrase: None, + new_passphrase: Some("pass".to_string()), + }, + ) .await?; // Decrypting without passphrase should fail. @@ -830,9 +980,11 @@ mod tests { .update_private_key( mock_user.id, private_key.id, - None, - Some("pass"), - Some("pass-1"), + PrivateKeysUpdateParams { + key_name: None, + passphrase: Some("pass".to_string()), + new_passphrase: Some("pass-1".to_string()), + }, ) .await?; @@ -872,7 +1024,15 @@ mod tests { // Remove passphrase. certificates - .update_private_key(mock_user.id, private_key.id, None, Some("pass-1"), None) + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: None, + passphrase: Some("pass-1".to_string()), + new_passphrase: None, + }, + ) .await?; // Decrypting without passphrase should succeed. @@ -923,15 +1083,25 @@ mod tests { let private_key = certificates .create_private_key( mock_user.id, - "pk", - PrivateKeyAlgorithm::Ed25519, - Some("pass"), + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: Some("pass".to_string()), + }, ) .await?; // Update name. certificates - .update_private_key(mock_user.id, private_key.id, Some("pk-new"), None, None) + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: Some("pk-new".to_string()), + passphrase: None, + new_passphrase: None, + }, + ) .await?; // Name should change, and pkcs8 shouldn't change. @@ -957,9 +1127,11 @@ mod tests { .update_private_key( mock_user.id, private_key.id, - Some("pk-new-new"), - Some("pass"), - Some("pass-1"), + PrivateKeysUpdateParams { + key_name: Some("pk-new-new".to_string()), + passphrase: Some("pass".to_string()), + new_passphrase: Some("pass-1".to_string()), + }, ) .await?; @@ -990,9 +1162,11 @@ mod tests { .update_private_key( mock_user.id, private_key.id, - Some("pk"), - Some("pass-1"), - None, + PrivateKeysUpdateParams { + key_name: Some("pk".to_string()), + passphrase: Some("pass-1".to_string()), + new_passphrase: None, + }, ) .await?; @@ -1013,6 +1187,107 @@ mod tests { Ok(()) } + #[actix_rt::test] + async fn fails_to_update_private_key_if_params_are_invalid() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = CertificatesApi::new(&api); + let private_key = certificates + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) + .await?; + // Invalid name. + assert_debug_snapshot!(certificates + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: Some("".to_string()), + passphrase: None, + new_passphrase: None, + }, + ) + .await, + @r###" + Err( + "Private key name cannot be empty.", + ) + "### + ); + + // Invalid name. + assert_debug_snapshot!(certificates + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: Some("a".repeat(101)), + passphrase: None, + new_passphrase: None, + }, + ) + .await, + @r###" + Err( + "Private key name cannot be longer than 100 characters.", + ) + "### + ); + + // Invalid params. + assert_eq!( + certificates + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: None, + passphrase: None, + new_passphrase: None, + }, + ) + .await + .unwrap_err() + .to_string(), + format!( + "Either new name or passphrase should be provided ({}).", + private_key.id + ) + ); + + // Invalid passphrases. + assert_eq!( + certificates + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: None, + passphrase: Some("some".to_string()), + new_passphrase: Some("some".to_string()), + }, + ) + .await + .unwrap_err() + .to_string(), + format!( + "New private key passphrase should be different from the current passphrase ({}).", + private_key.id + ) + ); + + Ok(()) + } + #[actix_rt::test] async fn can_export_private_key() -> anyhow::Result<()> { let api = mock_api().await?; @@ -1023,7 +1298,14 @@ mod tests { // Create private key without passphrase. let certificates = CertificatesApi::new(&api); let private_key = certificates - .create_private_key(mock_user.id, "pk", PrivateKeyAlgorithm::Ed25519, None) + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) .await?; // Export private key without passphrase and make sure it can be without passphrase. @@ -1031,9 +1313,11 @@ mod tests { .export_private_key( mock_user.id, private_key.id, - ExportFormat::Pkcs8, - None, - None, + PrivateKeysExportParams { + format: ExportFormat::Pkcs8, + passphrase: None, + export_passphrase: None, + }, ) .await?; assert!( @@ -1047,9 +1331,11 @@ mod tests { .export_private_key( mock_user.id, private_key.id, - ExportFormat::Pkcs8, - None, - Some("pass"), + PrivateKeysExportParams { + format: ExportFormat::Pkcs8, + passphrase: None, + export_passphrase: Some("pass".to_string()), + }, ) .await?; assert!( @@ -1062,7 +1348,15 @@ mod tests { // Set passphrase and repeat. certificates - .update_private_key(mock_user.id, private_key.id, None, None, Some("pass")) + .update_private_key( + mock_user.id, + private_key.id, + PrivateKeysUpdateParams { + key_name: None, + passphrase: None, + new_passphrase: Some("pass".to_string()), + }, + ) .await?; // Export private key without passphrase and make sure it can be without passphrase. @@ -1070,9 +1364,11 @@ mod tests { .export_private_key( mock_user.id, private_key.id, - ExportFormat::Pkcs8, - Some("pass"), - None, + PrivateKeysExportParams { + format: ExportFormat::Pkcs8, + passphrase: Some("pass".to_string()), + export_passphrase: None, + }, ) .await?; assert!( @@ -1086,9 +1382,11 @@ mod tests { .export_private_key( mock_user.id, private_key.id, - ExportFormat::Pkcs8, - Some("pass"), - Some("pass"), + PrivateKeysExportParams { + format: ExportFormat::Pkcs8, + passphrase: Some("pass".to_string()), + export_passphrase: Some("pass".to_string()), + }, ) .await?; assert!( @@ -1111,7 +1409,14 @@ mod tests { let certificates = CertificatesApi::new(&api); let private_key = certificates - .create_private_key(mock_user.id, "pk", PrivateKeyAlgorithm::Ed25519, None) + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) .await?; assert_eq!( private_key, @@ -1147,7 +1452,14 @@ mod tests { .is_empty()); let private_key_one = certificates - .create_private_key(mock_user.id, "pk", PrivateKeyAlgorithm::Ed25519, None) + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) .await .map(|mut private_key| { private_key.pkcs8.clear(); @@ -1159,7 +1471,14 @@ mod tests { ); let private_key_two = certificates - .create_private_key(mock_user.id, "pk-2", PrivateKeyAlgorithm::Ed25519, None) + .create_private_key( + mock_user.id, + PrivateKeysCreateParams { + key_name: "pk-2".to_string(), + alg: PrivateKeyAlgorithm::Ed25519, + passphrase: None, + }, + ) .await .map(|mut private_key| { private_key.pkcs8.clear(); @@ -1194,7 +1513,13 @@ mod tests { let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; assert_eq!(certificate_template.name, "ct"); assert_eq!( @@ -1205,6 +1530,49 @@ mod tests { Ok(()) } + #[actix_rt::test] + async fn fails_to_create_certificate_template_if_name_is_invalid() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = CertificatesApi::new(&api); + assert_debug_snapshot!(certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await, + @r###" + Err( + "Certificate template name cannot be empty.", + ) + "### + ); + + assert_debug_snapshot!(certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "a".repeat(101), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await, + @r###" + Err( + "Certificate template name cannot be longer than 100 characters.", + ) + "### + ); + + Ok(()) + } + #[actix_rt::test] async fn can_change_certificate_template_attributes() -> anyhow::Result<()> { let api = mock_api().await?; @@ -1214,7 +1582,13 @@ mod tests { let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; // Update attributes. @@ -1222,25 +1596,27 @@ mod tests { .update_certificate_template( mock_user.id, certificate_template.id, - None, - Some(CertificateAttributes { - common_name: Some("cn-new".to_string()), - country: Some("c".to_string()), - state_or_province: Some("s".to_string()), - locality: None, - organization: None, - organizational_unit: None, - key_algorithm: PrivateKeyAlgorithm::Ed25519, - signature_algorithm: SignatureAlgorithm::Md5, - not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, - not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, - version: Version::One, - is_ca: true, - key_usage: Some([KeyUsage::KeyAgreement].into_iter().collect()), - extended_key_usage: Some( - [ExtendedKeyUsage::EmailProtection].into_iter().collect(), - ), - }), + TemplatesUpdateParams { + template_name: None, + attributes: Some(CertificateAttributes { + common_name: Some("cn-new".to_string()), + country: Some("c".to_string()), + state_or_province: Some("s".to_string()), + locality: None, + organization: None, + organizational_unit: None, + key_algorithm: PrivateKeyAlgorithm::Ed25519, + signature_algorithm: SignatureAlgorithm::Md5, + not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, + not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, + version: Version::One, + is_ca: true, + key_usage: Some([KeyUsage::KeyAgreement].into_iter().collect()), + extended_key_usage: Some( + [ExtendedKeyUsage::EmailProtection].into_iter().collect(), + ), + }), + }, ) .await?; @@ -1282,7 +1658,13 @@ mod tests { let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; // Update name. @@ -1290,8 +1672,10 @@ mod tests { .update_certificate_template( mock_user.id, certificate_template.id, - Some("ct-new".to_string()), - None, + TemplatesUpdateParams { + template_name: Some("ct-new".to_string()), + attributes: None, + }, ) .await?; @@ -1311,25 +1695,27 @@ mod tests { .update_certificate_template( mock_user.id, certificate_template.id, - Some("ct-new-new".to_string()), - Some(CertificateAttributes { - common_name: Some("cn-new".to_string()), - country: Some("c".to_string()), - state_or_province: Some("s".to_string()), - locality: None, - organization: None, - organizational_unit: None, - key_algorithm: PrivateKeyAlgorithm::Ed25519, - signature_algorithm: SignatureAlgorithm::Md5, - not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, - not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, - version: Version::One, - is_ca: true, - key_usage: Some([KeyUsage::KeyAgreement].into_iter().collect()), - extended_key_usage: Some( - [ExtendedKeyUsage::EmailProtection].into_iter().collect(), - ), - }), + TemplatesUpdateParams { + template_name: Some("ct-new-new".to_string()), + attributes: Some(CertificateAttributes { + common_name: Some("cn-new".to_string()), + country: Some("c".to_string()), + state_or_province: Some("s".to_string()), + locality: None, + organization: None, + organizational_unit: None, + key_algorithm: PrivateKeyAlgorithm::Ed25519, + signature_algorithm: SignatureAlgorithm::Md5, + not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, + not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, + version: Version::One, + is_ca: true, + key_usage: Some([KeyUsage::KeyAgreement].into_iter().collect()), + extended_key_usage: Some( + [ExtendedKeyUsage::EmailProtection].into_iter().collect(), + ), + }), + }, ) .await?; @@ -1361,6 +1747,82 @@ mod tests { Ok(()) } + #[actix_rt::test] + async fn fails_to_update_certificate_template_if_params_are_invalid() -> anyhow::Result<()> { + let api = mock_api().await?; + + let mock_user = mock_user()?; + api.db.insert_user(&mock_user).await?; + + let certificates = CertificatesApi::new(&api); + let certificate_template = certificates + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) + .await?; + // Invalid name. + assert_debug_snapshot!(certificates + .update_certificate_template( + mock_user.id, + certificate_template.id, + TemplatesUpdateParams { + template_name: Some("".to_string()), + attributes: Some(get_mock_certificate_attributes()?), + }, + ) + .await, + @r###" + Err( + "Certificate template name cannot be empty.", + ) + "### + ); + + // Invalid name. + assert_debug_snapshot!(certificates + .update_certificate_template( + mock_user.id, + certificate_template.id, + TemplatesUpdateParams { + template_name: Some("a".repeat(101)), + attributes: Some(get_mock_certificate_attributes()?), + }, + ) + .await, + @r###" + Err( + "Certificate template name cannot be longer than 100 characters.", + ) + "### + ); + + // Invalid params. + assert_eq!( + certificates + .update_certificate_template( + mock_user.id, + certificate_template.id, + TemplatesUpdateParams { + template_name: None, + attributes: None, + }, + ) + .await + .unwrap_err() + .to_string(), + format!( + "Either new name or attributes should be provided ({}).", + certificate_template.id + ) + ); + + Ok(()) + } + #[actix_rt::test] async fn can_remove_certificate_template() -> anyhow::Result<()> { let api = mock_api().await?; @@ -1370,7 +1832,13 @@ mod tests { let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; assert_eq!( certificate_template, @@ -1406,7 +1874,13 @@ mod tests { .is_empty()); let certificate_template_one = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; assert_eq!( certificates.get_certificate_templates(mock_user.id).await?, @@ -1414,7 +1888,13 @@ mod tests { ); let certificate_template_two = certificates - .create_certificate_template(mock_user.id, "ct-2", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct-2".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; assert_eq!( certificates.get_certificate_templates(mock_user.id).await?, @@ -1516,7 +1996,13 @@ mod tests { let certificate_template = api .certificates() - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; let exported_certificate_pair = api @@ -1524,8 +2010,10 @@ mod tests { .generate_self_signed_certificate( mock_user.id, certificate_template.id, - ExportFormat::Pkcs12, - None, + TemplatesGenerateParams { + format: ExportFormat::Pkcs12, + passphrase: None, + }, ) .await?; @@ -1556,7 +2044,13 @@ mod tests { // Create and share policy. let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; let template_share_one = certificates .share_certificate_template(mock_user.id, certificate_template.id) @@ -1590,7 +2084,13 @@ mod tests { let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; let template_share_one = certificates .share_certificate_template(mock_user.id, certificate_template.id) @@ -1640,7 +2140,13 @@ mod tests { // Create and share template. let certificates = CertificatesApi::new(&api); let certificate_template = certificates - .create_certificate_template(mock_user.id, "ct", get_mock_certificate_attributes()?) + .create_certificate_template( + mock_user.id, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: get_mock_certificate_attributes()?, + }, + ) .await?; let template_share = certificates .share_certificate_template(mock_user.id, certificate_template.id) diff --git a/src/utils/certificates/api_ext/private_keys_create_params.rs b/src/utils/certificates/api_ext/private_keys_create_params.rs new file mode 100644 index 0000000..fca9877 --- /dev/null +++ b/src/utils/certificates/api_ext/private_keys_create_params.rs @@ -0,0 +1,57 @@ +use crate::utils::PrivateKeyAlgorithm; +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PrivateKeysCreateParams { + pub key_name: String, + pub alg: PrivateKeyAlgorithm, + pub passphrase: Option, +} + +#[cfg(test)] +mod tests { + use crate::utils::{PrivateKeyAlgorithm, PrivateKeySize, PrivateKeysCreateParams}; + + #[test] + fn deserialization() -> anyhow::Result<()> { + assert_eq!( + serde_json::from_str::( + r#" +{ + "keyName": "pk", + "alg": {"keyType": "rsa", "keySize": "1024"}, + "passphrase": "phrase" +} + "# + )?, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size1024 + }, + passphrase: Some("phrase".to_string()), + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ + "keyName": "pk", + "alg": {"keyType": "rsa", "keySize": "1024"} +} + "# + )?, + PrivateKeysCreateParams { + key_name: "pk".to_string(), + alg: PrivateKeyAlgorithm::Rsa { + key_size: PrivateKeySize::Size1024 + }, + passphrase: None, + } + ); + + Ok(()) + } +} diff --git a/src/utils/certificates/api_ext/private_keys_export_params.rs b/src/utils/certificates/api_ext/private_keys_export_params.rs new file mode 100644 index 0000000..c3c5b47 --- /dev/null +++ b/src/utils/certificates/api_ext/private_keys_export_params.rs @@ -0,0 +1,52 @@ +use crate::utils::ExportFormat; +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PrivateKeysExportParams { + pub format: ExportFormat, + pub passphrase: Option, + pub export_passphrase: Option, +} + +#[cfg(test)] +mod tests { + use crate::utils::{ExportFormat, PrivateKeysExportParams}; + + #[test] + fn deserialization() -> anyhow::Result<()> { + assert_eq!( + serde_json::from_str::( + r#" +{ + "format": "pem", + "passphrase": "phrase", + "exportPassphrase": "phrase_new" +} + "# + )?, + PrivateKeysExportParams { + format: ExportFormat::Pem, + passphrase: Some("phrase".to_string()), + export_passphrase: Some("phrase_new".to_string()), + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ + "format": "pem" +} + "# + )?, + PrivateKeysExportParams { + format: ExportFormat::Pem, + passphrase: None, + export_passphrase: None, + } + ); + + Ok(()) + } +} diff --git a/src/utils/certificates/api_ext/private_keys_update_params.rs b/src/utils/certificates/api_ext/private_keys_update_params.rs new file mode 100644 index 0000000..ffa36a9 --- /dev/null +++ b/src/utils/certificates/api_ext/private_keys_update_params.rs @@ -0,0 +1,81 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PrivateKeysUpdateParams { + pub key_name: Option, + pub new_passphrase: Option, + pub passphrase: Option, +} + +#[cfg(test)] +mod tests { + use crate::utils::PrivateKeysUpdateParams; + + #[test] + fn deserialization() -> anyhow::Result<()> { + assert_eq!( + serde_json::from_str::( + r#" +{ + "passphrase": "phrase", + "newPassphrase": "phrase_new" +} + "# + )?, + PrivateKeysUpdateParams { + key_name: None, + passphrase: Some("phrase".to_string()), + new_passphrase: Some("phrase_new".to_string()), + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ + "keyName": "pk", + "passphrase": "phrase", + "newPassphrase": "phrase_new" +} + "# + )?, + PrivateKeysUpdateParams { + key_name: Some("pk".to_string()), + passphrase: Some("phrase".to_string()), + new_passphrase: Some("phrase_new".to_string()), + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ + "keyName": "pk" +} + "# + )?, + PrivateKeysUpdateParams { + key_name: Some("pk".to_string()), + passphrase: None, + new_passphrase: None, + } + ); + + assert_eq!( + serde_json::from_str::( + r#" +{ +} + "# + )?, + PrivateKeysUpdateParams { + key_name: None, + passphrase: None, + new_passphrase: None, + } + ); + + Ok(()) + } +} diff --git a/src/utils/certificates/api_ext/templates_create_params.rs b/src/utils/certificates/api_ext/templates_create_params.rs new file mode 100644 index 0000000..cf6bd43 --- /dev/null +++ b/src/utils/certificates/api_ext/templates_create_params.rs @@ -0,0 +1,67 @@ +use crate::utils::CertificateAttributes; +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TemplatesCreateParams { + pub template_name: String, + pub attributes: CertificateAttributes, +} + +#[cfg(test)] +mod tests { + use crate::utils::{ + CertificateAttributes, ExtendedKeyUsage, KeyUsage, PrivateKeyAlgorithm, SignatureAlgorithm, + TemplatesCreateParams, Version, + }; + use time::OffsetDateTime; + + #[test] + fn deserialization() -> anyhow::Result<()> { + assert_eq!( + serde_json::from_str::( + r#" +{ + "templateName": "ct", + "attributes": { + "commonName": "CA Issuer", + "keyAlgorithm": { "keyType": "ed25519" }, + "signatureAlgorithm": "ed25519", + "notValidBefore": 946720800, + "notValidAfter": 1262340000, + "version": 3, + "isCa": true, + "keyUsage": ["crlSigning"], + "extendedKeyUsage": ["tlsWebServerAuthentication"] + } +} + "# + )?, + TemplatesCreateParams { + template_name: "ct".to_string(), + attributes: CertificateAttributes { + common_name: Some("CA Issuer".to_string()), + country: None, + state_or_province: None, + locality: None, + organization: None, + organizational_unit: None, + key_algorithm: PrivateKeyAlgorithm::Ed25519, + signature_algorithm: SignatureAlgorithm::Ed25519, + not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, + not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, + version: Version::Three, + is_ca: true, + key_usage: Some([KeyUsage::CrlSigning].into_iter().collect()), + extended_key_usage: Some( + [ExtendedKeyUsage::TlsWebServerAuthentication] + .into_iter() + .collect() + ), + } + } + ); + + Ok(()) + } +} diff --git a/src/utils/certificates/api_ext/templates_generate_params.rs b/src/utils/certificates/api_ext/templates_generate_params.rs new file mode 100644 index 0000000..9fb45ec --- /dev/null +++ b/src/utils/certificates/api_ext/templates_generate_params.rs @@ -0,0 +1,47 @@ +use crate::utils::ExportFormat; +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TemplatesGenerateParams { + pub format: ExportFormat, + pub passphrase: Option, +} + +#[cfg(test)] +mod tests { + use crate::utils::{ExportFormat, TemplatesGenerateParams}; + + #[test] + fn deserialization() -> anyhow::Result<()> { + assert_eq!( + serde_json::from_str::( + r#" + { + "format": "pem" + } + "# + )?, + TemplatesGenerateParams { + format: ExportFormat::Pem, + passphrase: None, + } + ); + assert_eq!( + serde_json::from_str::( + r#" + { + "format": "pkcs12", + "passphrase": "phrase" + } + "# + )?, + TemplatesGenerateParams { + format: ExportFormat::Pkcs12, + passphrase: Some("phrase".to_string()), + } + ); + + Ok(()) + } +} diff --git a/src/utils/certificates/api_ext/templates_update_params.rs b/src/utils/certificates/api_ext/templates_update_params.rs new file mode 100644 index 0000000..4585996 --- /dev/null +++ b/src/utils/certificates/api_ext/templates_update_params.rs @@ -0,0 +1,128 @@ +use crate::utils::CertificateAttributes; +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TemplatesUpdateParams { + pub template_name: Option, + pub attributes: Option, +} + +#[cfg(test)] +mod tests { + use crate::utils::{ + CertificateAttributes, ExtendedKeyUsage, KeyUsage, PrivateKeyAlgorithm, SignatureAlgorithm, + TemplatesUpdateParams, Version, + }; + use time::OffsetDateTime; + + #[test] + fn deserialization() -> anyhow::Result<()> { + assert_eq!( + serde_json::from_str::( + r#" + { + "templateName": "ct", + "attributes": { + "commonName": "CA Issuer", + "keyAlgorithm": { + "keyType": "ed25519" + }, + "signatureAlgorithm": "ed25519", + "notValidBefore": 946720800, + "notValidAfter": 1262340000, + "version": 3, + "isCa": true, + "keyUsage": ["crlSigning"], + "extendedKeyUsage": ["tlsWebServerAuthentication"] + } + } + "# + )?, + TemplatesUpdateParams { + template_name: Some("ct".to_string()), + attributes: Some(CertificateAttributes { + common_name: Some("CA Issuer".to_string()), + country: None, + state_or_province: None, + locality: None, + organization: None, + organizational_unit: None, + key_algorithm: PrivateKeyAlgorithm::Ed25519, + signature_algorithm: SignatureAlgorithm::Ed25519, + not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, + not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, + version: Version::Three, + is_ca: true, + key_usage: Some([KeyUsage::CrlSigning].into_iter().collect()), + extended_key_usage: Some( + [ExtendedKeyUsage::TlsWebServerAuthentication] + .into_iter() + .collect() + ), + }) + } + ); + + assert_eq!( + serde_json::from_str::( + r#" + { + "attributes": { + "commonName": "CA Issuer", + "keyAlgorithm": { + "keyType": "ed25519" + }, + "signatureAlgorithm": "ed25519", + "notValidBefore": 946720800, + "notValidAfter": 1262340000, + "version": 3, + "isCa": true, + "keyUsage": ["crlSigning"], + "extendedKeyUsage": ["tlsWebServerAuthentication"] + } + } + "# + )?, + TemplatesUpdateParams { + template_name: None, + attributes: Some(CertificateAttributes { + common_name: Some("CA Issuer".to_string()), + country: None, + state_or_province: None, + locality: None, + organization: None, + organizational_unit: None, + key_algorithm: PrivateKeyAlgorithm::Ed25519, + signature_algorithm: SignatureAlgorithm::Ed25519, + not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, + not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, + version: Version::Three, + is_ca: true, + key_usage: Some([KeyUsage::CrlSigning].into_iter().collect()), + extended_key_usage: Some( + [ExtendedKeyUsage::TlsWebServerAuthentication] + .into_iter() + .collect() + ), + }) + } + ); + + assert_eq!( + serde_json::from_str::( + r#" + { + "templateName": "ct" + } + "# + )?, + TemplatesUpdateParams { + template_name: Some("ct".to_string()), + attributes: None + } + ); + + Ok(()) + } +} diff --git a/src/utils/certificates/utils_certificates_action.rs b/src/utils/certificates/utils_certificates_action.rs deleted file mode 100644 index 3f1dbbc..0000000 --- a/src/utils/certificates/utils_certificates_action.rs +++ /dev/null @@ -1,882 +0,0 @@ -use crate::{ - api::Api, - error::Error as SecutilsError, - network::{DnsResolver, EmailTransport}, - users::{ClientUserShare, SharedResource, User}, - utils::{ - utils_action_validation::MAX_UTILS_ENTITY_NAME_LENGTH, CertificateAttributes, ExportFormat, - PrivateKeyAlgorithm, UtilsCertificatesActionResult, - }, -}; -use anyhow::bail; -use serde::Deserialize; -use uuid::Uuid; - -#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "type", content = "value")] -pub enum UtilsCertificatesAction { - GetPrivateKeys, - #[serde(rename_all = "camelCase")] - CreatePrivateKey { - key_name: String, - alg: PrivateKeyAlgorithm, - passphrase: Option, - }, - #[serde(rename_all = "camelCase")] - UpdatePrivateKey { - key_id: Uuid, - key_name: Option, - passphrase: Option, - new_passphrase: Option, - }, - #[serde(rename_all = "camelCase")] - RemovePrivateKey { - key_id: Uuid, - }, - #[serde(rename_all = "camelCase")] - ExportPrivateKey { - key_id: Uuid, - format: ExportFormat, - passphrase: Option, - export_passphrase: Option, - }, - #[serde(rename_all = "camelCase")] - GetCertificateTemplate { - template_id: Uuid, - }, - GetCertificateTemplates, - #[serde(rename_all = "camelCase")] - CreateCertificateTemplate { - template_name: String, - attributes: CertificateAttributes, - }, - #[serde(rename_all = "camelCase")] - UpdateCertificateTemplate { - template_id: Uuid, - template_name: Option, - attributes: Option, - }, - #[serde(rename_all = "camelCase")] - RemoveCertificateTemplate { - template_id: Uuid, - }, - #[serde(rename_all = "camelCase")] - GenerateSelfSignedCertificate { - template_id: Uuid, - format: ExportFormat, - passphrase: Option, - }, - #[serde(rename_all = "camelCase")] - ShareCertificateTemplate { - template_id: Uuid, - }, - #[serde(rename_all = "camelCase")] - UnshareCertificateTemplate { - template_id: Uuid, - }, -} - -impl UtilsCertificatesAction { - /// Validates action parameters and throws if action parameters aren't valid. - pub fn validate(&self) -> anyhow::Result<()> { - let assert_private_key_name = |name: &str| -> Result<(), SecutilsError> { - if name.is_empty() { - return Err(SecutilsError::client("Private key name cannot be empty.")); - } - - if name.len() > MAX_UTILS_ENTITY_NAME_LENGTH { - return Err(SecutilsError::client(format!( - "Private key name cannot be longer than {} characters.", - MAX_UTILS_ENTITY_NAME_LENGTH - ))); - } - - Ok(()) - }; - - let assert_certificate_template_name = |name: &str| -> Result<(), SecutilsError> { - if name.is_empty() { - return Err(SecutilsError::client( - "Certificate template name cannot be empty.", - )); - } - - if name.len() > MAX_UTILS_ENTITY_NAME_LENGTH { - return Err(SecutilsError::client(format!( - "Certificate template name cannot be longer than {} characters.", - MAX_UTILS_ENTITY_NAME_LENGTH - ))); - } - - Ok(()) - }; - - match self { - UtilsCertificatesAction::CreatePrivateKey { key_name, .. } => { - assert_private_key_name(key_name)?; - } - UtilsCertificatesAction::UpdatePrivateKey { - key_name, - passphrase, - new_passphrase, - key_id, - } => { - let includes_new_passphrase = passphrase.is_some() || new_passphrase.is_some(); - if let Some(name) = key_name { - assert_private_key_name(name)?; - } else if !includes_new_passphrase { - bail!(SecutilsError::client(format!( - "Either new name or passphrase should be provided ({key_id})." - ))); - } - - if includes_new_passphrase && passphrase == new_passphrase { - bail!(SecutilsError::client(format!( - "New private key passphrase should be different from the current passphrase ({key_id})." - ))); - } - } - UtilsCertificatesAction::CreateCertificateTemplate { template_name, .. } => { - assert_certificate_template_name(template_name)?; - } - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id, - template_name, - attributes, - } => { - if let Some(name) = template_name { - assert_certificate_template_name(name)?; - } else if !attributes.is_some() { - bail!(SecutilsError::client(format!( - "Either new name or attributes should be provided ({template_id})." - ))); - } - } - UtilsCertificatesAction::GetPrivateKeys - | UtilsCertificatesAction::RemovePrivateKey { .. } - | UtilsCertificatesAction::ExportPrivateKey { .. } - | UtilsCertificatesAction::GetCertificateTemplate { .. } - | UtilsCertificatesAction::GetCertificateTemplates - | UtilsCertificatesAction::RemoveCertificateTemplate { .. } - | UtilsCertificatesAction::GenerateSelfSignedCertificate { .. } - | UtilsCertificatesAction::ShareCertificateTemplate { .. } - | UtilsCertificatesAction::UnshareCertificateTemplate { .. } => {} - } - - Ok(()) - } - - pub async fn handle( - self, - user: User, - api: &Api, - ) -> anyhow::Result { - let certificates = api.certificates(); - match self { - UtilsCertificatesAction::GetPrivateKeys => { - Ok(UtilsCertificatesActionResult::GetPrivateKeys( - certificates.get_private_keys(user.id).await?, - )) - } - UtilsCertificatesAction::CreatePrivateKey { - key_name, - alg, - passphrase, - } => Ok(UtilsCertificatesActionResult::CreatePrivateKey( - certificates - .create_private_key(user.id, &key_name, alg, passphrase.as_deref()) - .await?, - )), - UtilsCertificatesAction::UpdatePrivateKey { - key_id, - key_name, - passphrase, - new_passphrase, - } => { - certificates - .update_private_key( - user.id, - key_id, - key_name.as_deref(), - passphrase.as_deref(), - new_passphrase.as_deref(), - ) - .await?; - Ok(UtilsCertificatesActionResult::UpdatePrivateKey) - } - UtilsCertificatesAction::ExportPrivateKey { - key_id, - passphrase, - export_passphrase, - format, - } => Ok(UtilsCertificatesActionResult::ExportPrivateKey( - certificates - .export_private_key( - user.id, - key_id, - format, - passphrase.as_deref(), - export_passphrase.as_deref(), - ) - .await?, - )), - UtilsCertificatesAction::RemovePrivateKey { key_id } => { - certificates.remove_private_key(user.id, key_id).await?; - Ok(UtilsCertificatesActionResult::RemovePrivateKey) - } - UtilsCertificatesAction::GetCertificateTemplates => { - Ok(UtilsCertificatesActionResult::GetCertificateTemplates( - certificates.get_certificate_templates(user.id).await?, - )) - } - UtilsCertificatesAction::GetCertificateTemplate { template_id } => { - let users = api.users(); - Ok(UtilsCertificatesActionResult::GetCertificateTemplate { - template: certificates - .get_certificate_template(user.id, template_id) - .await?, - user_share: users - .get_user_share_by_resource( - user.id, - &SharedResource::certificate_template(template_id), - ) - .await? - .map(ClientUserShare::from), - }) - } - UtilsCertificatesAction::CreateCertificateTemplate { - template_name, - attributes, - } => Ok(UtilsCertificatesActionResult::CreateCertificateTemplate( - certificates - .create_certificate_template(user.id, template_name, attributes) - .await?, - )), - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id, - template_name, - attributes, - } => { - certificates - .update_certificate_template(user.id, template_id, template_name, attributes) - .await?; - Ok(UtilsCertificatesActionResult::UpdateCertificateTemplate) - } - UtilsCertificatesAction::RemoveCertificateTemplate { template_id } => { - certificates - .remove_certificate_template(user.id, template_id) - .await?; - Ok(UtilsCertificatesActionResult::RemoveCertificateTemplate) - } - UtilsCertificatesAction::GenerateSelfSignedCertificate { - template_id, - format, - passphrase, - } => Ok( - UtilsCertificatesActionResult::GenerateSelfSignedCertificate( - certificates - .generate_self_signed_certificate( - user.id, - template_id, - format, - passphrase.as_deref(), - ) - .await?, - ), - ), - UtilsCertificatesAction::ShareCertificateTemplate { template_id } => certificates - .share_certificate_template(user.id, template_id) - .await - .map(|user_share| { - UtilsCertificatesActionResult::ShareCertificateTemplate(ClientUserShare::from( - user_share, - )) - }), - UtilsCertificatesAction::UnshareCertificateTemplate { template_id } => certificates - .unshare_certificate_template(user.id, template_id) - .await - .map(|user_share| user_share.map(ClientUserShare::from)) - .map(|user_share| { - UtilsCertificatesActionResult::UnshareCertificateTemplate(user_share) - }), - } - } -} - -#[cfg(test)] -mod tests { - use crate::{ - tests::MockCertificateAttributes, - utils::{ - CertificateAttributes, ExportFormat, ExtendedKeyUsage, KeyUsage, PrivateKeyAlgorithm, - PrivateKeySize, SignatureAlgorithm, UtilsCertificatesAction, Version, - }, - }; - use time::OffsetDateTime; - use uuid::uuid; - - #[test] - fn deserialization() -> anyhow::Result<()> { - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "getPrivateKeys" -} - "# - )?, - UtilsCertificatesAction::GetPrivateKeys - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "createPrivateKey", - "value": { "keyName": "pk", "alg": {"keyType": "rsa", "keySize": "1024"}, "passphrase": "phrase" } -} - "# - )?, - UtilsCertificatesAction::CreatePrivateKey { - key_name: "pk".to_string(), - alg: PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024 - }, - passphrase: Some("phrase".to_string()), - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "createPrivateKey", - "value": { "keyName": "pk", "alg": {"keyType": "rsa", "keySize": "1024"} } -} - "# - )?, - UtilsCertificatesAction::CreatePrivateKey { - key_name: "pk".to_string(), - alg: PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024 - }, - passphrase: None, - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "updatePrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001", "passphrase": "phrase", "newPassphrase": "phrase_new" } -} - "# - )?, - UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: None, - passphrase: Some("phrase".to_string()), - new_passphrase: Some("phrase_new".to_string()), - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "updatePrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001", "keyName": "pk", "passphrase": "phrase", "newPassphrase": "phrase_new" } -} - "# - )?, - UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: Some("pk".to_string()), - passphrase: Some("phrase".to_string()), - new_passphrase: Some("phrase_new".to_string()), - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "updatePrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001", "keyName": "pk" } -} - "# - )?, - UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: Some("pk".to_string()), - passphrase: None, - new_passphrase: None, - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "updatePrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001" } -} - "# - )?, - UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: None, - passphrase: None, - new_passphrase: None, - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "removePrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001" } -} - "# - )?, - UtilsCertificatesAction::RemovePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001") - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "exportPrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001", "format": "pem", "passphrase": "phrase", "exportPassphrase": "phrase_new" } -} - "# - )?, - UtilsCertificatesAction::ExportPrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - format: ExportFormat::Pem, - passphrase: Some("phrase".to_string()), - export_passphrase: Some("phrase_new".to_string()), - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "exportPrivateKey", - "value": { "keyId": "00000000-0000-0000-0000-000000000001", "format": "pem" } -} - "# - )?, - UtilsCertificatesAction::ExportPrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - format: ExportFormat::Pem, - passphrase: None, - export_passphrase: None, - } - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "getCertificateTemplates" -} - "# - )?, - UtilsCertificatesAction::GetCertificateTemplates - ); - - assert_eq!( - serde_json::from_str::( - r#" -{ - "type": "createCertificateTemplate", - "value": { - "templateName": "ct", - "attributes": { - "commonName": "CA Issuer", - "keyAlgorithm": { "keyType": "ed25519" }, - "signatureAlgorithm": "ed25519", - "notValidBefore": 946720800, - "notValidAfter": 1262340000, - "version": 3, - "isCa": true, - "keyUsage": ["crlSigning"], - "extendedKeyUsage": ["tlsWebServerAuthentication"] - } - } -} - "# - )?, - UtilsCertificatesAction::CreateCertificateTemplate { - template_name: "ct".to_string(), - attributes: CertificateAttributes { - common_name: Some("CA Issuer".to_string()), - country: None, - state_or_province: None, - locality: None, - organization: None, - organizational_unit: None, - key_algorithm: PrivateKeyAlgorithm::Ed25519, - signature_algorithm: SignatureAlgorithm::Ed25519, - not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, - not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, - version: Version::Three, - is_ca: true, - key_usage: Some([KeyUsage::CrlSigning].into_iter().collect()), - extended_key_usage: Some( - [ExtendedKeyUsage::TlsWebServerAuthentication] - .into_iter() - .collect() - ), - } - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "updateCertificateTemplate", - "value": { - "templateId": "00000000-0000-0000-0000-000000000001", - "templateName": "ct", - "attributes": { - "commonName": "CA Issuer", - "keyAlgorithm": { - "keyType": "ed25519" - }, - "signatureAlgorithm": "ed25519", - "notValidBefore": 946720800, - "notValidAfter": 1262340000, - "version": 3, - "isCa": true, - "keyUsage": ["crlSigning"], - "extendedKeyUsage": ["tlsWebServerAuthentication"] - } - } - } - "# - )?, - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - template_name: Some("ct".to_string()), - attributes: Some(CertificateAttributes { - common_name: Some("CA Issuer".to_string()), - country: None, - state_or_province: None, - locality: None, - organization: None, - organizational_unit: None, - key_algorithm: PrivateKeyAlgorithm::Ed25519, - signature_algorithm: SignatureAlgorithm::Ed25519, - not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, - not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, - version: Version::Three, - is_ca: true, - key_usage: Some([KeyUsage::CrlSigning].into_iter().collect()), - extended_key_usage: Some( - [ExtendedKeyUsage::TlsWebServerAuthentication] - .into_iter() - .collect() - ), - }) - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "updateCertificateTemplate", - "value": { - "templateId": "00000000-0000-0000-0000-000000000001", - "attributes": { - "commonName": "CA Issuer", - "keyAlgorithm": { - "keyType": "ed25519" - }, - "signatureAlgorithm": "ed25519", - "notValidBefore": 946720800, - "notValidAfter": 1262340000, - "version": 3, - "isCa": true, - "keyUsage": ["crlSigning"], - "extendedKeyUsage": ["tlsWebServerAuthentication"] - } - } - } - "# - )?, - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - template_name: None, - attributes: Some(CertificateAttributes { - common_name: Some("CA Issuer".to_string()), - country: None, - state_or_province: None, - locality: None, - organization: None, - organizational_unit: None, - key_algorithm: PrivateKeyAlgorithm::Ed25519, - signature_algorithm: SignatureAlgorithm::Ed25519, - not_valid_before: OffsetDateTime::from_unix_timestamp(946720800)?, - not_valid_after: OffsetDateTime::from_unix_timestamp(1262340000)?, - version: Version::Three, - is_ca: true, - key_usage: Some([KeyUsage::CrlSigning].into_iter().collect()), - extended_key_usage: Some( - [ExtendedKeyUsage::TlsWebServerAuthentication] - .into_iter() - .collect() - ), - }) - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "updateCertificateTemplate", - "value": { - "templateId": "00000000-0000-0000-0000-000000000001", - "templateName": "ct" - } - } - "# - )?, - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - template_name: Some("ct".to_string()), - attributes: None - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "removeCertificateTemplate", - "value": { "templateId": "00000000-0000-0000-0000-000000000001" } - } - "# - )?, - UtilsCertificatesAction::RemoveCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001") - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "generateSelfSignedCertificate", - "value": { "templateId": "00000000-0000-0000-0000-000000000001", "format": "pem" } - } - "# - )?, - UtilsCertificatesAction::GenerateSelfSignedCertificate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - format: ExportFormat::Pem, - passphrase: None, - } - ); - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "generateSelfSignedCertificate", - "value": { "templateId": "00000000-0000-0000-0000-000000000001", "format": "pkcs12", "passphrase": "phrase" } - } - "# - )?, - UtilsCertificatesAction::GenerateSelfSignedCertificate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - format: ExportFormat::Pkcs12, - passphrase: Some("phrase".to_string()), - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "getCertificateTemplate", - "value": { "templateId": "00000000-0000-0000-0000-000000000001" } - } - "# - )?, - UtilsCertificatesAction::GetCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001") - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "shareCertificateTemplate", - "value": { "templateId": "00000000-0000-0000-0000-000000000001" } - } - "# - )?, - UtilsCertificatesAction::ShareCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001") - } - ); - - assert_eq!( - serde_json::from_str::( - r#" - { - "type": "unshareCertificateTemplate", - "value": { "templateId": "00000000-0000-0000-0000-000000000001" } - } - "# - )?, - UtilsCertificatesAction::UnshareCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001") - } - ); - - Ok(()) - } - - #[test] - fn validation() -> anyhow::Result<()> { - let get_private_keys_actions_with_name = |key_name: String| { - vec![ - UtilsCertificatesAction::CreatePrivateKey { - key_name: key_name.clone(), - alg: PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024, - }, - passphrase: Some("phrase".to_string()), - }, - UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: Some(key_name), - passphrase: None, - new_passphrase: None, - }, - ] - }; - - for action in get_private_keys_actions_with_name("a".repeat(100)) { - assert!(action.validate().is_ok()); - } - - for action in get_private_keys_actions_with_name("".to_string()) { - assert_eq!( - action.validate().map_err(|err| err.to_string()), - Err("Private key name cannot be empty.".to_string()) - ); - } - - for action in get_private_keys_actions_with_name("a".repeat(101)) { - assert_eq!( - action.validate().map_err(|err| err.to_string()), - Err("Private key name cannot be longer than 100 characters.".to_string()) - ); - } - - let update_private_key_action = UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: Some("pk".to_string()), - passphrase: Some("pass".to_string()), - new_passphrase: Some("pass".to_string()), - }; - assert_eq!( - update_private_key_action.validate().map_err(|err| err.to_string()), - Err("New private key passphrase should be different from the current passphrase (00000000-0000-0000-0000-000000000001).".to_string()) - ); - - let update_private_key_action = UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: None, - passphrase: None, - new_passphrase: None, - }; - assert_eq!( - update_private_key_action.validate().map_err(|err| err.to_string()), - Err("Either new name or passphrase should be provided (00000000-0000-0000-0000-000000000001).".to_string()) - ); - - for (passphrase, new_passphrase) in [ - (None, Some("pass".to_string())), - (Some("pass".to_string()), Some("pass_new".to_string())), - (Some("pass".to_string()), None), - ] { - let update_private_key_action = UtilsCertificatesAction::UpdatePrivateKey { - key_id: uuid!("00000000-0000-0000-0000-000000000001"), - key_name: None, - passphrase, - new_passphrase, - }; - assert!(update_private_key_action.validate().is_ok()); - } - - let get_certificate_templates_actions_with_name = - |template_name: String| -> anyhow::Result> { - let attributes = MockCertificateAttributes::new( - PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024, - }, - SignatureAlgorithm::Sha256, - OffsetDateTime::from_unix_timestamp(946720800)?, - OffsetDateTime::from_unix_timestamp(946720800)?, - Version::One, - ) - .build(); - Ok(vec![ - UtilsCertificatesAction::CreateCertificateTemplate { - template_name: template_name.clone(), - attributes: attributes.clone(), - }, - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - template_name: Some(template_name), - attributes: Some(attributes), - }, - ]) - }; - - for action in get_certificate_templates_actions_with_name("a".repeat(100))? { - assert!(action.validate().is_ok()); - } - - for action in get_certificate_templates_actions_with_name("".to_string())? { - assert_eq!( - action.validate().map_err(|err| err.to_string()), - Err("Certificate template name cannot be empty.".to_string()) - ); - } - - for action in get_certificate_templates_actions_with_name("a".repeat(101))? { - assert_eq!( - action.validate().map_err(|err| err.to_string()), - Err("Certificate template name cannot be longer than 100 characters.".to_string()) - ); - } - - let update_certificate_template_action = - UtilsCertificatesAction::UpdateCertificateTemplate { - template_id: uuid!("00000000-0000-0000-0000-000000000001"), - template_name: None, - attributes: None, - }; - assert_eq!( - update_certificate_template_action.validate().map_err(|err| err.to_string()), - Err("Either new name or attributes should be provided (00000000-0000-0000-0000-000000000001).".to_string()) - ); - - Ok(()) - } -} diff --git a/src/utils/certificates/utils_certificates_action_result.rs b/src/utils/certificates/utils_certificates_action_result.rs deleted file mode 100644 index 097a9da..0000000 --- a/src/utils/certificates/utils_certificates_action_result.rs +++ /dev/null @@ -1,328 +0,0 @@ -use crate::{ - users::ClientUserShare, - utils::{CertificateTemplate, PrivateKey}, -}; -use serde::Serialize; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "type", content = "value")] -#[allow(clippy::large_enum_variant)] -pub enum UtilsCertificatesActionResult { - GetPrivateKeys(Vec), - CreatePrivateKey(PrivateKey), - UpdatePrivateKey, - RemovePrivateKey, - ExportPrivateKey(Vec), - #[serde(rename_all = "camelCase")] - GetCertificateTemplate { - #[serde(skip_serializing_if = "Option::is_none")] - template: Option, - #[serde(skip_serializing_if = "Option::is_none")] - user_share: Option, - }, - GetCertificateTemplates(Vec), - CreateCertificateTemplate(CertificateTemplate), - UpdateCertificateTemplate, - RemoveCertificateTemplate, - GenerateSelfSignedCertificate(Vec), - ShareCertificateTemplate(ClientUserShare), - UnshareCertificateTemplate(Option), -} - -#[cfg(test)] -mod tests { - use crate::{ - tests::MockCertificateAttributes, - users::{ClientUserShare, SharedResource, UserId, UserShare, UserShareId}, - utils::{ - CertificateTemplate, PrivateKey, PrivateKeyAlgorithm, PrivateKeySize, - SignatureAlgorithm, UtilsCertificatesActionResult, Version, - }, - }; - use insta::assert_json_snapshot; - use time::OffsetDateTime; - use uuid::uuid; - - #[test] - fn serialization() -> anyhow::Result<()> { - assert_json_snapshot!(UtilsCertificatesActionResult::GetPrivateKeys(vec![PrivateKey { - id: uuid!("00000000-0000-0000-0000-000000000001"), - name: "pk-name".to_string(), - alg: PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size2048 - }, - pkcs8: vec![], - encrypted: true, - created_at: OffsetDateTime::from_unix_timestamp(946720800)?, - }]), @r###" - { - "type": "getPrivateKeys", - "value": [ - { - "id": "00000000-0000-0000-0000-000000000001", - "name": "pk-name", - "alg": { - "keyType": "rsa", - "keySize": "2048" - }, - "pkcs8": [], - "encrypted": true, - "createdAt": 946720800 - } - ] - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::CreatePrivateKey(PrivateKey { - id: uuid!("00000000-0000-0000-0000-000000000001"), - name: "pk-name".to_string(), - alg: PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size2048 - }, - pkcs8: vec![1, 2, 3], - encrypted: false, - created_at: OffsetDateTime::from_unix_timestamp(946720800)?, - }), @r###" - { - "type": "createPrivateKey", - "value": { - "id": "00000000-0000-0000-0000-000000000001", - "name": "pk-name", - "alg": { - "keyType": "rsa", - "keySize": "2048" - }, - "pkcs8": [ - 1, - 2, - 3 - ], - "encrypted": false, - "createdAt": 946720800 - } - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::UpdatePrivateKey, @r###" - { - "type": "updatePrivateKey" - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::RemovePrivateKey, @r###" - { - "type": "removePrivateKey" - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::ExportPrivateKey(vec![1, 2, 3]), @r###" - { - "type": "exportPrivateKey", - "value": [ - 1, - 2, - 3 - ] - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::GetCertificateTemplate { - template: Some(CertificateTemplate { - id: uuid!("00000000-0000-0000-0000-000000000001"), - name: "ct-name".to_string(), - attributes: MockCertificateAttributes::new( - PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024, - }, - SignatureAlgorithm::Sha256, - OffsetDateTime::from_unix_timestamp(946720800)?, - OffsetDateTime::from_unix_timestamp(946720800)?, - Version::One, - ) - .build(), - created_at: OffsetDateTime::from_unix_timestamp(946720800)?, - }), - user_share: Some(ClientUserShare::from(UserShare { - id: UserShareId::from(uuid!("00000000-0000-0000-0000-000000000002")), - user_id: UserId::empty(), - resource: SharedResource::certificate_template(uuid!("00000000-0000-0000-0000-000000000001")), - created_at: time::OffsetDateTime::from_unix_timestamp(123456)?, - })) - }, @r###" - { - "type": "getCertificateTemplate", - "value": { - "template": { - "id": "00000000-0000-0000-0000-000000000001", - "name": "ct-name", - "attributes": { - "keyAlgorithm": { - "keyType": "rsa", - "keySize": "1024" - }, - "signatureAlgorithm": "sha256", - "notValidBefore": 946720800, - "notValidAfter": 946720800, - "version": 1, - "isCa": false - }, - "createdAt": 946720800 - }, - "userShare": { - "id": "00000000-0000-0000-0000-000000000002", - "resource": { - "type": "certificateTemplate", - "templateId": "00000000-0000-0000-0000-000000000001" - }, - "createdAt": 123456 - } - } - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::GetCertificateTemplates(vec![CertificateTemplate { - id: uuid!("00000000-0000-0000-0000-000000000001"), - name: "ct-name".to_string(), - attributes: MockCertificateAttributes::new( - PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024, - }, - SignatureAlgorithm::Sha256, - OffsetDateTime::from_unix_timestamp(946720800)?, - OffsetDateTime::from_unix_timestamp(946720800)?, - Version::One, - ) - .build(), - created_at: OffsetDateTime::from_unix_timestamp(946720800)?, - }]), @r###" - { - "type": "getCertificateTemplates", - "value": [ - { - "id": "00000000-0000-0000-0000-000000000001", - "name": "ct-name", - "attributes": { - "keyAlgorithm": { - "keyType": "rsa", - "keySize": "1024" - }, - "signatureAlgorithm": "sha256", - "notValidBefore": 946720800, - "notValidAfter": 946720800, - "version": 1, - "isCa": false - }, - "createdAt": 946720800 - } - ] - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::CreateCertificateTemplate(CertificateTemplate { - id: uuid!("00000000-0000-0000-0000-000000000001"), - name: "ct-name".to_string(), - attributes: MockCertificateAttributes::new( - PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024, - }, - SignatureAlgorithm::Sha256, - OffsetDateTime::from_unix_timestamp(946720800)?, - OffsetDateTime::from_unix_timestamp(946720800)?, - Version::One, - ) - .build(), - created_at: OffsetDateTime::from_unix_timestamp(946720800)?, - }), @r###" - { - "type": "createCertificateTemplate", - "value": { - "id": "00000000-0000-0000-0000-000000000001", - "name": "ct-name", - "attributes": { - "keyAlgorithm": { - "keyType": "rsa", - "keySize": "1024" - }, - "signatureAlgorithm": "sha256", - "notValidBefore": 946720800, - "notValidAfter": 946720800, - "version": 1, - "isCa": false - }, - "createdAt": 946720800 - } - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::UpdateCertificateTemplate, @r###" - { - "type": "updateCertificateTemplate" - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::RemoveCertificateTemplate, @r###" - { - "type": "removeCertificateTemplate" - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::GenerateSelfSignedCertificate (vec![1,2,3]), @r###" - { - "type": "generateSelfSignedCertificate", - "value": [ - 1, - 2, - 3 - ] - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::ShareCertificateTemplate(ClientUserShare::from(UserShare { - id: UserShareId::from(uuid!("00000000-0000-0000-0000-000000000001")), - user_id: UserId::empty(), - resource: SharedResource::certificate_template(uuid!("00000000-0000-0000-0000-000000000002")), - created_at: OffsetDateTime::from_unix_timestamp(123456)?, - })), @r###" - { - "type": "shareCertificateTemplate", - "value": { - "id": "00000000-0000-0000-0000-000000000001", - "resource": { - "type": "certificateTemplate", - "templateId": "00000000-0000-0000-0000-000000000002" - }, - "createdAt": 123456 - } - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::UnshareCertificateTemplate(Some(ClientUserShare::from(UserShare { - id: UserShareId::from(uuid!("00000000-0000-0000-0000-000000000001")), - user_id: UserId::empty(), - resource: SharedResource::certificate_template(uuid!("00000000-0000-0000-0000-000000000002")), - created_at: OffsetDateTime::from_unix_timestamp(123456)?, - }))), @r###" - { - "type": "unshareCertificateTemplate", - "value": { - "id": "00000000-0000-0000-0000-000000000001", - "resource": { - "type": "certificateTemplate", - "templateId": "00000000-0000-0000-0000-000000000002" - }, - "createdAt": 123456 - } - } - "###); - - assert_json_snapshot!(UtilsCertificatesActionResult::UnshareCertificateTemplate(None), @r###" - { - "type": "unshareCertificateTemplate", - "value": null - } - "###); - - Ok(()) - } -} diff --git a/src/utils/user_share_ext.rs b/src/utils/user_share_ext.rs index 4cfed35..cbffdbd 100644 --- a/src/utils/user_share_ext.rs +++ b/src/utils/user_share_ext.rs @@ -1,52 +1,66 @@ use crate::{ users::{SharedResource, UserShare}, - utils::{UtilsAction, UtilsCertificatesAction, UtilsWebSecurityAction}, + utils::{ + UtilsAction, UtilsLegacyAction, UtilsResource, UtilsResourceOperation, + UtilsWebSecurityAction, + }, }; impl UserShare { /// Checks if the user share is authorized to perform the specified action. - pub fn is_action_authorized(&self, action: &UtilsAction) -> bool { + pub fn is_legacy_action_authorized(&self, action: &UtilsLegacyAction) -> bool { match (&self.resource, action) { // Any user can access and serialize content of the shared content security policy. ( SharedResource::ContentSecurityPolicy { policy_name: resource_policy_name, }, - UtilsAction::WebSecurity(UtilsWebSecurityAction::GetContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::GetContentSecurityPolicy { policy_name, }) - | UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { - policy_name, - .. - }), + | UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { policy_name, .. }, + ), ) if resource_policy_name == policy_name => true, - // Any user can access certificate template and generate certificate/key pair. - ( - SharedResource::CertificateTemplate { - template_id: resource_template_id, - }, - UtilsAction::Certificates(UtilsCertificatesAction::GetCertificateTemplate { - template_id, - }) - | UtilsAction::Certificates(UtilsCertificatesAction::GenerateSelfSignedCertificate { - template_id, - .. - }), - ) if resource_template_id == template_id => true, _ => false, } } + + /// Checks if the user share is authorized to perform the specified action. + pub fn is_action_authorized(&self, action: &UtilsAction, resource: &UtilsResource) -> bool { + match &self.resource { + SharedResource::CertificateTemplate { template_id } => { + match (resource, action) { + // Any user can access certificate template and generate certificate/key pair. + (UtilsResource::CertificatesTemplates, UtilsAction::Get { resource_id }) => { + template_id == resource_id + } + ( + UtilsResource::CertificatesTemplates, + UtilsAction::Execute { + resource_id, + operation, + }, + ) => { + template_id == resource_id + && operation == &UtilsResourceOperation::CertificatesTemplateGenerate + } + _ => false, + } + } + SharedResource::ContentSecurityPolicy { .. } => false, + } + } } #[cfg(test)] mod tests { use crate::{ - tests::MockCertificateAttributes, users::{SharedResource, UserId, UserShare}, utils::{ ContentSecurityPolicy, ContentSecurityPolicyDirective, ContentSecurityPolicySource, - ExportFormat, PrivateKeyAlgorithm, PrivateKeySize, SignatureAlgorithm, UtilsAction, - UtilsCertificatesAction, UtilsWebSecurityAction, Version, + UtilsAction, UtilsLegacyAction, UtilsResource, UtilsResourceOperation, + UtilsWebSecurityAction, }, }; use time::OffsetDateTime; @@ -62,7 +76,7 @@ mod tests { }; let unauthorized_actions = vec![ - UtilsAction::WebSecurity(UtilsWebSecurityAction::SaveContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::SaveContentSecurityPolicy { policy: ContentSecurityPolicy { name: "".to_string(), directives: vec![ContentSecurityPolicyDirective::ChildSrc( @@ -70,54 +84,66 @@ mod tests { )], }, }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::RemoveContentSecurityPolicy { - policy_name: "not-my-policy".to_string(), - }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::ShareContentSecurityPolicy { - policy_name: "not-my-policy".to_string(), - }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::UnshareContentSecurityPolicy { - policy_name: "not-my-policy".to_string(), - }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::GetContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::RemoveContentSecurityPolicy { policy_name: "not-my-policy".to_string(), }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::ShareContentSecurityPolicy { policy_name: "not-my-policy".to_string(), - source: ContentSecurityPolicySource::Meta, }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::UnshareContentSecurityPolicy { policy_name: "not-my-policy".to_string(), - source: ContentSecurityPolicySource::EnforcingHeader, }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::GetContentSecurityPolicy { policy_name: "not-my-policy".to_string(), - source: ContentSecurityPolicySource::ReportOnlyHeader, }), + UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "not-my-policy".to_string(), + source: ContentSecurityPolicySource::Meta, + }, + ), + UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "not-my-policy".to_string(), + source: ContentSecurityPolicySource::EnforcingHeader, + }, + ), + UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "not-my-policy".to_string(), + source: ContentSecurityPolicySource::ReportOnlyHeader, + }, + ), ]; for action in unauthorized_actions { - assert!(!user_share.is_action_authorized(&action)); + assert!(!user_share.is_legacy_action_authorized(&action)); } let authorized_actions = vec![ - UtilsAction::WebSecurity(UtilsWebSecurityAction::GetContentSecurityPolicy { + UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::GetContentSecurityPolicy { policy_name: "my-policy".to_string(), }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { - policy_name: "my-policy".to_string(), - source: ContentSecurityPolicySource::Meta, - }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { - policy_name: "my-policy".to_string(), - source: ContentSecurityPolicySource::EnforcingHeader, - }), - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { - policy_name: "my-policy".to_string(), - source: ContentSecurityPolicySource::ReportOnlyHeader, - }), + UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "my-policy".to_string(), + source: ContentSecurityPolicySource::Meta, + }, + ), + UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "my-policy".to_string(), + source: ContentSecurityPolicySource::EnforcingHeader, + }, + ), + UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "my-policy".to_string(), + source: ContentSecurityPolicySource::ReportOnlyHeader, + }, + ), ]; for action in authorized_actions { - assert!(user_share.is_action_authorized(&action)); + assert!(user_share.is_legacy_action_authorized(&action)); } } @@ -133,51 +159,45 @@ mod tests { }; let unauthorized_actions = vec![ - UtilsAction::Certificates(UtilsCertificatesAction::GetCertificateTemplates), - UtilsAction::Certificates(UtilsCertificatesAction::CreateCertificateTemplate { - template_name: "a".to_string(), - attributes: MockCertificateAttributes::new( - PrivateKeyAlgorithm::Rsa { - key_size: PrivateKeySize::Size1024, - }, - SignatureAlgorithm::Sha256, - OffsetDateTime::from_unix_timestamp(946720800)?, - OffsetDateTime::from_unix_timestamp(946720800)?, - Version::One, - ) - .build(), - }), - UtilsAction::Certificates(UtilsCertificatesAction::UpdateCertificateTemplate { - template_id, - template_name: None, - attributes: None, - }), - UtilsAction::Certificates(UtilsCertificatesAction::RemoveCertificateTemplate { - template_id, - }), - UtilsAction::Certificates(UtilsCertificatesAction::ShareCertificateTemplate { - template_id, - }), - UtilsAction::Certificates(UtilsCertificatesAction::UnshareCertificateTemplate { - template_id, - }), + UtilsAction::List, + UtilsAction::Get { + resource_id: uuid!("00000000-0000-0000-0000-000000000002"), + }, + UtilsAction::Create, + UtilsAction::Update { + resource_id: template_id, + }, + UtilsAction::Delete { + resource_id: template_id, + }, + UtilsAction::Execute { + resource_id: uuid!("00000000-0000-0000-0000-000000000002"), + operation: UtilsResourceOperation::CertificatesTemplateGenerate, + }, ]; for action in unauthorized_actions { - assert!(!user_share.is_action_authorized(&action)); + assert!( + !user_share.is_action_authorized(&action, &UtilsResource::CertificatesTemplates) + ); } let authorized_actions = vec![ - UtilsAction::Certificates(UtilsCertificatesAction::GetCertificateTemplate { - template_id, - }), - UtilsAction::Certificates(UtilsCertificatesAction::GenerateSelfSignedCertificate { - template_id, - format: ExportFormat::Pem, - passphrase: None, - }), + UtilsAction::Get { + resource_id: template_id, + }, + UtilsAction::Execute { + resource_id: template_id, + operation: UtilsResourceOperation::CertificatesTemplateGenerate, + }, ]; + for action in authorized_actions.iter() { + assert!(user_share.is_action_authorized(action, &UtilsResource::CertificatesTemplates)); + } + for action in authorized_actions { - assert!(user_share.is_action_authorized(&action)); + assert!( + !user_share.is_action_authorized(&action, &UtilsResource::CertificatesPrivateKeys) + ); } Ok(()) diff --git a/src/utils/utils_action.rs b/src/utils/utils_action.rs index 7d08622..c8366f5 100644 --- a/src/utils/utils_action.rs +++ b/src/utils/utils_action.rs @@ -1,258 +1,69 @@ -use crate::{ - api::Api, - network::{DnsResolver, EmailTransport}, - users::User, - utils::{ - UtilsActionResult, UtilsCertificatesAction, UtilsWebScrapingAction, UtilsWebSecurityAction, - UtilsWebhooksAction, - }, -}; -use serde::Deserialize; +use crate::utils::UtilsResourceOperation; +use uuid::Uuid; -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "type", content = "value")] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum UtilsAction { - Certificates(UtilsCertificatesAction), - Webhooks(UtilsWebhooksAction), - WebScraping(UtilsWebScrapingAction), - WebSecurity(UtilsWebSecurityAction), + /// Get all util's resources (GET). + List, + /// Create util's resource (POST). + Create, + /// Get util's resource details (GET). + Get { resource_id: Uuid }, + /// Update util's resource (PUT). + Update { resource_id: Uuid }, + /// Delete util's resource (DELETE). + Delete { resource_id: Uuid }, + /// Execute util's resource custom operation (POST). + Execute { + resource_id: Uuid, + operation: UtilsResourceOperation, + }, } impl UtilsAction { - /// Validates action parameters and throws if action parameters aren't valid. - pub async fn validate( - &self, - api: &Api, - ) -> anyhow::Result<()> { - match self { - UtilsAction::Certificates(action) => action.validate(), - UtilsAction::Webhooks(action) => action.validate(), - UtilsAction::WebScraping(action) => action.validate(api).await, - UtilsAction::WebSecurity(action) => action.validate(api).await, - } - } - - /// Consumes and handles action. - pub async fn handle( - self, - user: User, - api: &Api, - ) -> anyhow::Result { + /// Returns true if the action requires parameters (via HTTP body). + pub fn requires_params(&self) -> bool { match self { - UtilsAction::Certificates(action) => action - .handle(user, api) - .await - .map(UtilsActionResult::Certificates), - UtilsAction::Webhooks(action) => action - .handle(user, api) - .await - .map(UtilsActionResult::Webhooks), - UtilsAction::WebScraping(action) => action - .handle(user, api) - .await - .map(UtilsActionResult::WebScraping), - UtilsAction::WebSecurity(action) => action - .handle(user, api) - .await - .map(UtilsActionResult::WebSecurity), + UtilsAction::Create | UtilsAction::Update { .. } => true, + UtilsAction::List | UtilsAction::Get { .. } | UtilsAction::Delete { .. } => false, + UtilsAction::Execute { operation, .. } => operation.requires_params(), } } } #[cfg(test)] mod tests { - use crate::{ - network::Network, - tests::{ - mock_api, mock_api_with_network, MockResolver, MockWebPageResourcesTrackerBuilder, - }, - utils::{ - AutoResponder, AutoResponderMethod, ContentSecurityPolicySource, PrivateKeyAlgorithm, - UtilsAction, UtilsCertificatesAction, UtilsWebScrapingAction, UtilsWebSecurityAction, - UtilsWebhooksAction, - }, - }; - use insta::assert_debug_snapshot; - use lettre::transport::stub::AsyncStubTransport; - use std::net::Ipv4Addr; - use trust_dns_resolver::{ - proto::rr::{rdata::A, RData, Record}, - Name, - }; - - fn mock_network_with_records( - records: Vec, - ) -> Network, AsyncStubTransport> { - Network::new( - MockResolver::new_with_records::(records), - AsyncStubTransport::new_ok(), - ) - } - - #[actix_rt::test] - async fn validation_certificates() -> anyhow::Result<()> { - assert!( - UtilsAction::Certificates(UtilsCertificatesAction::CreatePrivateKey { - key_name: "a".repeat(100), - alg: PrivateKeyAlgorithm::Ed25519, - passphrase: None, - }) - .validate(&mock_api().await?) - .await - .is_ok() - ); - - assert_debug_snapshot!(UtilsAction::Certificates(UtilsCertificatesAction::CreatePrivateKey { - key_name: "".to_string(), - alg: PrivateKeyAlgorithm::Ed25519, - passphrase: None, - }).validate(&mock_api().await?).await, @r###" - Err( - "Private key name cannot be empty.", - ) - "###); - - Ok(()) - } - - #[actix_rt::test] - async fn validation_webhooks() -> anyhow::Result<()> { - assert!( - UtilsAction::Webhooks(UtilsWebhooksAction::SaveAutoResponder { - responder: AutoResponder { - path: "/name".to_string(), - method: AutoResponderMethod::Post, - requests_to_track: 3, - status_code: 200, - body: None, - headers: Some(vec![("key".to_string(), "value".to_string())]), - delay: None, - } - }) - .validate(&mock_api().await?) - .await - .is_ok() - ); - - assert_debug_snapshot!(UtilsAction::Webhooks(UtilsWebhooksAction::SaveAutoResponder { - responder: AutoResponder { - path: "/name".to_string(), - method: AutoResponderMethod::Post, - requests_to_track: 3, - status_code: 2000, - body: None, - headers: Some(vec![("key".to_string(), "value".to_string())]), - delay: None, - } - }) - .validate(&mock_api().await?).await, @r###" - Err( - "Auto responder is not valid.", - ) - "###); - - assert!( - UtilsAction::Webhooks(UtilsWebhooksAction::RemoveAutoResponder { - responder_path: "/a".repeat(50), - }) - .validate(&mock_api().await?) - .await - .is_ok() - ); - - assert_debug_snapshot!(UtilsAction::Webhooks(UtilsWebhooksAction::RemoveAutoResponder { - responder_path: "a".to_string(), - }) - .validate(&mock_api().await?).await, @r###" - Err( - "Auto responder path is not valid.", - ) - "###); - - assert!( - UtilsAction::Webhooks(UtilsWebhooksAction::GetAutoRespondersRequests { - responder_path: "/a".repeat(50), - }) - .validate(&mock_api().await?) - .await - .is_ok() - ); - - assert_debug_snapshot!(UtilsAction::Webhooks(UtilsWebhooksAction::GetAutoRespondersRequests { - responder_path: "a".to_string(), - }) - .validate(&mock_api().await?).await, @r###" - Err( - "Auto responder path is not valid.", - ) - "###); - - Ok(()) - } - - #[actix_rt::test] - async fn validation_web_scraping() -> anyhow::Result<()> { - let tracker = MockWebPageResourcesTrackerBuilder::create( - "a".repeat(100), - "http://google.com/my/app?q=2", - 0, - )? - .with_schedule("0 0 0 1 * *") - .with_delay_millis(0) - .build(); - assert!( - UtilsAction::WebScraping(UtilsWebScrapingAction::SaveWebPageResourcesTracker { - tracker - }) - .validate( - &mock_api_with_network(mock_network_with_records::<1>(vec![Record::from_rdata( - Name::new(), - 300, - RData::A(A(Ipv4Addr::new(172, 32, 0, 2))), - )])) - .await? - ) - .await - .is_ok() - ); - - assert_debug_snapshot!(UtilsAction::WebScraping(UtilsWebScrapingAction::FetchWebPageResources { - tracker_name: "".to_string(), - refresh: false, - calculate_diff: false - }) - .validate(&mock_api().await?).await, @r###" - Err( - "Tracker name cannot be empty.", - ) - "###); - - Ok(()) - } - - #[actix_rt::test] - async fn validation_web_security() -> anyhow::Result<()> { - assert!( - UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { - policy_name: "a".repeat(100), - source: ContentSecurityPolicySource::Meta, - }) - .validate(&mock_api().await?) - .await - .is_ok() - ); + use super::UtilsAction; + use crate::utils::UtilsResourceOperation; + use uuid::uuid; + + #[test] + fn properly_checks_if_action_requires_params() { + assert!(UtilsAction::Create.requires_params()); + assert!(UtilsAction::Update { + resource_id: uuid!("00000000-0000-0000-0000-000000000001") + } + .requires_params()); - assert_debug_snapshot!( UtilsAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { - policy_name: "".to_string(), - source: ContentSecurityPolicySource::Meta, - }) - .validate(&mock_api().await?).await, @r###" - Err( - "Policy name cannot be empty.", - ) - "###); + assert!(!UtilsAction::List.requires_params()); + assert!(!UtilsAction::Get { + resource_id: uuid!("00000000-0000-0000-0000-000000000001") + } + .requires_params()); + assert!(!UtilsAction::Delete { + resource_id: uuid!("00000000-0000-0000-0000-000000000001") + } + .requires_params()); - Ok(()) + assert!(UtilsAction::Execute { + resource_id: uuid!("00000000-0000-0000-0000-000000000001"), + operation: UtilsResourceOperation::CertificatesPrivateKeyExport, + } + .requires_params()); + assert!(UtilsAction::Execute { + resource_id: uuid!("00000000-0000-0000-0000-000000000001"), + operation: UtilsResourceOperation::CertificatesTemplateGenerate, + } + .requires_params()); } } diff --git a/src/utils/utils_action_params.rs b/src/utils/utils_action_params.rs new file mode 100644 index 0000000..11a34c7 --- /dev/null +++ b/src/utils/utils_action_params.rs @@ -0,0 +1,52 @@ +use crate::error::Error as SecutilsError; +use anyhow::anyhow; +use serde::de::DeserializeOwned; +use serde_json::Value; + +/// Describes the parameters of an action. +pub struct UtilsActionParams(Value); +impl UtilsActionParams { + /// Creates a new action parameters instance from the given JSON value. + pub fn json(value: Value) -> Self { + Self(value) + } + + /// Consumes and returns the inner value deserialized to a specified type. + pub fn into_inner(self) -> anyhow::Result { + Ok(serde_json::from_value(self.0).map_err(|err| { + SecutilsError::client_with_root_cause( + anyhow!(err).context("Invalid action parameters."), + ) + })?) + } +} + +#[cfg(test)] +mod tests { + use super::UtilsActionParams; + use crate::error::Error as SecutilsError; + use insta::assert_debug_snapshot; + use serde_json::json; + + #[test] + fn properly_returns_inner_value() { + assert_eq!( + UtilsActionParams::json(json!([1, 2, 3])) + .into_inner::>() + .unwrap(), + vec![1, 2, 3] + ); + + assert_debug_snapshot!(UtilsActionParams::json(json!([1, 2, 3])) + .into_inner::>() + .unwrap_err() + .downcast::(), @r###" + Ok( + Error { + context: "Invalid action parameters.", + source: Error("invalid type: number, expected a string", line: 0, column: 0), + }, + ) + "###); + } +} diff --git a/src/utils/utils_action_result.rs b/src/utils/utils_action_result.rs index d73eaca..bb09f4e 100644 --- a/src/utils/utils_action_result.rs +++ b/src/utils/utils_action_result.rs @@ -1,15 +1,70 @@ -use crate::utils::{ - UtilsCertificatesActionResult, UtilsWebScrapingActionResult, UtilsWebSecurityActionResult, - UtilsWebhooksActionResult, -}; +use crate::error::Error as SecutilsError; +use anyhow::anyhow; use serde::Serialize; +use serde_json::Value; -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "type", content = "value")] -pub enum UtilsActionResult { - Certificates(UtilsCertificatesActionResult), - Webhooks(UtilsWebhooksActionResult), - WebScraping(UtilsWebScrapingActionResult), - WebSecurity(UtilsWebSecurityActionResult), +/// Describes the result of an action. +#[derive(Debug)] +pub struct UtilsActionResult(Option); +impl UtilsActionResult { + /// Creates a new action result in JSON format from the given serializable value. + pub fn json(value: impl Serialize) -> anyhow::Result { + let json_value = serde_json::to_value(value).map_err(|err| { + SecutilsError::client_with_root_cause( + anyhow!(err).context("Unable to serialize action result."), + ) + })?; + Ok(Self(Some(json_value))) + } + + /// Creates a new empty action result. + pub fn empty() -> Self { + Self(None) + } + + /// Consumes and returns the inner value. + pub fn into_inner(self) -> Option { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::UtilsActionResult; + use insta::assert_debug_snapshot; + use serde_json::json; + + #[test] + fn properly_returns_inner_value() { + assert_eq!( + UtilsActionResult::json(json!([1, 2, 3])) + .unwrap() + .into_inner() + .unwrap(), + json!([1, 2, 3]) + ); + + assert_eq!(UtilsActionResult::empty().into_inner(), None); + } + + #[test] + fn properly_serializes_to_json() { + assert_debug_snapshot!(UtilsActionResult::json(json!([1, 2, 3])).unwrap(), @r###" + UtilsActionResult( + Some( + Array [ + Number(1), + Number(2), + Number(3), + ], + ), + ) + "###); + + assert_debug_snapshot!(UtilsActionResult::empty(), @r###" + UtilsActionResult( + None, + ) + "###); + } } diff --git a/src/utils/utils_legacy_action.rs b/src/utils/utils_legacy_action.rs new file mode 100644 index 0000000..483bf57 --- /dev/null +++ b/src/utils/utils_legacy_action.rs @@ -0,0 +1,223 @@ +use crate::{ + api::Api, + network::{DnsResolver, EmailTransport}, + users::User, + utils::{ + UtilsLegacyActionResult, UtilsWebScrapingAction, UtilsWebSecurityAction, + UtilsWebhooksAction, + }, +}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type", content = "value")] +pub enum UtilsLegacyAction { + Webhooks(UtilsWebhooksAction), + WebScraping(UtilsWebScrapingAction), + WebSecurity(UtilsWebSecurityAction), +} + +impl UtilsLegacyAction { + /// Validates action parameters and throws if action parameters aren't valid. + pub async fn validate( + &self, + api: &Api, + ) -> anyhow::Result<()> { + match self { + UtilsLegacyAction::Webhooks(action) => action.validate(), + UtilsLegacyAction::WebScraping(action) => action.validate(api).await, + UtilsLegacyAction::WebSecurity(action) => action.validate(api).await, + } + } + + /// Consumes and handles action. + pub async fn handle( + self, + user: User, + api: &Api, + ) -> anyhow::Result { + match self { + UtilsLegacyAction::Webhooks(action) => action + .handle(user, api) + .await + .map(UtilsLegacyActionResult::Webhooks), + UtilsLegacyAction::WebScraping(action) => action + .handle(user, api) + .await + .map(UtilsLegacyActionResult::WebScraping), + UtilsLegacyAction::WebSecurity(action) => action + .handle(user, api) + .await + .map(UtilsLegacyActionResult::WebSecurity), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + network::Network, + tests::{ + mock_api, mock_api_with_network, MockResolver, MockWebPageResourcesTrackerBuilder, + }, + utils::{ + AutoResponder, AutoResponderMethod, ContentSecurityPolicySource, UtilsLegacyAction, + UtilsWebScrapingAction, UtilsWebSecurityAction, UtilsWebhooksAction, + }, + }; + use insta::assert_debug_snapshot; + use lettre::transport::stub::AsyncStubTransport; + use std::net::Ipv4Addr; + use trust_dns_resolver::{ + proto::rr::{rdata::A, RData, Record}, + Name, + }; + + fn mock_network_with_records( + records: Vec, + ) -> Network, AsyncStubTransport> { + Network::new( + MockResolver::new_with_records::(records), + AsyncStubTransport::new_ok(), + ) + } + + #[actix_rt::test] + async fn validation_webhooks() -> anyhow::Result<()> { + assert!( + UtilsLegacyAction::Webhooks(UtilsWebhooksAction::SaveAutoResponder { + responder: AutoResponder { + path: "/name".to_string(), + method: AutoResponderMethod::Post, + requests_to_track: 3, + status_code: 200, + body: None, + headers: Some(vec![("key".to_string(), "value".to_string())]), + delay: None, + } + }) + .validate(&mock_api().await?) + .await + .is_ok() + ); + + assert_debug_snapshot!(UtilsLegacyAction::Webhooks(UtilsWebhooksAction::SaveAutoResponder { + responder: AutoResponder { + path: "/name".to_string(), + method: AutoResponderMethod::Post, + requests_to_track: 3, + status_code: 2000, + body: None, + headers: Some(vec![("key".to_string(), "value".to_string())]), + delay: None, + } + }) + .validate(&mock_api().await?).await, @r###" + Err( + "Auto responder is not valid.", + ) + "###); + + assert!( + UtilsLegacyAction::Webhooks(UtilsWebhooksAction::RemoveAutoResponder { + responder_path: "/a".repeat(50), + }) + .validate(&mock_api().await?) + .await + .is_ok() + ); + + assert_debug_snapshot!(UtilsLegacyAction::Webhooks(UtilsWebhooksAction::RemoveAutoResponder { + responder_path: "a".to_string(), + }) + .validate(&mock_api().await?).await, @r###" + Err( + "Auto responder path is not valid.", + ) + "###); + + assert!( + UtilsLegacyAction::Webhooks(UtilsWebhooksAction::GetAutoRespondersRequests { + responder_path: "/a".repeat(50), + }) + .validate(&mock_api().await?) + .await + .is_ok() + ); + + assert_debug_snapshot!(UtilsLegacyAction::Webhooks(UtilsWebhooksAction::GetAutoRespondersRequests { + responder_path: "a".to_string(), + }) + .validate(&mock_api().await?).await, @r###" + Err( + "Auto responder path is not valid.", + ) + "###); + + Ok(()) + } + + #[actix_rt::test] + async fn validation_web_scraping() -> anyhow::Result<()> { + let tracker = MockWebPageResourcesTrackerBuilder::create( + "a".repeat(100), + "http://google.com/my/app?q=2", + 0, + )? + .with_schedule("0 0 0 1 * *") + .with_delay_millis(0) + .build(); + assert!(UtilsLegacyAction::WebScraping( + UtilsWebScrapingAction::SaveWebPageResourcesTracker { tracker } + ) + .validate( + &mock_api_with_network(mock_network_with_records::<1>(vec![Record::from_rdata( + Name::new(), + 300, + RData::A(A(Ipv4Addr::new(172, 32, 0, 2))), + )])) + .await? + ) + .await + .is_ok()); + + assert_debug_snapshot!(UtilsLegacyAction::WebScraping(UtilsWebScrapingAction::FetchWebPageResources { + tracker_name: "".to_string(), + refresh: false, + calculate_diff: false + }) + .validate(&mock_api().await?).await, @r###" + Err( + "Tracker name cannot be empty.", + ) + "###); + + Ok(()) + } + + #[actix_rt::test] + async fn validation_web_security() -> anyhow::Result<()> { + assert!(UtilsLegacyAction::WebSecurity( + UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "a".repeat(100), + source: ContentSecurityPolicySource::Meta, + } + ) + .validate(&mock_api().await?) + .await + .is_ok()); + + assert_debug_snapshot!(UtilsLegacyAction::WebSecurity(UtilsWebSecurityAction::SerializeContentSecurityPolicy { + policy_name: "".to_string(), + source: ContentSecurityPolicySource::Meta, + }) + .validate(&mock_api().await?).await, @r###" + Err( + "Policy name cannot be empty.", + ) + "###); + + Ok(()) + } +} diff --git a/src/utils/utils_legacy_action_result.rs b/src/utils/utils_legacy_action_result.rs new file mode 100644 index 0000000..cfe53df --- /dev/null +++ b/src/utils/utils_legacy_action_result.rs @@ -0,0 +1,13 @@ +use crate::utils::{ + UtilsWebScrapingActionResult, UtilsWebSecurityActionResult, UtilsWebhooksActionResult, +}; +use serde::Serialize; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type", content = "value")] +pub enum UtilsLegacyActionResult { + Webhooks(UtilsWebhooksActionResult), + WebScraping(UtilsWebScrapingActionResult), + WebSecurity(UtilsWebSecurityActionResult), +} diff --git a/src/utils/utils_resource.rs b/src/utils/utils_resource.rs new file mode 100644 index 0000000..5899732 --- /dev/null +++ b/src/utils/utils_resource.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum UtilsResource { + CertificatesTemplates, + CertificatesPrivateKeys, +} + +impl TryFrom<(&str, &str)> for UtilsResource { + type Error = (); + + fn try_from((area, resource): (&str, &str)) -> Result { + match (area, resource) { + ("certificates", "templates") => Ok(UtilsResource::CertificatesTemplates), + ("certificates", "private_keys") => Ok(UtilsResource::CertificatesPrivateKeys), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::UtilsResource; + + #[test] + fn properly_parses_resource() { + assert_eq!( + UtilsResource::try_from(("certificates", "templates")), + Ok(UtilsResource::CertificatesTemplates) + ); + assert_eq!( + UtilsResource::try_from(("certificates", "private_keys")), + Ok(UtilsResource::CertificatesPrivateKeys) + ); + + assert!(UtilsResource::try_from(("certificates_", "templates")).is_err()); + assert!(UtilsResource::try_from(("certificates_", "private_keys")).is_err()); + } +} diff --git a/src/utils/utils_resource_operation.rs b/src/utils/utils_resource_operation.rs new file mode 100644 index 0000000..840e1dd --- /dev/null +++ b/src/utils/utils_resource_operation.rs @@ -0,0 +1,105 @@ +use crate::utils::UtilsResource; + +/// Describe custom util's resource operation. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum UtilsResourceOperation { + CertificatesTemplateGenerate, + CertificatesTemplateShare, + CertificatesTemplateUnshare, + CertificatesPrivateKeyExport, +} + +impl UtilsResourceOperation { + /// Returns true if the operation requires parameters (via HTTP body). + pub fn requires_params(&self) -> bool { + matches!( + self, + Self::CertificatesTemplateGenerate | Self::CertificatesPrivateKeyExport + ) + } +} + +impl TryFrom<(&UtilsResource, &str)> for UtilsResourceOperation { + type Error = (); + + fn try_from((resource, operation): (&UtilsResource, &str)) -> Result { + match resource { + // Private keys custom actions. + UtilsResource::CertificatesPrivateKeys if operation == "export" => { + Ok(UtilsResourceOperation::CertificatesPrivateKeyExport) + } + + // Certificate templates custom actions. + UtilsResource::CertificatesTemplates if operation == "generate" => { + Ok(UtilsResourceOperation::CertificatesTemplateGenerate) + } + UtilsResource::CertificatesTemplates if operation == "share" => { + Ok(UtilsResourceOperation::CertificatesTemplateShare) + } + UtilsResource::CertificatesTemplates if operation == "unshare" => { + Ok(UtilsResourceOperation::CertificatesTemplateUnshare) + } + + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::UtilsResourceOperation; + use crate::utils::UtilsResource; + + #[test] + fn properly_checks_if_action_requires_params() { + assert!(UtilsResourceOperation::CertificatesPrivateKeyExport.requires_params()); + + assert!(UtilsResourceOperation::CertificatesTemplateGenerate.requires_params()); + assert!(!UtilsResourceOperation::CertificatesTemplateShare.requires_params()); + assert!(!UtilsResourceOperation::CertificatesTemplateUnshare.requires_params()); + } + + #[test] + fn properly_parses_resource_action_operation() { + assert_eq!( + UtilsResourceOperation::try_from((&UtilsResource::CertificatesPrivateKeys, "export")), + Ok(UtilsResourceOperation::CertificatesPrivateKeyExport) + ); + assert!(UtilsResourceOperation::try_from(( + &UtilsResource::CertificatesTemplates, + "export" + )) + .is_err()); + + assert_eq!( + UtilsResourceOperation::try_from((&UtilsResource::CertificatesTemplates, "generate")), + Ok(UtilsResourceOperation::CertificatesTemplateGenerate) + ); + assert!(UtilsResourceOperation::try_from(( + &UtilsResource::CertificatesPrivateKeys, + "generate" + )) + .is_err()); + + assert_eq!( + UtilsResourceOperation::try_from((&UtilsResource::CertificatesTemplates, "share")), + Ok(UtilsResourceOperation::CertificatesTemplateShare) + ); + assert!(UtilsResourceOperation::try_from(( + &UtilsResource::CertificatesPrivateKeys, + "share" + )) + .is_err()); + + assert_eq!( + UtilsResourceOperation::try_from((&UtilsResource::CertificatesTemplates, "unshare")), + Ok(UtilsResourceOperation::CertificatesTemplateUnshare) + ); + assert!(UtilsResourceOperation::try_from(( + &UtilsResource::CertificatesPrivateKeys, + "unshare" + )) + .is_err()); + } +} diff --git a/tools/api/utils/certificates.http b/tools/api/utils/certificates.http deleted file mode 100644 index 0fafb3b..0000000 --- a/tools/api/utils/certificates.http +++ /dev/null @@ -1,30 +0,0 @@ -### Generate RSA key pair. -POST {{host}}/api/utils/action -Authorization: {{api-credentials}} -Accept: application/json -Content-Type: application/json - -{ - "action": { - "type": "certificates", - "value": { "type": "generateRsaKeyPair" } - } -} - -### Generate self-signed CA. -POST {{host}}/api/utils/action -Authorization: {{api-credentials}} -Accept: application/json -Content-Type: application/json - -{ - "action": { - "type": "certificates", - "value": { - "type": "generateSelfSignedCertificate", - "value": { "templateName": "temp" } - } - } -} - -### diff --git a/tools/api/utils/certificates_private_keys.http b/tools/api/utils/certificates_private_keys.http index ae4bd2e..a805b56 100644 --- a/tools/api/utils/certificates_private_keys.http +++ b/tools/api/utils/certificates_private_keys.http @@ -1,62 +1,46 @@ -### Create private key (RSA, without passphrase). -POST {{host}}/api/utils/action +### List private keys. +GET {{host}}/api/utils/certificates/private_keys Authorization: {{api-credentials}} Accept: application/json -Content-Type: application/json -{ - "action": { - "type": "certificates", - "value": { - "type": "createPrivateKey", - "value": { "keyName": "pk", "alg": { "keyType": "rsa", "keySize": "1024" } } - } - } -} +### Get private key by ID +GET {{host}}/api/utils/certificates/private_keys/018b7720-7d12-7cd1-9fde-81a4b109199f +Authorization: {{api-credentials}} +Accept: application/json -### Create private key (ed25519, with passphrase). -POST {{host}}/api/utils/action +### Create private key (RSA, without passphrase). +POST {{host}}/api/utils/certificates/private_keys Authorization: {{api-credentials}} Accept: application/json Content-Type: application/json { - "action": { - "type": "certificates", - "value": { - "type": "createPrivateKey", - "value": { "keyName": "pk-ed25519", "alg": { "keyType": "ed25519" }, "passphrase": "123456" } - } - } + "keyName": "pk", + "alg": { "keyType": "rsa", "keySize": "1024" } } -### - -### Export private key. -POST {{host}}/api/utils/action +### Update private key. +PUT {{host}}/api/utils/certificates/private_keys/018b77c3-a901-7272-a5d3-41ca17d163c9 Authorization: {{api-credentials}} Accept: application/json Content-Type: application/json { - "action": { - "type": "certificates", - "value": { - "type": "exportPrivateKey", - "value": { "keyId": "018b4a1b-92d3-739d-a4dd-683d9eb47ce9", "format": "pem", "passphrase": "123456" } - } - } + "keyName": "pk-new-name", + "newPassphrase": "123" } -### Get private keys. -POST {{host}}/api/utils/action +### Delete private key. +DELETE {{host}}/api/utils/certificates/private_keys/018b7720-7d12-7cd1-9fde-81a4b109199f +Authorization: {{api-credentials}} + +### Export private key. +POST {{host}}/api/utils/certificates/private_keys/018b77c3-a901-7272-a5d3-41ca17d163c9/export Authorization: {{api-credentials}} Accept: application/json Content-Type: application/json { - "action": { - "type": "certificates", - "value": { "type": "getPrivateKeys" } - } + "format": "pem", + "passphrase": "123" } diff --git a/tools/api/utils/certificates_templates.http b/tools/api/utils/certificates_templates.http new file mode 100644 index 0000000..9e88f0a --- /dev/null +++ b/tools/api/utils/certificates_templates.http @@ -0,0 +1,61 @@ +### List templates. +GET {{host}}/api/utils/certificates/templates +Authorization: {{api-credentials}} +Accept: application/json + +### Get template by ID +GET {{host}}/api/utils/certificates/templates/018b77c7-f3b6-75f0-b5e8-47d0c73772bb +Authorization: {{api-credentials}} +Accept: application/json + +### Create template +POST {{host}}/api/utils/certificates/templates +Authorization: {{api-credentials}} +Accept: application/json +Content-Type: application/json + +{ + "templateName": "ct", + "attributes": { + "commonName": "CA Issuer", + "keyAlgorithm": { "keyType": "ed25519" }, + "signatureAlgorithm": "ed25519", + "notValidBefore": 1698521570, + "notValidAfter":1730147570, + "isCa":false + } +} + +### Update template +PUT {{host}}/api/utils/certificates/templates/018b77c7-f3b6-75f0-b5e8-47d0c73772bb +Authorization: {{api-credentials}} +Accept: application/json +Content-Type: application/json + +{ + "templateName": "ct-new-name" +} + +### Delete template. +DELETE {{host}}/api/utils/certificates/templates/018b593c-62db-70cd-83c6-598a54067ec2 +Authorization: {{api-credentials}} + +### Generate certificate and private key pair. +POST {{host}}/api/utils/certificates/templates/018b593c-62db-70cd-83c6-598a54067ec2/generate +Authorization: {{api-credentials}} +Accept: application/json +Content-Type: application/json + +{ + "format": "pem" +} + +### Share template +POST {{host}}/api/utils/certificates/templates/018b77c7-f3b6-75f0-b5e8-47d0c73772bb/share +Authorization: {{api-credentials}} +Accept: application/json + +### Unshare template +POST {{host}}/api/utils/certificates/templates/018b77c7-f3b6-75f0-b5e8-47d0c73772bb/unshare +Authorization: {{api-credentials}} +Accept: application/json