From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <pbs-devel-bounces@lists.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9])
	by lore.proxmox.com (Postfix) with ESMTPS id 76AD71FF183
	for <inbox@lore.proxmox.com>; Mon, 19 May 2025 13:47:10 +0200 (CEST)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
	by firstgate.proxmox.com (Proxmox) with ESMTP id 303A1828A;
	Mon, 19 May 2025 13:47:06 +0200 (CEST)
From: Christian Ebner <c.ebner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Date: Mon, 19 May 2025 13:46:02 +0200
Message-Id: <20250519114640.303640-2-c.ebner@proxmox.com>
X-Mailer: git-send-email 2.39.5
In-Reply-To: <20250519114640.303640-1-c.ebner@proxmox.com>
References: <20250519114640.303640-1-c.ebner@proxmox.com>
MIME-Version: 1.0
X-SPAM-LEVEL: Spam detection results:  0
 AWL 0.028 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DMARC_MISSING             0.1 Missing DMARC policy
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to
 Validity was blocked. See
 https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more
 information.
 RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to
 Validity was blocked. See
 https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more
 information.
 RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to
 Validity was blocked. See
 https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more
 information.
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See
 http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more
 information. [s3.rs, lib.rs]
Subject: [pbs-devel] [RFC proxmox 1/2] pbs-api-types: add types for S3
 client configs and secrets
X-BeenThere: pbs-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox Backup Server development discussion
 <pbs-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/>
List-Post: <mailto:pbs-devel@lists.proxmox.com>
List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe>
Reply-To: Proxmox Backup Server development discussion
 <pbs-devel@lists.proxmox.com>
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Errors-To: pbs-devel-bounces@lists.proxmox.com
Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com>

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@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



_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel