use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Serialize)]
pub struct BucketOwner {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "DisplayName")]
pub display_name: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct BucketConfig {
pub name: String,
pub backend_type: String,
pub backend_prefix: Option<String>,
pub anonymous_access: bool,
#[serde(default)]
pub allowed_roles: Vec<String>,
#[serde(default)]
pub backend_options: HashMap<String, String>,
}
const REDACTED_OPTION_KEYS: &[&str] = &[
"secret_access_key",
"access_key",
"service_account_key",
"token",
];
impl fmt::Debug for BucketConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let redacted_opts: HashMap<&str, &str> = self
.backend_options
.iter()
.map(|(k, v)| {
let val = if REDACTED_OPTION_KEYS.contains(&k.as_str()) {
"[REDACTED]"
} else {
v.as_str()
};
(k.as_str(), val)
})
.collect();
f.debug_struct("BucketConfig")
.field("name", &self.name)
.field("backend_type", &self.backend_type)
.field("backend_prefix", &self.backend_prefix)
.field("anonymous_access", &self.anonymous_access)
.field("allowed_roles", &self.allowed_roles)
.field("backend_options", &redacted_opts)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendType {
S3,
Azure,
Gcs,
}
impl BucketConfig {
pub fn parsed_backend_type(&self) -> Option<BackendType> {
match self.backend_type.as_str() {
"s3" => Some(BackendType::S3),
"az" | "azure" => Some(BackendType::Azure),
"gcs" | "gs" => Some(BackendType::Gcs),
_ => None,
}
}
pub fn supports_s3_multipart(&self) -> bool {
matches!(self.parsed_backend_type(), Some(BackendType::S3))
}
pub fn option(&self, key: &str) -> Option<&str> {
self.backend_options.get(key).map(|s| s.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleConfig {
pub role_id: String,
pub name: String,
#[serde(default)]
pub trusted_oidc_issuers: Vec<String>,
pub required_audience: Option<String>,
#[serde(default)]
pub subject_conditions: Vec<String>,
#[serde(default)]
pub allowed_scopes: Vec<AccessScope>,
pub max_session_duration_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessScope {
pub bucket: String,
pub prefixes: Vec<String>,
pub actions: Vec<Action>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Action {
GetObject,
HeadObject,
PutObject,
ListBucket,
CreateMultipartUpload,
UploadPart,
CompleteMultipartUpload,
AbortMultipartUpload,
DeleteObject,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct StoredCredential {
pub access_key_id: String,
pub secret_access_key: String,
pub principal_name: String,
pub allowed_scopes: Vec<AccessScope>,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub enabled: bool,
}
impl fmt::Debug for StoredCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoredCredential")
.field("access_key_id", &self.access_key_id)
.field("secret_access_key", &"[REDACTED]")
.field("principal_name", &self.principal_name)
.field("allowed_scopes", &self.allowed_scopes)
.field("created_at", &self.created_at)
.field("expires_at", &self.expires_at)
.field("enabled", &self.enabled)
.finish()
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct TemporaryCredentials {
pub access_key_id: String,
pub secret_access_key: String,
pub session_token: String,
pub expiration: DateTime<Utc>,
pub allowed_scopes: Vec<AccessScope>,
pub assumed_role_id: String,
pub source_identity: String,
}
impl fmt::Debug for TemporaryCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TemporaryCredentials")
.field("access_key_id", &self.access_key_id)
.field("secret_access_key", &"[REDACTED]")
.field("session_token", &"[REDACTED]")
.field("expiration", &self.expiration)
.field("allowed_scopes", &self.allowed_scopes)
.field("assumed_role_id", &self.assumed_role_id)
.field("source_identity", &self.source_identity)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct AuthenticatedIdentity {
pub principal_name: String,
pub allowed_scopes: Vec<AccessScope>,
}
#[derive(Debug, Clone)]
pub enum ResolvedIdentity {
Anonymous,
Authenticated(AuthenticatedIdentity),
}
#[derive(Debug, Clone)]
pub enum S3Operation {
GetObject {
bucket: String,
key: String,
},
HeadObject {
bucket: String,
key: String,
},
PutObject {
bucket: String,
key: String,
},
CreateMultipartUpload {
bucket: String,
key: String,
},
UploadPart {
bucket: String,
key: String,
upload_id: String,
part_number: u32,
},
CompleteMultipartUpload {
bucket: String,
key: String,
upload_id: String,
},
AbortMultipartUpload {
bucket: String,
key: String,
upload_id: String,
},
DeleteObject {
bucket: String,
key: String,
},
ListBucket {
bucket: String,
raw_query: Option<String>,
},
ListBuckets,
}
impl S3Operation {
pub fn method(&self) -> http::Method {
match self {
S3Operation::GetObject { .. }
| S3Operation::ListBucket { .. }
| S3Operation::ListBuckets => http::Method::GET,
S3Operation::HeadObject { .. } => http::Method::HEAD,
S3Operation::PutObject { .. } | S3Operation::UploadPart { .. } => http::Method::PUT,
S3Operation::DeleteObject { .. } | S3Operation::AbortMultipartUpload { .. } => {
http::Method::DELETE
}
S3Operation::CreateMultipartUpload { .. }
| S3Operation::CompleteMultipartUpload { .. } => http::Method::POST,
}
}
pub fn action(&self) -> Action {
match self {
S3Operation::GetObject { .. } => Action::GetObject,
S3Operation::HeadObject { .. } => Action::HeadObject,
S3Operation::PutObject { .. } => Action::PutObject,
S3Operation::ListBucket { .. } => Action::ListBucket,
S3Operation::CreateMultipartUpload { .. } => Action::CreateMultipartUpload,
S3Operation::UploadPart { .. } => Action::UploadPart,
S3Operation::CompleteMultipartUpload { .. } => Action::CompleteMultipartUpload,
S3Operation::AbortMultipartUpload { .. } => Action::AbortMultipartUpload,
S3Operation::DeleteObject { .. } => Action::DeleteObject,
S3Operation::ListBuckets => Action::ListBucket,
}
}
pub fn bucket(&self) -> Option<&str> {
match self {
S3Operation::GetObject { bucket, .. }
| S3Operation::HeadObject { bucket, .. }
| S3Operation::PutObject { bucket, .. }
| S3Operation::ListBucket { bucket, .. }
| S3Operation::CreateMultipartUpload { bucket, .. }
| S3Operation::UploadPart { bucket, .. }
| S3Operation::CompleteMultipartUpload { bucket, .. }
| S3Operation::AbortMultipartUpload { bucket, .. }
| S3Operation::DeleteObject { bucket, .. } => Some(bucket),
S3Operation::ListBuckets => None,
}
}
pub fn key(&self) -> &str {
match self {
S3Operation::GetObject { key, .. }
| S3Operation::HeadObject { key, .. }
| S3Operation::PutObject { key, .. }
| S3Operation::CreateMultipartUpload { key, .. }
| S3Operation::UploadPart { key, .. }
| S3Operation::CompleteMultipartUpload { key, .. }
| S3Operation::AbortMultipartUpload { key, .. }
| S3Operation::DeleteObject { key, .. } => key,
S3Operation::ListBucket { .. } | S3Operation::ListBuckets => "",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action() {
let op = S3Operation::GetObject {
bucket: "b".into(),
key: "k".into(),
};
assert_eq!(op.action(), Action::GetObject);
let op = S3Operation::PutObject {
bucket: "b".into(),
key: "k".into(),
};
assert_eq!(op.action(), Action::PutObject);
let op = S3Operation::ListBucket {
bucket: "b".into(),
raw_query: None,
};
assert_eq!(op.action(), Action::ListBucket);
assert_eq!(S3Operation::ListBuckets.action(), Action::ListBucket);
let op = S3Operation::DeleteObject {
bucket: "b".into(),
key: "k".into(),
};
assert_eq!(op.action(), Action::DeleteObject);
}
#[test]
fn test_bucket() {
let op = S3Operation::GetObject {
bucket: "my-bucket".into(),
key: "k".into(),
};
assert_eq!(op.bucket(), Some("my-bucket"));
assert_eq!(S3Operation::ListBuckets.bucket(), None);
}
#[test]
fn test_key() {
let op = S3Operation::GetObject {
bucket: "b".into(),
key: "my/key.txt".into(),
};
assert_eq!(op.key(), "my/key.txt");
let op = S3Operation::ListBucket {
bucket: "b".into(),
raw_query: Some("prefix=foo/".into()),
};
assert_eq!(op.key(), "");
assert_eq!(S3Operation::ListBuckets.key(), "");
}
}