move basic ACME types into src/api2/types/acme.rs

And rename AccountName into AcmeAccountName.
This commit is contained in:
Dietmar Maurer 2021-05-04 11:29:27 +02:00
parent 603aa09d54
commit 39c5db7f0f
10 changed files with 168 additions and 149 deletions

View File

@ -17,7 +17,8 @@ use proxmox_acme_rs::order::{Order, OrderData};
use proxmox_acme_rs::Request as AcmeRequest; use proxmox_acme_rs::Request as AcmeRequest;
use proxmox_acme_rs::{Account, Authorization, Challenge, Directory, Error, ErrorResponse}; use proxmox_acme_rs::{Account, Authorization, Challenge, Directory, Error, ErrorResponse};
use crate::config::acme::{account_path, AccountName}; use crate::api2::types::AcmeAccountName;
use crate::config::acme::account_path;
use crate::tools::http::SimpleHttp; use crate::tools::http::SimpleHttp;
/// Our on-disk format inherited from PVE's proxmox-acme code. /// Our on-disk format inherited from PVE's proxmox-acme code.
@ -76,7 +77,7 @@ impl AcmeClient {
} }
/// Load an existing ACME account by name. /// Load an existing ACME account by name.
pub async fn load(account_name: &AccountName) -> Result<Self, anyhow::Error> { pub async fn load(account_name: &AcmeAccountName) -> Result<Self, anyhow::Error> {
Self::load_path(account_path(account_name.as_ref())).await Self::load_path(account_path(account_name.as_ref())).await
} }
@ -98,7 +99,7 @@ impl AcmeClient {
pub async fn new_account<'a>( pub async fn new_account<'a>(
&'a mut self, &'a mut self,
account_name: &AccountName, account_name: &AcmeAccountName,
tos_agreed: bool, tos_agreed: bool,
contact: Vec<String>, contact: Vec<String>,
rsa_bits: Option<u32>, rsa_bits: Option<u32>,

View File

@ -11,7 +11,7 @@ use tokio::process::Command;
use proxmox_acme_rs::{Authorization, Challenge}; use proxmox_acme_rs::{Authorization, Challenge};
use crate::acme::AcmeClient; use crate::acme::AcmeClient;
use crate::config::acme::AcmeDomain; use crate::api2::types::AcmeDomain;
use crate::server::WorkerTask; use crate::server::WorkerTask;
use crate::config::acme::plugin::{DnsPlugin, PluginData}; use crate::config::acme::plugin::{DnsPlugin, PluginData};

View File

@ -14,12 +14,11 @@ use proxmox_acme_rs::account::AccountData as AcmeAccountData;
use proxmox_acme_rs::Account; use proxmox_acme_rs::Account;
use crate::acme::AcmeClient; use crate::acme::AcmeClient;
use crate::api2::types::Authid;
use crate::config::acl::PRIV_SYS_MODIFY; use crate::config::acl::PRIV_SYS_MODIFY;
use crate::config::acme::plugin::{ use crate::config::acme::plugin::{
DnsPlugin, DnsPluginCore, DnsPluginCoreUpdater, PLUGIN_ID_SCHEMA, DnsPlugin, DnsPluginCore, DnsPluginCoreUpdater, PLUGIN_ID_SCHEMA,
}; };
use crate::config::acme::{AccountName, KnownAcmeDirectory}; use crate::api2::types::{Authid, KnownAcmeDirectory, AcmeAccountName};
use crate::server::WorkerTask; use crate::server::WorkerTask;
use crate::tools::ControlFlow; use crate::tools::ControlFlow;
@ -65,7 +64,7 @@ const PLUGIN_ITEM_ROUTER: Router = Router::new()
#[api( #[api(
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
}, },
)] )]
/// An ACME Account entry. /// An ACME Account entry.
@ -73,7 +72,7 @@ const PLUGIN_ITEM_ROUTER: Router = Router::new()
/// Currently only contains a 'name' property. /// Currently only contains a 'name' property.
#[derive(Serialize)] #[derive(Serialize)]
pub struct AccountEntry { pub struct AccountEntry {
name: AccountName, name: AcmeAccountName,
} }
#[api( #[api(
@ -128,7 +127,7 @@ pub struct AccountInfo {
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
}, },
}, },
access: { access: {
@ -138,7 +137,7 @@ pub struct AccountInfo {
protected: true, protected: true,
)] )]
/// Return existing ACME account information. /// Return existing ACME account information.
pub async fn get_account(name: AccountName) -> Result<AccountInfo, Error> { pub async fn get_account(name: AcmeAccountName) -> Result<AccountInfo, Error> {
let client = AcmeClient::load(&name).await?; let client = AcmeClient::load(&name).await?;
let account = client.account()?; let account = client.account()?;
Ok(AccountInfo { Ok(AccountInfo {
@ -162,7 +161,7 @@ fn account_contact_from_string(s: &str) -> Vec<String> {
input: { input: {
properties: { properties: {
name: { name: {
type: AccountName, type: AcmeAccountName,
optional: true, optional: true,
}, },
contact: { contact: {
@ -186,7 +185,7 @@ fn account_contact_from_string(s: &str) -> Vec<String> {
)] )]
/// Register an ACME account. /// Register an ACME account.
fn register_account( fn register_account(
name: Option<AccountName>, name: Option<AcmeAccountName>,
// Todo: email & email-list schema // Todo: email & email-list schema
contact: String, contact: String,
tos_url: Option<String>, tos_url: Option<String>,
@ -196,7 +195,7 @@ fn register_account(
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let name = name let name = name
.unwrap_or_else(|| unsafe { AccountName::from_string_unchecked("default".to_string()) }); .unwrap_or_else(|| unsafe { AcmeAccountName::from_string_unchecked("default".to_string()) });
if Path::new(&crate::config::acme::account_path(&name)).exists() { if Path::new(&crate::config::acme::account_path(&name)).exists() {
http_bail!(BAD_REQUEST, "account {:?} already exists", name); http_bail!(BAD_REQUEST, "account {:?} already exists", name);
@ -233,7 +232,7 @@ fn register_account(
pub async fn do_register_account<'a>( pub async fn do_register_account<'a>(
client: &'a mut AcmeClient, client: &'a mut AcmeClient,
name: &AccountName, name: &AcmeAccountName,
agree_to_tos: bool, agree_to_tos: bool,
contact: String, contact: String,
rsa_bits: Option<u32>, rsa_bits: Option<u32>,
@ -247,7 +246,7 @@ pub async fn do_register_account<'a>(
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
contact: { contact: {
description: "List of email addresses.", description: "List of email addresses.",
optional: true, optional: true,
@ -261,7 +260,7 @@ pub async fn do_register_account<'a>(
)] )]
/// Update an ACME account. /// Update an ACME account.
pub fn update_account( pub fn update_account(
name: AccountName, name: AcmeAccountName,
// Todo: email & email-list schema // Todo: email & email-list schema
contact: Option<String>, contact: Option<String>,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
@ -291,7 +290,7 @@ pub fn update_account(
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
force: { force: {
description: description:
"Delete account data even if the server refuses to deactivate the account.", "Delete account data even if the server refuses to deactivate the account.",
@ -307,7 +306,7 @@ pub fn update_account(
)] )]
/// Deactivate an ACME account. /// Deactivate an ACME account.
pub fn deactivate_account( pub fn deactivate_account(
name: AccountName, name: AcmeAccountName,
force: bool, force: bool,
rpcenv: &mut dyn RpcEnvironment, rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> { ) -> Result<String, Error> {

View File

@ -14,8 +14,8 @@ use proxmox::list_subdirs_api_method;
use crate::acme::AcmeClient; use crate::acme::AcmeClient;
use crate::api2::types::Authid; use crate::api2::types::Authid;
use crate::api2::types::NODE_SCHEMA; use crate::api2::types::NODE_SCHEMA;
use crate::api2::types::AcmeDomain;
use crate::config::acl::PRIV_SYS_MODIFY; use crate::config::acl::PRIV_SYS_MODIFY;
use crate::config::acme::AcmeDomain;
use crate::config::node::NodeConfig; use crate::config::node::NodeConfig;
use crate::server::WorkerTask; use crate::server::WorkerTask;
use crate::tools::cert; use crate::tools::cert;

126
src/api2/types/acme.rs Normal file
View File

@ -0,0 +1,126 @@
use std::fmt;
use anyhow::Error;
use serde::{Deserialize, Serialize};
use proxmox::api::{api, schema::{Schema, StringSchema, ApiStringFormat}};
use crate::api2::types::{
DNS_ALIAS_FORMAT, DNS_NAME_FORMAT, PROXMOX_SAFE_ID_FORMAT,
};
#[api(
properties: {
"domain": { format: &DNS_NAME_FORMAT },
"alias": {
optional: true,
format: &DNS_ALIAS_FORMAT,
},
"plugin": {
optional: true,
format: &PROXMOX_SAFE_ID_FORMAT,
},
},
default_key: "domain",
)]
#[derive(Deserialize, Serialize)]
/// A domain entry for an ACME certificate.
pub struct AcmeDomain {
/// The domain to certify for.
pub domain: String,
/// The domain to use for challenges instead of the default acme challenge domain.
///
/// This is useful if you use CNAME entries to redirect `_acme-challenge.*` domains to a
/// different DNS server.
#[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
/// The plugin to use to validate this domain.
///
/// Empty means standalone HTTP validation is used.
#[serde(skip_serializing_if = "Option::is_none")]
pub plugin: Option<String>,
}
pub const ACME_DOMAIN_PROPERTY_SCHEMA: Schema = StringSchema::new(
"ACME domain configuration string")
.format(&ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA))
.schema();
#[api(
properties: {
name: { type: String },
url: { type: String },
},
)]
/// An ACME directory endpoint with a name and URL.
#[derive(Serialize)]
pub struct KnownAcmeDirectory {
/// The ACME directory's name.
pub name: &'static str,
/// The ACME directory's endpoint URL.
pub url: &'static str,
}
#[api(format: &PROXMOX_SAFE_ID_FORMAT)]
/// ACME account name.
#[derive(Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct AcmeAccountName(String);
impl AcmeAccountName {
pub fn into_string(self) -> String {
self.0
}
pub fn from_string(name: String) -> Result<Self, Error> {
match &Self::API_SCHEMA {
Schema::String(s) => s.check_constraints(&name)?,
_ => unreachable!(),
}
Ok(Self(name))
}
pub unsafe fn from_string_unchecked(name: String) -> Self {
Self(name)
}
}
impl std::ops::Deref for AcmeAccountName {
type Target = str;
#[inline]
fn deref(&self) -> &str {
&self.0
}
}
impl std::ops::DerefMut for AcmeAccountName {
#[inline]
fn deref_mut(&mut self) -> &mut str {
&mut self.0
}
}
impl AsRef<str> for AcmeAccountName {
#[inline]
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Debug for AcmeAccountName {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl fmt::Display for AcmeAccountName {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

View File

@ -37,6 +37,9 @@ pub use tape::*;
mod file_restore; mod file_restore;
pub use file_restore::*; pub use file_restore::*;
mod acme;
pub use acme::*;
// File names: may not contain slashes, may not start with "." // File names: may not contain slashes, may not start with "."
pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
if name.starts_with('.') { if name.starts_with('.') {

View File

@ -8,8 +8,9 @@ use proxmox::tools::fs::file_get_contents;
use proxmox_backup::acme::AcmeClient; use proxmox_backup::acme::AcmeClient;
use proxmox_backup::api2; use proxmox_backup::api2;
use proxmox_backup::api2::types::AcmeAccountName;
use proxmox_backup::config::acme::plugin::DnsPluginCoreUpdater; use proxmox_backup::config::acme::plugin::DnsPluginCoreUpdater;
use proxmox_backup::config::acme::{AccountName, KNOWN_ACME_DIRECTORIES}; use proxmox_backup::config::acme::KNOWN_ACME_DIRECTORIES;
pub fn acme_mgmt_cli() -> CommandLineInterface { pub fn acme_mgmt_cli() -> CommandLineInterface {
let cmd_def = CliCommandMap::new() let cmd_def = CliCommandMap::new()
@ -49,7 +50,7 @@ fn list_accounts(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<(), Er
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
"output-format": { "output-format": {
schema: OUTPUT_FORMAT, schema: OUTPUT_FORMAT,
optional: true, optional: true,
@ -83,7 +84,7 @@ async fn get_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<()
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
contact: { contact: {
description: "List of email addresses.", description: "List of email addresses.",
}, },
@ -97,7 +98,7 @@ async fn get_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<()
)] )]
/// Register an ACME account. /// Register an ACME account.
async fn register_account( async fn register_account(
name: AccountName, name: AcmeAccountName,
contact: String, contact: String,
directory: Option<String>, directory: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
@ -169,7 +170,7 @@ async fn register_account(
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
contact: { contact: {
description: "List of email addresses.", description: "List of email addresses.",
type: String, type: String,
@ -194,7 +195,7 @@ async fn update_account(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result
#[api( #[api(
input: { input: {
properties: { properties: {
name: { type: AccountName }, name: { type: AcmeAccountName },
force: { force: {
description: description:
"Delete account data even if the server refuses to deactivate the account.", "Delete account data even if the server refuses to deactivate the account.",

View File

@ -1,16 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::path::Path; use std::path::Path;
use anyhow::{bail, format_err, Error}; use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use proxmox::api::{api, schema::{Schema, StringSchema, ApiStringFormat}};
use proxmox::sys::error::SysError; use proxmox::sys::error::SysError;
use proxmox::tools::fs::CreateOptions; use proxmox::tools::fs::CreateOptions;
use crate::api2::types::{ use crate::api2::types::{
DNS_ALIAS_FORMAT, DNS_NAME_FORMAT, PROXMOX_SAFE_ID_FORMAT, PROXMOX_SAFE_ID_REGEX, PROXMOX_SAFE_ID_REGEX,
KnownAcmeDirectory,
AcmeAccountName,
}; };
use crate::tools::ControlFlow; use crate::tools::ControlFlow;
@ -44,61 +43,6 @@ pub(crate) fn make_acme_account_dir() -> nix::Result<()> {
create_acme_subdir(ACME_ACCOUNT_DIR) create_acme_subdir(ACME_ACCOUNT_DIR)
} }
#[api(
properties: {
"domain": { format: &DNS_NAME_FORMAT },
"alias": {
optional: true,
format: &DNS_ALIAS_FORMAT,
},
"plugin": {
optional: true,
format: &PROXMOX_SAFE_ID_FORMAT,
},
},
default_key: "domain",
)]
#[derive(Deserialize, Serialize)]
/// A domain entry for an ACME certificate.
pub struct AcmeDomain {
/// The domain to certify for.
pub domain: String,
/// The domain to use for challenges instead of the default acme challenge domain.
///
/// This is useful if you use CNAME entries to redirect `_acme-challenge.*` domains to a
/// different DNS server.
#[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
/// The plugin to use to validate this domain.
///
/// Empty means standalone HTTP validation is used.
#[serde(skip_serializing_if = "Option::is_none")]
pub plugin: Option<String>,
}
pub const ACME_DOMAIN_PROPERTY_SCHEMA: Schema = StringSchema::new(
"ACME domain configuration string")
.format(&ApiStringFormat::PropertyString(&AcmeDomain::API_SCHEMA))
.schema();
#[api(
properties: {
name: { type: String },
url: { type: String },
},
)]
/// An ACME directory endpoint with a name and URL.
#[derive(Serialize)]
pub struct KnownAcmeDirectory {
/// The ACME directory's name.
pub name: &'static str,
/// The ACME directory's endpoint URL.
pub url: &'static str,
}
pub const KNOWN_ACME_DIRECTORIES: &[KnownAcmeDirectory] = &[ pub const KNOWN_ACME_DIRECTORIES: &[KnownAcmeDirectory] = &[
KnownAcmeDirectory { KnownAcmeDirectory {
name: "Let's Encrypt V2", name: "Let's Encrypt V2",
@ -116,70 +60,10 @@ pub fn account_path(name: &str) -> String {
format!("{}/{}", ACME_ACCOUNT_DIR, name) format!("{}/{}", ACME_ACCOUNT_DIR, name)
} }
#[api(format: &PROXMOX_SAFE_ID_FORMAT)]
/// ACME account name.
#[derive(Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct AccountName(String);
impl AccountName {
pub fn into_string(self) -> String {
self.0
}
pub fn from_string(name: String) -> Result<Self, Error> {
match &Self::API_SCHEMA {
Schema::String(s) => s.check_constraints(&name)?,
_ => unreachable!(),
}
Ok(Self(name))
}
pub unsafe fn from_string_unchecked(name: String) -> Self {
Self(name)
}
}
impl std::ops::Deref for AccountName {
type Target = str;
#[inline]
fn deref(&self) -> &str {
&self.0
}
}
impl std::ops::DerefMut for AccountName {
#[inline]
fn deref_mut(&mut self) -> &mut str {
&mut self.0
}
}
impl AsRef<str> for AccountName {
#[inline]
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Debug for AccountName {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl fmt::Display for AccountName {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
pub fn foreach_acme_account<F>(mut func: F) -> Result<(), Error> pub fn foreach_acme_account<F>(mut func: F) -> Result<(), Error>
where where
F: FnMut(AccountName) -> ControlFlow<Result<(), Error>>, F: FnMut(AcmeAccountName) -> ControlFlow<Result<(), Error>>,
{ {
match crate::tools::fs::scan_subdir(-1, ACME_ACCOUNT_DIR, &PROXMOX_SAFE_ID_REGEX) { match crate::tools::fs::scan_subdir(-1, ACME_ACCOUNT_DIR, &PROXMOX_SAFE_ID_REGEX) {
Ok(files) => { Ok(files) => {
@ -191,7 +75,10 @@ where
continue; continue;
} }
let account_name = AccountName(file_name.to_owned()); let account_name = match AcmeAccountName::from_string(file_name.to_owned()) {
Ok(account_name) => account_name,
Err(_) => continue,
};
if let ControlFlow::Break(result) = func(account_name) { if let ControlFlow::Break(result) = func(account_name) {
return result; return result;

View File

@ -15,6 +15,8 @@ use crate::api2::types::PROXMOX_SAFE_ID_FORMAT;
pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.") pub const PLUGIN_ID_SCHEMA: Schema = StringSchema::new("ACME Challenge Plugin ID.")
.format(&PROXMOX_SAFE_ID_FORMAT) .format(&PROXMOX_SAFE_ID_FORMAT)
.min_length(1)
.max_length(32)
.schema(); .schema();
lazy_static! { lazy_static! {

View File

@ -10,8 +10,8 @@ use proxmox::api::api;
use proxmox::api::schema::{ApiStringFormat, Updater}; use proxmox::api::schema::{ApiStringFormat, Updater};
use proxmox::tools::fs::{replace_file, CreateOptions}; use proxmox::tools::fs::{replace_file, CreateOptions};
use crate::api2::types::{AcmeDomain, AcmeAccountName, ACME_DOMAIN_PROPERTY_SCHEMA};
use crate::acme::AcmeClient; use crate::acme::AcmeClient;
use crate::config::acme::{AccountName, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA};
const CONF_FILE: &str = configdir!("/node.cfg"); const CONF_FILE: &str = configdir!("/node.cfg");
const LOCK_FILE: &str = configdir!("/.node.lck"); const LOCK_FILE: &str = configdir!("/.node.lck");
@ -49,7 +49,7 @@ pub fn save_config(config: &NodeConfig) -> Result<(), Error> {
#[api( #[api(
properties: { properties: {
account: { type: AccountName }, account: { type: AcmeAccountName },
} }
)] )]
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -58,7 +58,7 @@ pub fn save_config(config: &NodeConfig) -> Result<(), Error> {
/// Currently only contains the name of the account use. /// Currently only contains the name of the account use.
pub struct AcmeConfig { pub struct AcmeConfig {
/// Account to use to acquire ACME certificates. /// Account to use to acquire ACME certificates.
account: AccountName, account: AcmeAccountName,
} }
#[api( #[api(