remote config: derive and use Updater

Defined a new struct RemoteConfig (without name and password). This makes it
possible to bas64-encode the pasword in the config, but still allow plain
passwords with the API.
This commit is contained in:
Dietmar Maurer 2021-08-12 09:27:55 +02:00
parent e351ac786d
commit 97dfc62f0d
3 changed files with 79 additions and 86 deletions

View File

@ -63,29 +63,14 @@ pub fn list_remotes(
name: {
schema: REMOTE_ID_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
host: {
schema: DNS_NAME_OR_IP_SCHEMA,
},
port: {
description: "The (optional) port.",
type: u16,
optional: true,
default: 8007,
},
"auth-id": {
type: Authid,
config: {
type: remote::RemoteConfig,
flatten: true,
},
password: {
// We expect the plain password here (not base64 encoded)
schema: remote::REMOTE_PASSWORD_SCHEMA,
},
fingerprint: {
optional: true,
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
},
},
},
access: {
@ -93,23 +78,25 @@ pub fn list_remotes(
},
)]
/// Create new remote.
pub fn create_remote(password: String, param: Value) -> Result<(), Error> {
pub fn create_remote(
name: String,
config: remote::RemoteConfig,
password: String,
) -> Result<(), Error> {
let _lock = open_backup_lockfile(remote::REMOTE_CFG_LOCKFILE, None, true)?;
let mut data = param;
data["password"] = Value::from(base64::encode(password.as_bytes()));
let remote: remote::Remote = serde_json::from_value(data)?;
let (mut section_config, _digest) = remote::config()?;
let (mut config, _digest) = remote::config()?;
if config.sections.get(&remote.name).is_some() {
bail!("remote '{}' already exists.", remote.name);
if section_config.sections.get(&name).is_some() {
bail!("remote '{}' already exists.", name);
}
config.set_data(&remote.name, "remote", &remote)?;
let remote = remote::Remote { name: name.clone(), config, password };
remote::save_config(&config)?;
section_config.set_data(&name, "remote", &remote)?;
remote::save_config(&section_config)?;
Ok(())
}
@ -160,31 +147,15 @@ pub enum DeletableProperty {
name: {
schema: REMOTE_ID_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
host: {
optional: true,
schema: DNS_NAME_OR_IP_SCHEMA,
},
port: {
description: "The (optional) port.",
type: u16,
optional: true,
},
"auth-id": {
optional: true,
type: Authid,
update: {
type: remote::RemoteConfigUpdater,
flatten: true,
},
password: {
// We expect the plain password here (not base64 encoded)
optional: true,
schema: remote::REMOTE_PASSWORD_SCHEMA,
},
fingerprint: {
optional: true,
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
},
delete: {
description: "List of properties to delete.",
type: Array,
@ -204,15 +175,10 @@ pub enum DeletableProperty {
},
)]
/// Update remote configuration.
#[allow(clippy::too_many_arguments)]
pub fn update_remote(
name: String,
comment: Option<String>,
host: Option<String>,
port: Option<u16>,
auth_id: Option<Authid>,
update: remote::RemoteConfigUpdater,
password: Option<String>,
fingerprint: Option<String>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
) -> Result<(), Error> {
@ -231,27 +197,27 @@ pub fn update_remote(
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableProperty::comment => { data.comment = None; },
DeletableProperty::fingerprint => { data.fingerprint = None; },
DeletableProperty::port => { data.port = None; },
DeletableProperty::comment => { data.config.comment = None; },
DeletableProperty::fingerprint => { data.config.fingerprint = None; },
DeletableProperty::port => { data.config.port = None; },
}
}
}
if let Some(comment) = comment {
if let Some(comment) = update.comment {
let comment = comment.trim().to_string();
if comment.is_empty() {
data.comment = None;
data.config.comment = None;
} else {
data.comment = Some(comment);
data.config.comment = Some(comment);
}
}
if let Some(host) = host { data.host = host; }
if port.is_some() { data.port = port; }
if let Some(auth_id) = auth_id { data.auth_id = auth_id; }
if let Some(host) = update.host { data.config.host = host; }
if update.port.is_some() { data.config.port = update.port; }
if let Some(auth_id) = update.auth_id { data.config.auth_id = auth_id; }
if let Some(password) = password { data.password = password; }
if let Some(fingerprint) = fingerprint { data.fingerprint = Some(fingerprint); }
if update.fingerprint.is_some() { data.config.fingerprint = update.fingerprint; }
config.set_data(&name, "remote", &data)?;
@ -312,16 +278,16 @@ pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error>
/// Helper to get client for remote.cfg entry
pub async fn remote_client(remote: remote::Remote) -> Result<HttpClient, Error> {
let options = HttpClientOptions::new_non_interactive(remote.password.clone(), remote.fingerprint.clone());
let options = HttpClientOptions::new_non_interactive(remote.password.clone(), remote.config.fingerprint.clone());
let client = HttpClient::new(
&remote.host,
remote.port.unwrap_or(8007),
&remote.auth_id,
&remote.config.host,
remote.config.port.unwrap_or(8007),
&remote.config.auth_id,
options)?;
let _auth_info = client.login() // make sure we can auth
.await
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.config.host, err))?;
Ok(client)
}

View File

@ -53,7 +53,12 @@ pub async fn get_pull_parameters(
let (remote_config, _digest) = remote::config()?;
let remote: remote::Remote = remote_config.lookup("remote", remote)?;
let src_repo = BackupRepository::new(Some(remote.auth_id.clone()), Some(remote.host.clone()), remote.port, remote_store.to_string());
let src_repo = BackupRepository::new(
Some(remote.config.auth_id.clone()),
Some(remote.config.host.clone()),
remote.config.port,
remote_store.to_string(),
);
let client = crate::api2::config::remote::remote_client(remote).await?;

View File

@ -25,11 +25,14 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
.max_length(1024)
.schema();
pub const REMOTE_PASSWORD_BASE64_SCHEMA: Schema = StringSchema::new("Password or auth token for remote host (stored as base64 string).")
.format(&PASSWORD_FORMAT)
.min_length(1)
.max_length(1024)
.schema();
#[api(
properties: {
name: {
schema: REMOTE_ID_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
@ -45,36 +48,55 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
"auth-id": {
type: Authid,
},
password: {
schema: REMOTE_PASSWORD_SCHEMA,
},
fingerprint: {
optional: true,
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
},
}
},
)]
#[derive(Serialize,Deserialize)]
#[derive(Serialize,Deserialize,Updater)]
#[serde(rename_all = "kebab-case")]
/// Remote properties.
pub struct Remote {
pub name: String,
/// Remote configuration properties.
pub struct RemoteConfig {
#[serde(skip_serializing_if="Option::is_none")]
pub comment: Option<String>,
pub host: String,
#[serde(skip_serializing_if="Option::is_none")]
pub port: Option<u16>,
pub auth_id: Authid,
#[serde(skip_serializing_if="String::is_empty")]
#[serde(with = "proxmox::tools::serde::string_as_base64")]
pub password: String,
#[serde(skip_serializing_if="Option::is_none")]
pub fingerprint: Option<String>,
}
#[api(
properties: {
name: {
schema: REMOTE_ID_SCHEMA,
},
config: {
type: RemoteConfig,
},
password: {
schema: REMOTE_PASSWORD_BASE64_SCHEMA,
},
},
)]
#[derive(Serialize,Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Remote properties.
pub struct Remote {
pub name: String,
// Note: The stored password is base64 encoded
#[serde(skip_serializing_if="String::is_empty")]
#[serde(with = "proxmox::tools::serde::string_as_base64")]
pub password: String,
#[serde(flatten)]
pub config: RemoteConfig,
}
fn init() -> SectionConfig {
let obj_schema = match Remote::API_SCHEMA {
Schema::Object(ref obj_schema) => obj_schema,
Schema::AllOf(ref allof_schema) => allof_schema,
_ => unreachable!(),
};