mirror of
				https://git.proxmox.com/git/proxmox-backup
				synced 2025-11-02 15:18:42 +00:00 
			
		
		
		
	add config/acme api path
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
		
							parent
							
								
									426847e1ce
								
							
						
					
					
						commit
						d4b84c1dec
					
				@ -4,6 +4,7 @@ use proxmox::api::router::{Router, SubdirMap};
 | 
			
		||||
use proxmox::list_subdirs_api_method;
 | 
			
		||||
 | 
			
		||||
pub mod access;
 | 
			
		||||
pub mod acme;
 | 
			
		||||
pub mod datastore;
 | 
			
		||||
pub mod remote;
 | 
			
		||||
pub mod sync;
 | 
			
		||||
@ -16,6 +17,7 @@ pub mod tape_backup_job;
 | 
			
		||||
 | 
			
		||||
const SUBDIRS: SubdirMap = &[
 | 
			
		||||
    ("access", &access::ROUTER),
 | 
			
		||||
    ("acme", &acme::ROUTER),
 | 
			
		||||
    ("changer", &changer::ROUTER),
 | 
			
		||||
    ("datastore", &datastore::ROUTER),
 | 
			
		||||
    ("drive", &drive::ROUTER),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										725
									
								
								src/api2/config/acme.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										725
									
								
								src/api2/config/acme.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,725 @@
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_json::{json, Value};
 | 
			
		||||
 | 
			
		||||
use proxmox::api::router::SubdirMap;
 | 
			
		||||
use proxmox::api::schema::Updatable;
 | 
			
		||||
use proxmox::api::{api, Permission, Router, RpcEnvironment};
 | 
			
		||||
use proxmox::http_bail;
 | 
			
		||||
use proxmox::list_subdirs_api_method;
 | 
			
		||||
 | 
			
		||||
use proxmox_acme_rs::account::AccountData as AcmeAccountData;
 | 
			
		||||
use proxmox_acme_rs::Account;
 | 
			
		||||
 | 
			
		||||
use crate::acme::AcmeClient;
 | 
			
		||||
use crate::api2::types::Authid;
 | 
			
		||||
use crate::config::acl::PRIV_SYS_MODIFY;
 | 
			
		||||
use crate::config::acme::plugin::{
 | 
			
		||||
    DnsPlugin, DnsPluginCore, DnsPluginCoreUpdater, PLUGIN_ID_SCHEMA,
 | 
			
		||||
};
 | 
			
		||||
use crate::config::acme::{AccountName, KnownAcmeDirectory};
 | 
			
		||||
use crate::server::WorkerTask;
 | 
			
		||||
use crate::tools::ControlFlow;
 | 
			
		||||
 | 
			
		||||
pub(crate) const ROUTER: Router = Router::new()
 | 
			
		||||
    .get(&list_subdirs_api_method!(SUBDIRS))
 | 
			
		||||
    .subdirs(SUBDIRS);
 | 
			
		||||
 | 
			
		||||
const SUBDIRS: SubdirMap = &[
 | 
			
		||||
    (
 | 
			
		||||
        "account",
 | 
			
		||||
        &Router::new()
 | 
			
		||||
            .get(&API_METHOD_LIST_ACCOUNTS)
 | 
			
		||||
            .post(&API_METHOD_REGISTER_ACCOUNT)
 | 
			
		||||
            .match_all("name", &ACCOUNT_ITEM_ROUTER),
 | 
			
		||||
    ),
 | 
			
		||||
    (
 | 
			
		||||
        "challenge-schema",
 | 
			
		||||
        &Router::new().get(&API_METHOD_GET_CHALLENGE_SCHEMA),
 | 
			
		||||
    ),
 | 
			
		||||
    (
 | 
			
		||||
        "directories",
 | 
			
		||||
        &Router::new().get(&API_METHOD_GET_DIRECTORIES),
 | 
			
		||||
    ),
 | 
			
		||||
    (
 | 
			
		||||
        "plugins",
 | 
			
		||||
        &Router::new()
 | 
			
		||||
            .get(&API_METHOD_LIST_PLUGINS)
 | 
			
		||||
            .post(&API_METHOD_ADD_PLUGIN)
 | 
			
		||||
            .match_all("id", &PLUGIN_ITEM_ROUTER),
 | 
			
		||||
    ),
 | 
			
		||||
    ("tos", &Router::new().get(&API_METHOD_GET_TOS)),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const ACCOUNT_ITEM_ROUTER: Router = Router::new()
 | 
			
		||||
    .get(&API_METHOD_GET_ACCOUNT)
 | 
			
		||||
    .put(&API_METHOD_UPDATE_ACCOUNT)
 | 
			
		||||
    .delete(&API_METHOD_DEACTIVATE_ACCOUNT);
 | 
			
		||||
 | 
			
		||||
const PLUGIN_ITEM_ROUTER: Router = Router::new()
 | 
			
		||||
    .get(&API_METHOD_GET_PLUGIN)
 | 
			
		||||
    .put(&API_METHOD_UPDATE_PLUGIN)
 | 
			
		||||
    .delete(&API_METHOD_DELETE_PLUGIN);
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    properties: {
 | 
			
		||||
        name: { type: AccountName },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// An ACME Account entry.
 | 
			
		||||
///
 | 
			
		||||
/// Currently only contains a 'name' property.
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
pub struct AccountEntry {
 | 
			
		||||
    name: AccountName,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    returns: {
 | 
			
		||||
        type: Array,
 | 
			
		||||
        items: { type: AccountEntry },
 | 
			
		||||
        description: "List of ACME accounts.",
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// List ACME accounts.
 | 
			
		||||
pub fn list_accounts() -> Result<Vec<AccountEntry>, Error> {
 | 
			
		||||
    let mut entries = Vec::new();
 | 
			
		||||
    crate::config::acme::foreach_acme_account(|name| {
 | 
			
		||||
        entries.push(AccountEntry { name });
 | 
			
		||||
        ControlFlow::Continue(())
 | 
			
		||||
    })?;
 | 
			
		||||
    Ok(entries)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    properties: {
 | 
			
		||||
        account: { type: Object, properties: {}, additional_properties: true },
 | 
			
		||||
        tos: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            optional: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// ACME Account information.
 | 
			
		||||
///
 | 
			
		||||
/// This is what we return via the API.
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
pub struct AccountInfo {
 | 
			
		||||
    /// Raw account data.
 | 
			
		||||
    account: AcmeAccountData,
 | 
			
		||||
 | 
			
		||||
    /// The ACME directory URL the account was created at.
 | 
			
		||||
    directory: String,
 | 
			
		||||
 | 
			
		||||
    /// The account's own URL within the ACME directory.
 | 
			
		||||
    location: String,
 | 
			
		||||
 | 
			
		||||
    /// The ToS URL, if the user agreed to one.
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    tos: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            name: { type: AccountName },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    returns: { type: AccountInfo },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Return existing ACME account information.
 | 
			
		||||
pub async fn get_account(name: AccountName) -> Result<AccountInfo, Error> {
 | 
			
		||||
    let client = AcmeClient::load(&name).await?;
 | 
			
		||||
    let account = client.account()?;
 | 
			
		||||
    Ok(AccountInfo {
 | 
			
		||||
        location: account.location.clone(),
 | 
			
		||||
        tos: client.tos().map(str::to_owned),
 | 
			
		||||
        directory: client.directory_url().to_owned(),
 | 
			
		||||
        account: AcmeAccountData {
 | 
			
		||||
            only_return_existing: false, // don't actually write this out in case it's set
 | 
			
		||||
            ..account.data.clone()
 | 
			
		||||
        },
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn account_contact_from_string(s: &str) -> Vec<String> {
 | 
			
		||||
    s.split(&[' ', ';', ',', '\0'][..])
 | 
			
		||||
        .map(|s| format!("mailto:{}", s))
 | 
			
		||||
        .collect()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            name: {
 | 
			
		||||
                type: AccountName,
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            contact: {
 | 
			
		||||
                description: "List of email addresses.",
 | 
			
		||||
            },
 | 
			
		||||
            tos_url: {
 | 
			
		||||
                description: "URL of CA TermsOfService - setting this indicates agreement.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            directory: {
 | 
			
		||||
                type: String,
 | 
			
		||||
                description: "The ACME Directory.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Register an ACME account.
 | 
			
		||||
fn register_account(
 | 
			
		||||
    name: Option<AccountName>,
 | 
			
		||||
    // Todo: email & email-list schema
 | 
			
		||||
    contact: String,
 | 
			
		||||
    tos_url: Option<String>,
 | 
			
		||||
    directory: Option<String>,
 | 
			
		||||
    rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<String, Error> {
 | 
			
		||||
    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
 | 
			
		||||
 | 
			
		||||
    let name = name
 | 
			
		||||
        .unwrap_or_else(|| unsafe { AccountName::from_string_unchecked("default".to_string()) });
 | 
			
		||||
 | 
			
		||||
    if Path::new(&crate::config::acme::account_path(&name)).exists() {
 | 
			
		||||
        http_bail!(BAD_REQUEST, "account {:?} already exists", name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let directory = directory.unwrap_or_else(|| {
 | 
			
		||||
        crate::config::acme::DEFAULT_ACME_DIRECTORY_ENTRY
 | 
			
		||||
            .url
 | 
			
		||||
            .to_owned()
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    WorkerTask::spawn(
 | 
			
		||||
        "acme-register",
 | 
			
		||||
        None,
 | 
			
		||||
        auth_id,
 | 
			
		||||
        true,
 | 
			
		||||
        move |worker| async move {
 | 
			
		||||
            let mut client = AcmeClient::new(directory);
 | 
			
		||||
 | 
			
		||||
            worker.log("Registering ACME account...");
 | 
			
		||||
 | 
			
		||||
            let account =
 | 
			
		||||
                do_register_account(&mut client, &name, tos_url.is_some(), contact, None).await?;
 | 
			
		||||
 | 
			
		||||
            worker.log(format!(
 | 
			
		||||
                "Registration successful, account URL: {}",
 | 
			
		||||
                account.location
 | 
			
		||||
            ));
 | 
			
		||||
 | 
			
		||||
            Ok(())
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn do_register_account<'a>(
 | 
			
		||||
    client: &'a mut AcmeClient,
 | 
			
		||||
    name: &AccountName,
 | 
			
		||||
    agree_to_tos: bool,
 | 
			
		||||
    contact: String,
 | 
			
		||||
    rsa_bits: Option<u32>,
 | 
			
		||||
) -> Result<&'a Account, Error> {
 | 
			
		||||
    let contact = account_contact_from_string(&contact);
 | 
			
		||||
    Ok(client
 | 
			
		||||
        .new_account(name, agree_to_tos, contact, rsa_bits)
 | 
			
		||||
        .await?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            name: { type: AccountName },
 | 
			
		||||
            contact: {
 | 
			
		||||
                description: "List of email addresses.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Update an ACME account.
 | 
			
		||||
pub fn update_account(
 | 
			
		||||
    name: AccountName,
 | 
			
		||||
    // Todo: email & email-list schema
 | 
			
		||||
    contact: Option<String>,
 | 
			
		||||
    rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<String, Error> {
 | 
			
		||||
    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
 | 
			
		||||
 | 
			
		||||
    WorkerTask::spawn(
 | 
			
		||||
        "acme-update",
 | 
			
		||||
        None,
 | 
			
		||||
        auth_id,
 | 
			
		||||
        true,
 | 
			
		||||
        move |_worker| async move {
 | 
			
		||||
            let data = match contact {
 | 
			
		||||
                Some(data) => json!({
 | 
			
		||||
                    "contact": account_contact_from_string(&data),
 | 
			
		||||
                }),
 | 
			
		||||
                None => json!({}),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            AcmeClient::load(&name).await?.update_account(&data).await?;
 | 
			
		||||
 | 
			
		||||
            Ok(())
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            name: { type: AccountName },
 | 
			
		||||
            force: {
 | 
			
		||||
                description:
 | 
			
		||||
                    "Delete account data even if the server refuses to deactivate the account.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Deactivate an ACME account.
 | 
			
		||||
pub fn deactivate_account(
 | 
			
		||||
    name: AccountName,
 | 
			
		||||
    force: bool,
 | 
			
		||||
    rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<String, Error> {
 | 
			
		||||
    let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
 | 
			
		||||
 | 
			
		||||
    WorkerTask::spawn(
 | 
			
		||||
        "acme-deactivate",
 | 
			
		||||
        None,
 | 
			
		||||
        auth_id,
 | 
			
		||||
        true,
 | 
			
		||||
        move |worker| async move {
 | 
			
		||||
            match AcmeClient::load(&name)
 | 
			
		||||
                .await?
 | 
			
		||||
                .update_account(&json!({"status": "deactivated"}))
 | 
			
		||||
                .await
 | 
			
		||||
            {
 | 
			
		||||
                Ok(_account) => (),
 | 
			
		||||
                Err(err) if !force => return Err(err),
 | 
			
		||||
                Err(err) => {
 | 
			
		||||
                    worker.warn(format!(
 | 
			
		||||
                        "error deactivating account {:?}, proceedeing anyway - {}",
 | 
			
		||||
                        name, err,
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            crate::config::acme::mark_account_deactivated(&name)?;
 | 
			
		||||
            Ok(())
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            directory: {
 | 
			
		||||
                type: String,
 | 
			
		||||
                description: "The ACME Directory.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Anybody,
 | 
			
		||||
    },
 | 
			
		||||
    returns: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        optional: true,
 | 
			
		||||
        description: "The ACME Directory's ToS URL, if any.",
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// Get the Terms of Service URL for an ACME directory.
 | 
			
		||||
async fn get_tos(directory: Option<String>) -> Result<Option<String>, Error> {
 | 
			
		||||
    let directory = directory.unwrap_or_else(|| {
 | 
			
		||||
        crate::config::acme::DEFAULT_ACME_DIRECTORY_ENTRY
 | 
			
		||||
            .url
 | 
			
		||||
            .to_owned()
 | 
			
		||||
    });
 | 
			
		||||
    Ok(AcmeClient::new(directory)
 | 
			
		||||
        .terms_of_service_url()
 | 
			
		||||
        .await?
 | 
			
		||||
        .map(str::to_owned))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Anybody,
 | 
			
		||||
    },
 | 
			
		||||
    returns: {
 | 
			
		||||
        description: "List of known ACME directories.",
 | 
			
		||||
        type: Array,
 | 
			
		||||
        items: { type: KnownAcmeDirectory },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// Get named known ACME directory endpoints.
 | 
			
		||||
fn get_directories() -> Result<&'static [KnownAcmeDirectory], Error> {
 | 
			
		||||
    Ok(crate::config::acme::KNOWN_ACME_DIRECTORIES)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    properties: {
 | 
			
		||||
        schema: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            additional_properties: true,
 | 
			
		||||
            properties: {},
 | 
			
		||||
        },
 | 
			
		||||
        type: {
 | 
			
		||||
            type: String,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
/// Schema for an ACME challenge plugin.
 | 
			
		||||
pub struct ChallengeSchema {
 | 
			
		||||
    /// Plugin ID.
 | 
			
		||||
    id: String,
 | 
			
		||||
 | 
			
		||||
    /// Human readable name, falls back to id.
 | 
			
		||||
    name: String,
 | 
			
		||||
 | 
			
		||||
    /// Plugin Type.
 | 
			
		||||
    #[serde(rename = "type")]
 | 
			
		||||
    ty: &'static str,
 | 
			
		||||
 | 
			
		||||
    /// The plugin's parameter schema.
 | 
			
		||||
    schema: Value,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Anybody,
 | 
			
		||||
    },
 | 
			
		||||
    returns: {
 | 
			
		||||
        description: "ACME Challenge Plugin Shema.",
 | 
			
		||||
        type: Array,
 | 
			
		||||
        items: { type: ChallengeSchema },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// Get named known ACME directory endpoints.
 | 
			
		||||
fn get_challenge_schema() -> Result<Vec<ChallengeSchema>, Error> {
 | 
			
		||||
    let mut out = Vec::new();
 | 
			
		||||
    crate::config::acme::foreach_dns_plugin(|id| {
 | 
			
		||||
        out.push(ChallengeSchema {
 | 
			
		||||
            id: id.to_owned(),
 | 
			
		||||
            name: id.to_owned(),
 | 
			
		||||
            ty: "dns",
 | 
			
		||||
            schema: Value::Object(Default::default()),
 | 
			
		||||
        });
 | 
			
		||||
        ControlFlow::Continue(())
 | 
			
		||||
    })?;
 | 
			
		||||
    Ok(out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api]
 | 
			
		||||
#[derive(Default, Deserialize, Serialize)]
 | 
			
		||||
#[serde(rename_all = "kebab-case")]
 | 
			
		||||
/// The API's format is inherited from PVE/PMG:
 | 
			
		||||
pub struct PluginConfig {
 | 
			
		||||
    /// Plugin ID.
 | 
			
		||||
    plugin: String,
 | 
			
		||||
 | 
			
		||||
    /// Plugin type.
 | 
			
		||||
    #[serde(rename = "type")]
 | 
			
		||||
    ty: String,
 | 
			
		||||
 | 
			
		||||
    /// DNS Api name.
 | 
			
		||||
    api: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// Plugin configuration data.
 | 
			
		||||
    data: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// Extra delay in seconds to wait before requesting validation.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Allows to cope with long TTL of DNS records.
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none", default)]
 | 
			
		||||
    validation_delay: Option<u32>,
 | 
			
		||||
 | 
			
		||||
    /// Flag to disable the config.
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none", default)]
 | 
			
		||||
    disable: Option<bool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See PMG/PVE's $modify_cfg_for_api sub
 | 
			
		||||
fn modify_cfg_for_api(id: &str, ty: &str, data: &Value) -> PluginConfig {
 | 
			
		||||
    let mut entry = data.clone();
 | 
			
		||||
 | 
			
		||||
    let obj = entry.as_object_mut().unwrap();
 | 
			
		||||
    obj.remove("id");
 | 
			
		||||
    obj.insert("plugin".to_string(), Value::String(id.to_owned()));
 | 
			
		||||
    obj.insert("type".to_string(), Value::String(ty.to_owned()));
 | 
			
		||||
 | 
			
		||||
    // FIXME: This needs to go once the `Updater` is fixed.
 | 
			
		||||
    // None of these should be able to fail unless the user changed the files by hand, in which
 | 
			
		||||
    // case we leave the unmodified string in the Value for now. This will be handled with an error
 | 
			
		||||
    // later.
 | 
			
		||||
    if let Some(Value::String(ref mut data)) = obj.get_mut("data") {
 | 
			
		||||
        if let Ok(new) = base64::decode_config(&data, base64::URL_SAFE_NO_PAD) {
 | 
			
		||||
            if let Ok(utf8) = String::from_utf8(new) {
 | 
			
		||||
                *data = utf8;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // PVE/PMG do this explicitly for ACME plugins...
 | 
			
		||||
    // obj.insert("digest".to_string(), Value::String(digest.clone()));
 | 
			
		||||
 | 
			
		||||
    serde_json::from_value(entry).unwrap_or_else(|_| PluginConfig {
 | 
			
		||||
        plugin: "*Error*".to_string(),
 | 
			
		||||
        ty: "*Error*".to_string(),
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
    returns: {
 | 
			
		||||
        type: Array,
 | 
			
		||||
        description: "List of ACME plugin configurations.",
 | 
			
		||||
        items: { type: PluginConfig },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// List ACME challenge plugins.
 | 
			
		||||
pub fn list_plugins(mut rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<PluginConfig>, Error> {
 | 
			
		||||
    use crate::config::acme::plugin;
 | 
			
		||||
 | 
			
		||||
    let (plugins, digest) = plugin::config()?;
 | 
			
		||||
    rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
 | 
			
		||||
    Ok(plugins
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|(id, (ty, data))| modify_cfg_for_api(&id, &ty, data))
 | 
			
		||||
        .collect())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            id: { schema: PLUGIN_ID_SCHEMA },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
    returns: { type: PluginConfig },
 | 
			
		||||
)]
 | 
			
		||||
/// List ACME challenge plugins.
 | 
			
		||||
pub fn get_plugin(id: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result<PluginConfig, Error> {
 | 
			
		||||
    use crate::config::acme::plugin;
 | 
			
		||||
 | 
			
		||||
    let (plugins, digest) = plugin::config()?;
 | 
			
		||||
    rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
 | 
			
		||||
 | 
			
		||||
    match plugins.get(&id) {
 | 
			
		||||
        Some((ty, data)) => Ok(modify_cfg_for_api(&id, &ty, &data)),
 | 
			
		||||
        None => http_bail!(NOT_FOUND, "no such plugin"),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Currently we only have "the" standalone plugin and DNS plugins so we can just flatten a
 | 
			
		||||
// DnsPluginUpdater:
 | 
			
		||||
//
 | 
			
		||||
// FIXME: The 'id' parameter should not be "optional" in the schema.
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            type: {
 | 
			
		||||
                type: String,
 | 
			
		||||
                description: "The ACME challenge plugin type.",
 | 
			
		||||
            },
 | 
			
		||||
            core: {
 | 
			
		||||
                type: DnsPluginCoreUpdater,
 | 
			
		||||
                flatten: true,
 | 
			
		||||
            },
 | 
			
		||||
            data: {
 | 
			
		||||
                type: String,
 | 
			
		||||
                // This is different in the API!
 | 
			
		||||
                description: "DNS plugin data (base64 encoded with padding).",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Add ACME plugin configuration.
 | 
			
		||||
pub fn add_plugin(r#type: String, core: DnsPluginCoreUpdater, data: String) -> Result<(), Error> {
 | 
			
		||||
    use crate::config::acme::plugin;
 | 
			
		||||
 | 
			
		||||
    // Currently we only support DNS plugins and the standalone plugin is "fixed":
 | 
			
		||||
    if r#type != "dns" {
 | 
			
		||||
        bail!("invalid ACME plugin type: {:?}", r#type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let data = String::from_utf8(base64::decode(&data)?)
 | 
			
		||||
        .map_err(|_| format_err!("data must be valid UTF-8"))?;
 | 
			
		||||
    //core.api_fixup()?;
 | 
			
		||||
 | 
			
		||||
    // FIXME: Solve the Updater with non-optional fields thing...
 | 
			
		||||
    let id = core
 | 
			
		||||
        .id
 | 
			
		||||
        .clone()
 | 
			
		||||
        .ok_or_else(|| format_err!("missing required 'id' parameter"))?;
 | 
			
		||||
 | 
			
		||||
    let _lock = plugin::lock()?;
 | 
			
		||||
 | 
			
		||||
    let (mut plugins, _digest) = plugin::config()?;
 | 
			
		||||
    if plugins.contains_key(&id) {
 | 
			
		||||
        bail!("ACME plugin ID {:?} already exists", id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let plugin = serde_json::to_value(DnsPlugin {
 | 
			
		||||
        core: DnsPluginCore::try_build_from(core)?,
 | 
			
		||||
        data,
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    plugins.insert(id, r#type, plugin);
 | 
			
		||||
 | 
			
		||||
    plugin::save_config(&plugins)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            id: { schema: PLUGIN_ID_SCHEMA },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Delete an ACME plugin configuration.
 | 
			
		||||
pub fn delete_plugin(id: String) -> Result<(), Error> {
 | 
			
		||||
    use crate::config::acme::plugin;
 | 
			
		||||
 | 
			
		||||
    let _lock = plugin::lock()?;
 | 
			
		||||
 | 
			
		||||
    let (mut plugins, _digest) = plugin::config()?;
 | 
			
		||||
    if plugins.remove(&id).is_none() {
 | 
			
		||||
        http_bail!(NOT_FOUND, "no such plugin");
 | 
			
		||||
    }
 | 
			
		||||
    plugin::save_config(&plugins)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            core_update: {
 | 
			
		||||
                type: DnsPluginCoreUpdater,
 | 
			
		||||
                flatten: true,
 | 
			
		||||
            },
 | 
			
		||||
            data: {
 | 
			
		||||
                type: String,
 | 
			
		||||
                optional: true,
 | 
			
		||||
                // This is different in the API!
 | 
			
		||||
                description: "DNS plugin data (base64 encoded with padding).",
 | 
			
		||||
            },
 | 
			
		||||
            digest: {
 | 
			
		||||
                description: "Digest to protect against concurrent updates",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            delete: {
 | 
			
		||||
                description: "Options to remove from the configuration",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    access: {
 | 
			
		||||
        permission: &Permission::Privilege(&["system", "certificates"], PRIV_SYS_MODIFY, false),
 | 
			
		||||
    },
 | 
			
		||||
    protected: true,
 | 
			
		||||
)]
 | 
			
		||||
/// Update an ACME plugin configuration.
 | 
			
		||||
pub fn update_plugin(
 | 
			
		||||
    core_update: DnsPluginCoreUpdater,
 | 
			
		||||
    data: Option<String>,
 | 
			
		||||
    delete: Option<String>,
 | 
			
		||||
    digest: Option<String>,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    use crate::config::acme::plugin;
 | 
			
		||||
 | 
			
		||||
    let data = data
 | 
			
		||||
        .as_deref()
 | 
			
		||||
        .map(base64::decode)
 | 
			
		||||
        .transpose()?
 | 
			
		||||
        .map(String::from_utf8)
 | 
			
		||||
        .transpose()
 | 
			
		||||
        .map_err(|_| format_err!("data must be valid UTF-8"))?;
 | 
			
		||||
    //core_update.api_fixup()?;
 | 
			
		||||
 | 
			
		||||
    // unwrap: the id is matched by this method's API path
 | 
			
		||||
    let id = core_update.id.clone().unwrap();
 | 
			
		||||
 | 
			
		||||
    let delete: Vec<&str> = delete
 | 
			
		||||
        .as_deref()
 | 
			
		||||
        .unwrap_or("")
 | 
			
		||||
        .split(&[' ', ',', ';', '\0'][..])
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    let _lock = plugin::lock()?;
 | 
			
		||||
 | 
			
		||||
    let (mut plugins, expected_digest) = plugin::config()?;
 | 
			
		||||
 | 
			
		||||
    if let Some(digest) = digest {
 | 
			
		||||
        let digest = proxmox::tools::hex_to_digest(&digest)?;
 | 
			
		||||
        crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match plugins.get_mut(&id) {
 | 
			
		||||
        Some((ty, ref mut entry)) => {
 | 
			
		||||
            if ty != "dns" {
 | 
			
		||||
                bail!("cannot update plugin of type {:?}", ty);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let mut plugin: DnsPlugin = serde_json::from_value(entry.clone())?;
 | 
			
		||||
            plugin.core.update_from(core_update, &delete)?;
 | 
			
		||||
            if let Some(data) = data {
 | 
			
		||||
                plugin.data = data;
 | 
			
		||||
            }
 | 
			
		||||
            *entry = serde_json::to_value(plugin)?;
 | 
			
		||||
        }
 | 
			
		||||
        None => http_bail!(NOT_FOUND, "no such plugin"),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    plugin::save_config(&plugins)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user