[pbs-devel] [RFC proxmox 1/2] pbs-api-types: add types for S3 client configs and secrets

Christian Ebner c.ebner at proxmox.com
Mon May 19 13:46:02 CEST 2025


Adds the new config types `S3ClientConfig` and `S3ClientSecret` to
configure datastore backends using an S3 compatible object store.

Secrets are stored as different config to never be returned on api
calls, only allowing to set/update the values.

Use a different name (`secrets_id`) for the unique identifier in case
of the secrets type, although the same id should be used for storing
and lookup. By this, clashing of property names when using flattened
types as api parameters is avoided.

Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
---
 pbs-api-types/src/lib.rs |   3 +
 pbs-api-types/src/s3.rs  | 138 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+)
 create mode 100644 pbs-api-types/src/s3.rs

diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs
index 99ec7961..7a5ea11d 100644
--- a/pbs-api-types/src/lib.rs
+++ b/pbs-api-types/src/lib.rs
@@ -147,6 +147,9 @@ pub use remote::*;
 mod pathpatterns;
 pub use pathpatterns::*;
 
+mod s3;
+pub use s3::*;
+
 mod tape;
 pub use tape::*;
 
diff --git a/pbs-api-types/src/s3.rs b/pbs-api-types/src/s3.rs
new file mode 100644
index 00000000..40c502ba
--- /dev/null
+++ b/pbs-api-types/src/s3.rs
@@ -0,0 +1,138 @@
+use anyhow::bail;
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::api_types::{
+    CERT_FINGERPRINT_SHA256_SCHEMA, DNS_NAME_OR_IP_SCHEMA, SAFE_ID_FORMAT,
+};
+use proxmox_schema::{api, const_regex, ApiStringFormat, Schema, StringSchema, Updater};
+
+#[rustfmt::skip]
+pub const S3_BUCKET_NAME_REGEX_STR: &str = r"^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$";
+
+const_regex! {
+    /// Regex to match S3 bucket names.
+    ///
+    /// Be as strict as possible following the rules as described here:
+    /// https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html#general-purpose-bucket-names
+    pub S3_BUCKET_NAME_REGEX = r"^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$";
+    /// Regex to match S3 regions.
+    pub S3_REGION_REGEX = r"^[a-z]{2}\-[a-z]{4,}\-[0-9]$";
+}
+
+pub const S3_REGION_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&S3_REGION_REGEX);
+
+pub const S3_CLIENT_ID_SCHEMA: Schema =
+    StringSchema::new("Unique ID to identify s3 client config.")
+        .format(&SAFE_ID_FORMAT)
+        .min_length(3)
+        .max_length(32)
+        .schema();
+
+pub const S3_REGION_SCHEMA: Schema = StringSchema::new("Region to access S3 object store.")
+    .format(&S3_REGION_FORMAT)
+    .min_length(3)
+    .max_length(32)
+    .schema();
+
+pub const S3_BUCKET_NAME_SCHEMA: Schema = StringSchema::new("Bucket name for S3 object store.")
+    .format(&ApiStringFormat::VerifyFn(|bucket_name| {
+        if !(S3_BUCKET_NAME_REGEX.regex_obj)().is_match(bucket_name) {
+            bail!("Bucket name does not match the regex pattern");
+        }
+
+        // Exclude pre- and postfixes described here:
+        // https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html#general-purpose-bucket-names
+        let forbidden_prefixes = ["xn--", "sthree-", "amzn-s3-demo-"];
+        for prefix in forbidden_prefixes {
+            if bucket_name.starts_with(prefix) {
+                bail!("Bucket name cannot start with '{prefix}'");
+            }
+        }
+
+        let forbidden_postfixes = ["--ol-s3", ".mrap", "--x-s3"];
+        for postfix in forbidden_postfixes {
+            if bucket_name.ends_with(postfix) {
+                bail!("Bucket name cannot end with '{postfix}'");
+            }
+        }
+
+        Ok(())
+    }))
+    .min_length(3)
+    .max_length(63)
+    .schema();
+
+#[api(
+    properties: {
+        id: {
+            schema: S3_CLIENT_ID_SCHEMA,
+        },
+        host: {
+            schema: DNS_NAME_OR_IP_SCHEMA,
+        },
+        bucket: {
+            schema: S3_BUCKET_NAME_SCHEMA,
+        },
+        port: {
+            type: u16,
+            description: "Port to access S3 object store.",
+            optional: true,
+        },
+        region: {
+            schema: S3_REGION_SCHEMA,
+            optional: true,
+        },
+        fingerprint: {
+            schema: CERT_FINGERPRINT_SHA256_SCHEMA,
+            optional: true,
+        },
+        "access-key": {
+            type: String,
+            description: "Access key for S3 object store.",
+        },
+    }
+)]
+#[derive(Serialize, Deserialize, Updater, Clone, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// S3 client configuration properties.
+pub struct S3ClientConfig {
+    #[updater(skip)]
+    pub id: String,
+    pub host: String,
+    pub bucket: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub port: Option<u16>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub region: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub fingerprint: Option<String>,
+    pub access_key: String,
+}
+
+impl S3ClientConfig {
+    pub fn acl_path(&self) -> Vec<&str> {
+        // Needs permissions on root path
+        Vec::new()
+    }
+}
+
+#[api(
+    properties: {
+        "secrets-id": {
+            type: String,
+            description: "Unique ID to identify s3 client secret config.",
+        },
+        "secret-key": {
+            type: String,
+            description: "Secret key for S3 object store.",
+        },
+    }
+)]
+#[derive(Serialize, Deserialize, Updater, Clone, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// S3 client secrets configuration properties.
+pub struct S3ClientSecretsConfig {
+    #[updater(skip)]
+    pub secrets_id: String,
+    pub secret_key: String,
+}
-- 
2.39.5





More information about the pbs-devel mailing list