diff --git a/Cargo.lock b/Cargo.lock index dc5ff17ab5d..a8fcddfe471 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,6 +351,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -947,6 +953,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1184,6 +1202,20 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -1227,6 +1259,27 @@ dependencies = [ "string_cache", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1354,6 +1407,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1577,6 +1640,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1640,6 +1704,17 @@ dependencies = [ "scroll", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.7" @@ -2321,6 +2396,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "jsonwebtoken" +version = "10.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.15", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", +] + [[package]] name = "jwalk" version = "0.8.1" @@ -2977,14 +3075,15 @@ dependencies = [ [[package]] name = "objectstore-client" -version = "0.0.14" +version = "0.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260ccf94ed55dc83c0a5b470a4d9404d9f15e4b975d6c6e734e89296eef75b59" +checksum = "84e3b79fb455eaf034e809ebf57a4b1896d84c1eb457c3af319bf7fb3c4a32b8" dependencies = [ "async-compression", "bytes", "futures-util", "infer", + "jsonwebtoken", "objectstore-types", "reqwest", "sentry-core", @@ -2997,9 +3096,9 @@ dependencies = [ [[package]] name = "objectstore-types" -version = "0.0.14" +version = "0.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da8b79e6156554da409ff350f06dea1b5ea0b1f863ca753fe9222a74e917592b" +checksum = "a537dd2aad24fc95bd6dd90f986ef35d4261e02adb190807df4f6438136ad2ac" dependencies = [ "http", "humantime", @@ -3140,6 +3239,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "papaya" version = "0.2.3" @@ -3191,6 +3314,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3447,6 +3580,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "priority-queue" version = "2.7.0" @@ -4633,6 +4775,16 @@ dependencies = [ "quick-error", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -4889,6 +5041,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -5307,6 +5473,18 @@ dependencies = [ "similar", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index 6e956367744..d4dbd5ea5f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,7 @@ minidump = "0.26.0" multer = "3.1.0" num-traits = "0.2.19" num_cpus = "1.17.0" -objectstore-client = "0.0.14" +objectstore-client = "0.0.15" opentelemetry-semantic-conventions = { version = "0.31.0" } opentelemetry-proto = { version = "0.30.0", default-features = false } papaya = "0.2.3" diff --git a/pyproject.toml b/pyproject.toml index 243c0da5dc7..ba9118dfaa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev = [ "flask>=3.0.3", "msgpack>=1.1.0", "mypy>=1.10.0", - "objectstore-client==0.0.14", + "objectstore-client==0.0.15", "opentelemetry-proto>=1.32.1", "pre-commit>=4.2.0", "pytest>=7.4.3", diff --git a/relay-config/src/config.rs b/relay-config/src/config.rs index a04fed26fe0..83d1c67184a 100644 --- a/relay-config/src/config.rs +++ b/relay-config/src/config.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::error::Error; use std::io::Write; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; @@ -1268,6 +1268,25 @@ impl Default for OutcomeAggregatorConfig { } } +/// Configuration values for attachment upload auth. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default)] +pub struct UploadServiceAuthConfig { + /// JWT private key ID that Objectstore must use to load a corresponding public key. + pub kid: String, + + /// A file where the JWT private key content is located, in EdDSA PEM form. + pub key_file: String, + + /// The number of seconds that an Objectstore JWT should be valid for. + pub expiry_seconds: Option, + + /// The permissions a token signed by this client should have. + /// + /// TODO: Expose `Permission` type in `objectstore_client` + pub permissions: Option>, +} + /// Configuration values for attachment uploads. #[derive(Serialize, Deserialize, Debug)] #[serde(default)] @@ -1283,6 +1302,9 @@ pub struct UploadServiceConfig { /// Maximum duration of an attachment upload in seconds. Uploads that take longer are discarded. pub timeout: u64, + + /// Configuration values for upload auth. + pub auth: Option, } impl Default for UploadServiceConfig { @@ -1291,6 +1313,7 @@ impl Default for UploadServiceConfig { objectstore_url: None, max_concurrent_requests: 10, timeout: 60, + auth: None, } } } diff --git a/relay-server/src/services/upload.rs b/relay-server/src/services/upload.rs index ce9451a6fd0..6cb720bf19a 100644 --- a/relay-server/src/services/upload.rs +++ b/relay-server/src/services/upload.rs @@ -6,7 +6,9 @@ use bytes::Bytes; use futures::future::BoxFuture; use futures::stream::FuturesUnordered; use futures::{FutureExt, StreamExt}; -use objectstore_client::{Client, ExpirationPolicy, Session, Usecase}; +use objectstore_client::{ + Client, ExpirationPolicy, SecretKey as ObjectstoreKey, Session, TokenGenerator, Usecase, +}; use relay_config::UploadServiceConfig; use relay_quotas::DataCategory; use relay_system::{Addr, FromMessage, Interface, NoResponse, Receiver, Service}; @@ -138,12 +140,30 @@ impl UploadService { objectstore_url, max_concurrent_requests, timeout, + auth, } = config; let Some(objectstore_url) = objectstore_url else { return Ok(None); }; - let objectstore_client = Client::builder(objectstore_url).build()?; + let mut objectstore_builder = Client::builder(objectstore_url); + if let Some(auth) = auth { + let secret_key = ObjectstoreKey { + kid: auth.kid.clone(), + secret_key: std::fs::read_to_string(auth.key_file.clone())?, + }; + let mut token_generator = TokenGenerator::new(secret_key)?; + if let Some(expiry_seconds) = auth.expiry_seconds { + token_generator = token_generator.expiry_seconds(expiry_seconds); + } + // // TODO: Export `Permission` enum from `objectstore_client` + // if let Some(permissions) = auth.max_permissions { + // token_generator = token_generator.permissions(auth.permissions); + // } + objectstore_builder = objectstore_builder.token_generator(token_generator); + } + + let objectstore_client = objectstore_builder.build()?; let event_attachments = Usecase::new("attachments") .with_expiration_policy(ExpirationPolicy::TimeToLive(DEFAULT_ATTACHMENT_RETENTION)); let trace_attachments = Usecase::new("trace_attachments")