diff --git a/codegen/src/v1/aws_conv.rs b/codegen/src/v1/aws_conv.rs index f2fd2954..171d8f48 100644 --- a/codegen/src/v1/aws_conv.rs +++ b/codegen/src/v1/aws_conv.rs @@ -291,6 +291,7 @@ fn has_unconditional_builder(name: &str) -> bool { matches!( name, "AnalyticsExportDestination" + | "CreateSessionOutput" | "InventoryDestination" | "RoutingRule" | "MetadataTableConfiguration" diff --git a/codegen/src/v1/dto.rs b/codegen/src/v1/dto.rs index ab6ce929..686f8d5b 100644 --- a/codegen/src/v1/dto.rs +++ b/codegen/src/v1/dto.rs @@ -512,7 +512,7 @@ fn collect_types_needing_serde(rust_types: &RustTypes) -> BTreeSet { types_needing_serde } -fn collect_types_needing_custom_default(rust_types: &RustTypes) -> BTreeSet { +fn collect_types_needing_custom_default(rust_types: &RustTypes, ops: &Operations) -> BTreeSet { let mut types_needing_custom_default = BTreeSet::new(); // Start with Configuration types that can't derive Default @@ -526,6 +526,15 @@ fn collect_types_needing_custom_default(rust_types: &RustTypes) -> BTreeSet) { let types_needing_serde = collect_types_needing_serde(rust_types); // Collect types that need custom Default implementations - let types_needing_custom_default = collect_types_needing_custom_default(rust_types); + let types_needing_custom_default = collect_types_needing_custom_default(rust_types, ops); g([ "#![allow(clippy::empty_structs_with_brackets)]", diff --git a/codegen/src/v1/ops.rs b/codegen/src/v1/ops.rs index 51e52fd4..e9b6a826 100644 --- a/codegen/src/v1/ops.rs +++ b/codegen/src/v1/ops.rs @@ -38,7 +38,7 @@ pub struct Operation { pub type Operations = BTreeMap; // TODO: handle these operations -pub const SKIPPED_OPS: &[&str] = &["CreateSession", "ListDirectoryBuckets"]; +pub const SKIPPED_OPS: &[&str] = &["ListDirectoryBuckets"]; pub fn collect_operations(model: &smithy::Model) -> Operations { let mut operations: Operations = default(); diff --git a/crates/s3s-aws/src/conv/generated.rs b/crates/s3s-aws/src/conv/generated.rs index 8b958e04..21b2fe68 100644 --- a/crates/s3s-aws/src/conv/generated.rs +++ b/crates/s3s-aws/src/conv/generated.rs @@ -1314,6 +1314,58 @@ impl AwsConversion for s3s::dto::CreateMultipartUploadOutput { } } +impl AwsConversion for s3s::dto::CreateSessionInput { + type Target = aws_sdk_s3::operation::create_session::CreateSessionInput; + type Error = S3Error; + + fn try_from_aws(x: Self::Target) -> S3Result { + Ok(Self { + bucket: unwrap_from_aws(x.bucket, "bucket")?, + bucket_key_enabled: try_from_aws(x.bucket_key_enabled)?, + ssekms_encryption_context: try_from_aws(x.ssekms_encryption_context)?, + ssekms_key_id: try_from_aws(x.ssekms_key_id)?, + server_side_encryption: try_from_aws(x.server_side_encryption)?, + session_mode: try_from_aws(x.session_mode)?, + }) + } + + fn try_into_aws(x: Self) -> S3Result { + let mut y = Self::Target::builder(); + y = y.set_bucket(Some(try_into_aws(x.bucket)?)); + y = y.set_bucket_key_enabled(try_into_aws(x.bucket_key_enabled)?); + y = y.set_ssekms_encryption_context(try_into_aws(x.ssekms_encryption_context)?); + y = y.set_ssekms_key_id(try_into_aws(x.ssekms_key_id)?); + y = y.set_server_side_encryption(try_into_aws(x.server_side_encryption)?); + y = y.set_session_mode(try_into_aws(x.session_mode)?); + y.build().map_err(S3Error::internal_error) + } +} + +impl AwsConversion for s3s::dto::CreateSessionOutput { + type Target = aws_sdk_s3::operation::create_session::CreateSessionOutput; + type Error = S3Error; + + fn try_from_aws(x: Self::Target) -> S3Result { + Ok(Self { + bucket_key_enabled: try_from_aws(x.bucket_key_enabled)?, + credentials: unwrap_from_aws(x.credentials, "credentials")?, + ssekms_encryption_context: try_from_aws(x.ssekms_encryption_context)?, + ssekms_key_id: try_from_aws(x.ssekms_key_id)?, + server_side_encryption: try_from_aws(x.server_side_encryption)?, + }) + } + + fn try_into_aws(x: Self) -> S3Result { + let mut y = Self::Target::builder(); + y = y.set_bucket_key_enabled(try_into_aws(x.bucket_key_enabled)?); + y = y.set_credentials(Some(try_into_aws(x.credentials)?)); + y = y.set_ssekms_encryption_context(try_into_aws(x.ssekms_encryption_context)?); + y = y.set_ssekms_key_id(try_into_aws(x.ssekms_key_id)?); + y = y.set_server_side_encryption(try_into_aws(x.server_side_encryption)?); + Ok(y.build()) + } +} + impl AwsConversion for s3s::dto::DataRedundancy { type Target = aws_sdk_s3::types::DataRedundancy; type Error = S3Error; diff --git a/crates/s3s-aws/src/conv/generated_minio.rs b/crates/s3s-aws/src/conv/generated_minio.rs index 0d61fdd4..52e0ab7f 100644 --- a/crates/s3s-aws/src/conv/generated_minio.rs +++ b/crates/s3s-aws/src/conv/generated_minio.rs @@ -1316,6 +1316,58 @@ impl AwsConversion for s3s::dto::CreateMultipartUploadOutput { } } +impl AwsConversion for s3s::dto::CreateSessionInput { + type Target = aws_sdk_s3::operation::create_session::CreateSessionInput; + type Error = S3Error; + + fn try_from_aws(x: Self::Target) -> S3Result { + Ok(Self { + bucket: unwrap_from_aws(x.bucket, "bucket")?, + bucket_key_enabled: try_from_aws(x.bucket_key_enabled)?, + ssekms_encryption_context: try_from_aws(x.ssekms_encryption_context)?, + ssekms_key_id: try_from_aws(x.ssekms_key_id)?, + server_side_encryption: try_from_aws(x.server_side_encryption)?, + session_mode: try_from_aws(x.session_mode)?, + }) + } + + fn try_into_aws(x: Self) -> S3Result { + let mut y = Self::Target::builder(); + y = y.set_bucket(Some(try_into_aws(x.bucket)?)); + y = y.set_bucket_key_enabled(try_into_aws(x.bucket_key_enabled)?); + y = y.set_ssekms_encryption_context(try_into_aws(x.ssekms_encryption_context)?); + y = y.set_ssekms_key_id(try_into_aws(x.ssekms_key_id)?); + y = y.set_server_side_encryption(try_into_aws(x.server_side_encryption)?); + y = y.set_session_mode(try_into_aws(x.session_mode)?); + y.build().map_err(S3Error::internal_error) + } +} + +impl AwsConversion for s3s::dto::CreateSessionOutput { + type Target = aws_sdk_s3::operation::create_session::CreateSessionOutput; + type Error = S3Error; + + fn try_from_aws(x: Self::Target) -> S3Result { + Ok(Self { + bucket_key_enabled: try_from_aws(x.bucket_key_enabled)?, + credentials: unwrap_from_aws(x.credentials, "credentials")?, + ssekms_encryption_context: try_from_aws(x.ssekms_encryption_context)?, + ssekms_key_id: try_from_aws(x.ssekms_key_id)?, + server_side_encryption: try_from_aws(x.server_side_encryption)?, + }) + } + + fn try_into_aws(x: Self) -> S3Result { + let mut y = Self::Target::builder(); + y = y.set_bucket_key_enabled(try_into_aws(x.bucket_key_enabled)?); + y = y.set_credentials(Some(try_into_aws(x.credentials)?)); + y = y.set_ssekms_encryption_context(try_into_aws(x.ssekms_encryption_context)?); + y = y.set_ssekms_key_id(try_into_aws(x.ssekms_key_id)?); + y = y.set_server_side_encryption(try_into_aws(x.server_side_encryption)?); + Ok(y.build()) + } +} + impl AwsConversion for s3s::dto::DataRedundancy { type Target = aws_sdk_s3::types::DataRedundancy; type Error = S3Error; diff --git a/crates/s3s-aws/src/proxy/generated.rs b/crates/s3s-aws/src/proxy/generated.rs index 2374e61a..270a7094 100644 --- a/crates/s3s-aws/src/proxy/generated.rs +++ b/crates/s3s-aws/src/proxy/generated.rs @@ -241,6 +241,32 @@ impl S3 for Proxy { } } + #[tracing::instrument(skip(self, req))] + async fn create_session( + &self, + req: S3Request, + ) -> S3Result> { + let input = req.input; + debug!(?input); + let mut b = self.0.create_session(); + b = b.set_bucket(Some(try_into_aws(input.bucket)?)); + b = b.set_bucket_key_enabled(try_into_aws(input.bucket_key_enabled)?); + b = b.set_ssekms_encryption_context(try_into_aws(input.ssekms_encryption_context)?); + b = b.set_ssekms_key_id(try_into_aws(input.ssekms_key_id)?); + b = b.set_server_side_encryption(try_into_aws(input.server_side_encryption)?); + b = b.set_session_mode(try_into_aws(input.session_mode)?); + let result = b.send().await; + match result { + Ok(output) => { + let headers = super::meta::build_headers(&output)?; + let output = try_from_aws(output)?; + debug!(?output); + Ok(S3Response::with_headers(output, headers)) + } + Err(e) => Err(wrap_sdk_error!(e)), + } + } + #[tracing::instrument(skip(self, req))] async fn delete_bucket( &self, diff --git a/crates/s3s-aws/src/proxy/generated_minio.rs b/crates/s3s-aws/src/proxy/generated_minio.rs index 2374e61a..270a7094 100644 --- a/crates/s3s-aws/src/proxy/generated_minio.rs +++ b/crates/s3s-aws/src/proxy/generated_minio.rs @@ -241,6 +241,32 @@ impl S3 for Proxy { } } + #[tracing::instrument(skip(self, req))] + async fn create_session( + &self, + req: S3Request, + ) -> S3Result> { + let input = req.input; + debug!(?input); + let mut b = self.0.create_session(); + b = b.set_bucket(Some(try_into_aws(input.bucket)?)); + b = b.set_bucket_key_enabled(try_into_aws(input.bucket_key_enabled)?); + b = b.set_ssekms_encryption_context(try_into_aws(input.ssekms_encryption_context)?); + b = b.set_ssekms_key_id(try_into_aws(input.ssekms_key_id)?); + b = b.set_server_side_encryption(try_into_aws(input.server_side_encryption)?); + b = b.set_session_mode(try_into_aws(input.session_mode)?); + let result = b.send().await; + match result { + Ok(output) => { + let headers = super::meta::build_headers(&output)?; + let output = try_from_aws(output)?; + debug!(?output); + Ok(S3Response::with_headers(output, headers)) + } + Err(e) => Err(wrap_sdk_error!(e)), + } + } + #[tracing::instrument(skip(self, req))] async fn delete_bucket( &self, diff --git a/crates/s3s/src/access/generated.rs b/crates/s3s/src/access/generated.rs index c16a4669..b76cb3ac 100644 --- a/crates/s3s/src/access/generated.rs +++ b/crates/s3s/src/access/generated.rs @@ -72,6 +72,13 @@ pub trait S3Access: Send + Sync + 'static { Ok(()) } + /// Checks whether the CreateSession request has accesses to the resources. + /// + /// This method returns `Ok(())` by default. + async fn create_session(&self, _req: &mut S3Request) -> S3Result<()> { + Ok(()) + } + /// Checks whether the DeleteBucket request has accesses to the resources. /// /// This method returns `Ok(())` by default. diff --git a/crates/s3s/src/access/generated_minio.rs b/crates/s3s/src/access/generated_minio.rs index c16a4669..b76cb3ac 100644 --- a/crates/s3s/src/access/generated_minio.rs +++ b/crates/s3s/src/access/generated_minio.rs @@ -72,6 +72,13 @@ pub trait S3Access: Send + Sync + 'static { Ok(()) } + /// Checks whether the CreateSession request has accesses to the resources. + /// + /// This method returns `Ok(())` by default. + async fn create_session(&self, _req: &mut S3Request) -> S3Result<()> { + Ok(()) + } + /// Checks whether the DeleteBucket request has accesses to the resources. /// /// This method returns `Ok(())` by default. diff --git a/crates/s3s/src/dto/generated.rs b/crates/s3s/src/dto/generated.rs index fcaa6db7..97628389 100644 --- a/crates/s3s/src/dto/generated.rs +++ b/crates/s3s/src/dto/generated.rs @@ -3707,6 +3707,131 @@ impl fmt::Debug for CreateMultipartUploadOutput { } } +#[derive(Clone, Default, PartialEq)] +pub struct CreateSessionInput { + ///

The name of the bucket that you create a session for.

+ pub bucket: BucketName, + ///

Specifies whether Amazon S3 should use an S3 Bucket Key for object encryption with + /// server-side encryption using KMS keys (SSE-KMS).

+ ///

S3 Bucket Keys are always enabled for GET and PUT operations in a directory bucket and can’t be disabled. S3 Bucket Keys aren't supported, when you copy SSE-KMS encrypted objects from general purpose buckets + /// to directory buckets, from directory buckets to general purpose buckets, or between directory buckets, through CopyObject, UploadPartCopy, the Copy operation in Batch Operations, or + /// the import jobs. In this case, Amazon S3 makes a call to KMS every time a copy request is made for a KMS-encrypted object.

+ pub bucket_key_enabled: Option, + ///

Specifies the Amazon Web Services KMS Encryption Context as an additional encryption context to use for object encryption. The value of + /// this header is a Base64 encoded string of a UTF-8 encoded JSON, which contains the encryption context as key-value pairs. + /// This value is stored as object metadata and automatically gets passed on + /// to Amazon Web Services KMS for future GetObject operations on + /// this object.

+ ///

+ /// General purpose buckets - This value must be explicitly added during CopyObject operations if you want an additional encryption context for your object. For more information, see Encryption context in the Amazon S3 User Guide.

+ ///

+ /// Directory buckets - You can optionally provide an explicit encryption context value. The value must match the default encryption context - the bucket Amazon Resource Name (ARN). An additional encryption context value is not supported.

+ pub ssekms_encryption_context: Option, + ///

If you specify x-amz-server-side-encryption with aws:kms, you must specify the + /// x-amz-server-side-encryption-aws-kms-key-id header with the ID (Key ID or Key ARN) of the KMS + /// symmetric encryption customer managed key to use. Otherwise, you get an HTTP 400 Bad Request error. Only use the key ID or key ARN. The key alias format of the KMS key isn't supported. Also, if the KMS key doesn't exist in the same + /// account that't issuing the command, you must use the full Key ARN not the Key ID.

+ ///

Your SSE-KMS configuration can only support 1 customer managed key per directory bucket's lifetime. + /// The Amazon Web Services managed key (aws/s3) isn't supported. + ///

+ pub ssekms_key_id: Option, + ///

The server-side encryption algorithm to use when you store objects in the directory bucket.

+ ///

For directory buckets, there are only two supported options for server-side encryption: server-side encryption with Amazon S3 managed keys (SSE-S3) (AES256) and server-side encryption with KMS keys (SSE-KMS) (aws:kms). By default, Amazon S3 encrypts data with SSE-S3. + /// For more + /// information, see Protecting data with server-side encryption in the Amazon S3 User Guide.

+ pub server_side_encryption: Option, + ///

Specifies the mode of the session that will be created, either ReadWrite or + /// ReadOnly. By default, a ReadWrite session is created. A + /// ReadWrite session is capable of executing all the Zonal endpoint API operations on a + /// directory bucket. A ReadOnly session is constrained to execute the following + /// Zonal endpoint API operations: GetObject, HeadObject, ListObjectsV2, + /// GetObjectAttributes, ListParts, and + /// ListMultipartUploads.

+ pub session_mode: Option, +} + +impl fmt::Debug for CreateSessionInput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("CreateSessionInput"); + d.field("bucket", &self.bucket); + if let Some(ref val) = self.bucket_key_enabled { + d.field("bucket_key_enabled", val); + } + if let Some(ref val) = self.ssekms_encryption_context { + d.field("ssekms_encryption_context", val); + } + if let Some(ref val) = self.ssekms_key_id { + d.field("ssekms_key_id", val); + } + if let Some(ref val) = self.server_side_encryption { + d.field("server_side_encryption", val); + } + if let Some(ref val) = self.session_mode { + d.field("session_mode", val); + } + d.finish_non_exhaustive() + } +} + +impl CreateSessionInput { + #[must_use] + pub fn builder() -> builders::CreateSessionInputBuilder { + default() + } +} + +#[derive(Clone, PartialEq)] +pub struct CreateSessionOutput { + ///

Indicates whether to use an S3 Bucket Key for server-side encryption + /// with KMS keys (SSE-KMS).

+ pub bucket_key_enabled: Option, + ///

The established temporary security credentials for the created session.

+ pub credentials: SessionCredentials, + ///

If present, indicates the Amazon Web Services KMS Encryption Context to use for object encryption. The value of + /// this header is a Base64 encoded string of a UTF-8 encoded JSON, which contains the encryption context as key-value pairs. + /// This value is stored as object metadata and automatically gets + /// passed on to Amazon Web Services KMS for future GetObject + /// operations on this object.

+ pub ssekms_encryption_context: Option, + ///

If you specify x-amz-server-side-encryption with aws:kms, this header indicates the ID of the KMS + /// symmetric encryption customer managed key that was used for object encryption.

+ pub ssekms_key_id: Option, + ///

The server-side encryption algorithm used when you store objects in the directory bucket.

+ pub server_side_encryption: Option, +} + +impl fmt::Debug for CreateSessionOutput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("CreateSessionOutput"); + if let Some(ref val) = self.bucket_key_enabled { + d.field("bucket_key_enabled", val); + } + d.field("credentials", &self.credentials); + if let Some(ref val) = self.ssekms_encryption_context { + d.field("ssekms_encryption_context", val); + } + if let Some(ref val) = self.ssekms_key_id { + d.field("ssekms_key_id", val); + } + if let Some(ref val) = self.server_side_encryption { + d.field("server_side_encryption", val); + } + d.finish_non_exhaustive() + } +} + +impl Default for CreateSessionOutput { + fn default() -> Self { + Self { + bucket_key_enabled: None, + credentials: default(), + ssekms_encryption_context: None, + ssekms_key_id: None, + server_side_encryption: None, + } + } +} + pub type CreationDate = Timestamp; ///

Amazon Web Services credentials for API authentication.

@@ -19986,6 +20111,17 @@ impl fmt::Debug for SessionCredentials { } } +impl Default for SessionCredentials { + fn default() -> Self { + Self { + access_key_id: default(), + expiration: default(), + secret_access_key: default(), + session_token: default(), + } + } +} + pub type SessionExpiration = Timestamp; #[derive(Debug, Clone, PartialEq, Eq)] @@ -21762,6 +21898,7 @@ mod tests { require_default::(); require_default::(); require_default::(); + require_default::(); require_default::(); require_default::(); require_default::(); @@ -21867,6 +22004,8 @@ mod tests { require_clone::(); require_clone::(); require_clone::(); + require_clone::(); + require_clone::(); require_clone::(); require_clone::(); require_clone::(); @@ -23783,6 +23922,107 @@ pub mod builders { } } + /// A builder for [`CreateSessionInput`] + #[derive(Default)] + pub struct CreateSessionInputBuilder { + bucket: Option, + + bucket_key_enabled: Option, + + ssekms_encryption_context: Option, + + ssekms_key_id: Option, + + server_side_encryption: Option, + + session_mode: Option, + } + + impl CreateSessionInputBuilder { + pub fn set_bucket(&mut self, field: BucketName) -> &mut Self { + self.bucket = Some(field); + self + } + + pub fn set_bucket_key_enabled(&mut self, field: Option) -> &mut Self { + self.bucket_key_enabled = field; + self + } + + pub fn set_ssekms_encryption_context(&mut self, field: Option) -> &mut Self { + self.ssekms_encryption_context = field; + self + } + + pub fn set_ssekms_key_id(&mut self, field: Option) -> &mut Self { + self.ssekms_key_id = field; + self + } + + pub fn set_server_side_encryption(&mut self, field: Option) -> &mut Self { + self.server_side_encryption = field; + self + } + + pub fn set_session_mode(&mut self, field: Option) -> &mut Self { + self.session_mode = field; + self + } + + #[must_use] + pub fn bucket(mut self, field: BucketName) -> Self { + self.bucket = Some(field); + self + } + + #[must_use] + pub fn bucket_key_enabled(mut self, field: Option) -> Self { + self.bucket_key_enabled = field; + self + } + + #[must_use] + pub fn ssekms_encryption_context(mut self, field: Option) -> Self { + self.ssekms_encryption_context = field; + self + } + + #[must_use] + pub fn ssekms_key_id(mut self, field: Option) -> Self { + self.ssekms_key_id = field; + self + } + + #[must_use] + pub fn server_side_encryption(mut self, field: Option) -> Self { + self.server_side_encryption = field; + self + } + + #[must_use] + pub fn session_mode(mut self, field: Option) -> Self { + self.session_mode = field; + self + } + + pub fn build(self) -> Result { + let bucket = self.bucket.ok_or_else(|| BuildError::missing_field("bucket"))?; + let bucket_key_enabled = self.bucket_key_enabled; + let ssekms_encryption_context = self.ssekms_encryption_context; + let ssekms_key_id = self.ssekms_key_id; + let server_side_encryption = self.server_side_encryption; + let session_mode = self.session_mode; + Ok(CreateSessionInput { + bucket, + bucket_key_enabled, + ssekms_encryption_context, + ssekms_key_id, + server_side_encryption, + session_mode, + }) + } + } + /// A builder for [`DeleteBucketInput`] #[derive(Default)] pub struct DeleteBucketInputBuilder { @@ -34039,6 +34279,42 @@ impl DtoExt for CreateMultipartUploadOutput { } } } +impl DtoExt for CreateSessionInput { + fn ignore_empty_strings(&mut self) { + if self.ssekms_encryption_context.as_deref() == Some("") { + self.ssekms_encryption_context = None; + } + if self.ssekms_key_id.as_deref() == Some("") { + self.ssekms_key_id = None; + } + if let Some(ref val) = self.server_side_encryption + && val.as_str() == "" + { + self.server_side_encryption = None; + } + if let Some(ref val) = self.session_mode + && val.as_str() == "" + { + self.session_mode = None; + } + } +} +impl DtoExt for CreateSessionOutput { + fn ignore_empty_strings(&mut self) { + self.credentials.ignore_empty_strings(); + if self.ssekms_encryption_context.as_deref() == Some("") { + self.ssekms_encryption_context = None; + } + if self.ssekms_key_id.as_deref() == Some("") { + self.ssekms_key_id = None; + } + if let Some(ref val) = self.server_side_encryption + && val.as_str() == "" + { + self.server_side_encryption = None; + } + } +} impl DtoExt for Credentials { fn ignore_empty_strings(&mut self) {} } diff --git a/crates/s3s/src/dto/generated_minio.rs b/crates/s3s/src/dto/generated_minio.rs index 0677b15b..731566e2 100644 --- a/crates/s3s/src/dto/generated_minio.rs +++ b/crates/s3s/src/dto/generated_minio.rs @@ -3715,6 +3715,131 @@ impl fmt::Debug for CreateMultipartUploadOutput { } } +#[derive(Clone, Default, PartialEq)] +pub struct CreateSessionInput { + ///

The name of the bucket that you create a session for.

+ pub bucket: BucketName, + ///

Specifies whether Amazon S3 should use an S3 Bucket Key for object encryption with + /// server-side encryption using KMS keys (SSE-KMS).

+ ///

S3 Bucket Keys are always enabled for GET and PUT operations in a directory bucket and can’t be disabled. S3 Bucket Keys aren't supported, when you copy SSE-KMS encrypted objects from general purpose buckets + /// to directory buckets, from directory buckets to general purpose buckets, or between directory buckets, through CopyObject, UploadPartCopy, the Copy operation in Batch Operations, or + /// the import jobs. In this case, Amazon S3 makes a call to KMS every time a copy request is made for a KMS-encrypted object.

+ pub bucket_key_enabled: Option, + ///

Specifies the Amazon Web Services KMS Encryption Context as an additional encryption context to use for object encryption. The value of + /// this header is a Base64 encoded string of a UTF-8 encoded JSON, which contains the encryption context as key-value pairs. + /// This value is stored as object metadata and automatically gets passed on + /// to Amazon Web Services KMS for future GetObject operations on + /// this object.

+ ///

+ /// General purpose buckets - This value must be explicitly added during CopyObject operations if you want an additional encryption context for your object. For more information, see Encryption context in the Amazon S3 User Guide.

+ ///

+ /// Directory buckets - You can optionally provide an explicit encryption context value. The value must match the default encryption context - the bucket Amazon Resource Name (ARN). An additional encryption context value is not supported.

+ pub ssekms_encryption_context: Option, + ///

If you specify x-amz-server-side-encryption with aws:kms, you must specify the + /// x-amz-server-side-encryption-aws-kms-key-id header with the ID (Key ID or Key ARN) of the KMS + /// symmetric encryption customer managed key to use. Otherwise, you get an HTTP 400 Bad Request error. Only use the key ID or key ARN. The key alias format of the KMS key isn't supported. Also, if the KMS key doesn't exist in the same + /// account that't issuing the command, you must use the full Key ARN not the Key ID.

+ ///

Your SSE-KMS configuration can only support 1 customer managed key per directory bucket's lifetime. + /// The Amazon Web Services managed key (aws/s3) isn't supported. + ///

+ pub ssekms_key_id: Option, + ///

The server-side encryption algorithm to use when you store objects in the directory bucket.

+ ///

For directory buckets, there are only two supported options for server-side encryption: server-side encryption with Amazon S3 managed keys (SSE-S3) (AES256) and server-side encryption with KMS keys (SSE-KMS) (aws:kms). By default, Amazon S3 encrypts data with SSE-S3. + /// For more + /// information, see Protecting data with server-side encryption in the Amazon S3 User Guide.

+ pub server_side_encryption: Option, + ///

Specifies the mode of the session that will be created, either ReadWrite or + /// ReadOnly. By default, a ReadWrite session is created. A + /// ReadWrite session is capable of executing all the Zonal endpoint API operations on a + /// directory bucket. A ReadOnly session is constrained to execute the following + /// Zonal endpoint API operations: GetObject, HeadObject, ListObjectsV2, + /// GetObjectAttributes, ListParts, and + /// ListMultipartUploads.

+ pub session_mode: Option, +} + +impl fmt::Debug for CreateSessionInput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("CreateSessionInput"); + d.field("bucket", &self.bucket); + if let Some(ref val) = self.bucket_key_enabled { + d.field("bucket_key_enabled", val); + } + if let Some(ref val) = self.ssekms_encryption_context { + d.field("ssekms_encryption_context", val); + } + if let Some(ref val) = self.ssekms_key_id { + d.field("ssekms_key_id", val); + } + if let Some(ref val) = self.server_side_encryption { + d.field("server_side_encryption", val); + } + if let Some(ref val) = self.session_mode { + d.field("session_mode", val); + } + d.finish_non_exhaustive() + } +} + +impl CreateSessionInput { + #[must_use] + pub fn builder() -> builders::CreateSessionInputBuilder { + default() + } +} + +#[derive(Clone, PartialEq)] +pub struct CreateSessionOutput { + ///

Indicates whether to use an S3 Bucket Key for server-side encryption + /// with KMS keys (SSE-KMS).

+ pub bucket_key_enabled: Option, + ///

The established temporary security credentials for the created session.

+ pub credentials: SessionCredentials, + ///

If present, indicates the Amazon Web Services KMS Encryption Context to use for object encryption. The value of + /// this header is a Base64 encoded string of a UTF-8 encoded JSON, which contains the encryption context as key-value pairs. + /// This value is stored as object metadata and automatically gets + /// passed on to Amazon Web Services KMS for future GetObject + /// operations on this object.

+ pub ssekms_encryption_context: Option, + ///

If you specify x-amz-server-side-encryption with aws:kms, this header indicates the ID of the KMS + /// symmetric encryption customer managed key that was used for object encryption.

+ pub ssekms_key_id: Option, + ///

The server-side encryption algorithm used when you store objects in the directory bucket.

+ pub server_side_encryption: Option, +} + +impl fmt::Debug for CreateSessionOutput { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("CreateSessionOutput"); + if let Some(ref val) = self.bucket_key_enabled { + d.field("bucket_key_enabled", val); + } + d.field("credentials", &self.credentials); + if let Some(ref val) = self.ssekms_encryption_context { + d.field("ssekms_encryption_context", val); + } + if let Some(ref val) = self.ssekms_key_id { + d.field("ssekms_key_id", val); + } + if let Some(ref val) = self.server_side_encryption { + d.field("server_side_encryption", val); + } + d.finish_non_exhaustive() + } +} + +impl Default for CreateSessionOutput { + fn default() -> Self { + Self { + bucket_key_enabled: None, + credentials: default(), + ssekms_encryption_context: None, + ssekms_key_id: None, + server_side_encryption: None, + } + } +} + pub type CreationDate = Timestamp; ///

Amazon Web Services credentials for API authentication.

@@ -20146,6 +20271,17 @@ impl fmt::Debug for SessionCredentials { } } +impl Default for SessionCredentials { + fn default() -> Self { + Self { + access_key_id: default(), + expiration: default(), + secret_access_key: default(), + session_token: default(), + } + } +} + pub type SessionExpiration = Timestamp; #[derive(Debug, Clone, PartialEq, Eq)] @@ -21930,6 +22066,7 @@ mod tests { require_default::(); require_default::(); require_default::(); + require_default::(); require_default::(); require_default::(); require_default::(); @@ -22035,6 +22172,8 @@ mod tests { require_clone::(); require_clone::(); require_clone::(); + require_clone::(); + require_clone::(); require_clone::(); require_clone::(); require_clone::(); @@ -23981,6 +24120,107 @@ pub mod builders { } } + /// A builder for [`CreateSessionInput`] + #[derive(Default)] + pub struct CreateSessionInputBuilder { + bucket: Option, + + bucket_key_enabled: Option, + + ssekms_encryption_context: Option, + + ssekms_key_id: Option, + + server_side_encryption: Option, + + session_mode: Option, + } + + impl CreateSessionInputBuilder { + pub fn set_bucket(&mut self, field: BucketName) -> &mut Self { + self.bucket = Some(field); + self + } + + pub fn set_bucket_key_enabled(&mut self, field: Option) -> &mut Self { + self.bucket_key_enabled = field; + self + } + + pub fn set_ssekms_encryption_context(&mut self, field: Option) -> &mut Self { + self.ssekms_encryption_context = field; + self + } + + pub fn set_ssekms_key_id(&mut self, field: Option) -> &mut Self { + self.ssekms_key_id = field; + self + } + + pub fn set_server_side_encryption(&mut self, field: Option) -> &mut Self { + self.server_side_encryption = field; + self + } + + pub fn set_session_mode(&mut self, field: Option) -> &mut Self { + self.session_mode = field; + self + } + + #[must_use] + pub fn bucket(mut self, field: BucketName) -> Self { + self.bucket = Some(field); + self + } + + #[must_use] + pub fn bucket_key_enabled(mut self, field: Option) -> Self { + self.bucket_key_enabled = field; + self + } + + #[must_use] + pub fn ssekms_encryption_context(mut self, field: Option) -> Self { + self.ssekms_encryption_context = field; + self + } + + #[must_use] + pub fn ssekms_key_id(mut self, field: Option) -> Self { + self.ssekms_key_id = field; + self + } + + #[must_use] + pub fn server_side_encryption(mut self, field: Option) -> Self { + self.server_side_encryption = field; + self + } + + #[must_use] + pub fn session_mode(mut self, field: Option) -> Self { + self.session_mode = field; + self + } + + pub fn build(self) -> Result { + let bucket = self.bucket.ok_or_else(|| BuildError::missing_field("bucket"))?; + let bucket_key_enabled = self.bucket_key_enabled; + let ssekms_encryption_context = self.ssekms_encryption_context; + let ssekms_key_id = self.ssekms_key_id; + let server_side_encryption = self.server_side_encryption; + let session_mode = self.session_mode; + Ok(CreateSessionInput { + bucket, + bucket_key_enabled, + ssekms_encryption_context, + ssekms_key_id, + server_side_encryption, + session_mode, + }) + } + } + /// A builder for [`DeleteBucketInput`] #[derive(Default)] pub struct DeleteBucketInputBuilder { @@ -34288,6 +34528,42 @@ impl DtoExt for CreateMultipartUploadOutput { } } } +impl DtoExt for CreateSessionInput { + fn ignore_empty_strings(&mut self) { + if self.ssekms_encryption_context.as_deref() == Some("") { + self.ssekms_encryption_context = None; + } + if self.ssekms_key_id.as_deref() == Some("") { + self.ssekms_key_id = None; + } + if let Some(ref val) = self.server_side_encryption + && val.as_str() == "" + { + self.server_side_encryption = None; + } + if let Some(ref val) = self.session_mode + && val.as_str() == "" + { + self.session_mode = None; + } + } +} +impl DtoExt for CreateSessionOutput { + fn ignore_empty_strings(&mut self) { + self.credentials.ignore_empty_strings(); + if self.ssekms_encryption_context.as_deref() == Some("") { + self.ssekms_encryption_context = None; + } + if self.ssekms_key_id.as_deref() == Some("") { + self.ssekms_key_id = None; + } + if let Some(ref val) = self.server_side_encryption + && val.as_str() == "" + { + self.server_side_encryption = None; + } + } +} impl DtoExt for Credentials { fn ignore_empty_strings(&mut self) {} } diff --git a/crates/s3s/src/dto/timestamp.rs b/crates/s3s/src/dto/timestamp.rs index d5646bde..7f032cac 100644 --- a/crates/s3s/src/dto/timestamp.rs +++ b/crates/s3s/src/dto/timestamp.rs @@ -60,6 +60,12 @@ impl From for Timestamp { } } +impl Default for Timestamp { + fn default() -> Self { + Self(time::OffsetDateTime::UNIX_EPOCH) + } +} + #[derive(Debug, thiserror::Error)] pub enum ParseTimestampError { #[error("time: {0}")] @@ -159,6 +165,13 @@ impl Timestamp { mod tests { use super::*; + #[test] + fn default_is_unix_epoch() { + let ts = Timestamp::default(); + let dt: time::OffsetDateTime = ts.into(); + assert_eq!(dt, time::OffsetDateTime::UNIX_EPOCH); + } + #[test] fn text_repr() { let cases = [ diff --git a/crates/s3s/src/ops/generated.rs b/crates/s3s/src/ops/generated.rs index ce0dcdd1..bdd2f552 100644 --- a/crates/s3s/src/ops/generated.rs +++ b/crates/s3s/src/ops/generated.rs @@ -6,6 +6,7 @@ // CreateBucket // CreateBucketMetadataTableConfiguration // CreateMultipartUpload +// CreateSession // DeleteBucket // DeleteBucketAnalyticsConfiguration // DeleteBucketCors @@ -284,6 +285,16 @@ impl http::TryIntoHeaderValue for ServerSideEncryption { } } +impl http::TryIntoHeaderValue for SessionMode { + type Error = http::InvalidHeaderValue; + fn try_into_header_value(self) -> Result { + match Cow::from(self) { + Cow::Borrowed(s) => http::HeaderValue::try_from(s), + Cow::Owned(s) => http::HeaderValue::try_from(s), + } + } +} + impl http::TryIntoHeaderValue for StorageClass { type Error = http::InvalidHeaderValue; fn try_into_header_value(self) -> Result { @@ -450,6 +461,14 @@ impl http::TryFromHeaderValue for ServerSideEncryption { } } +impl http::TryFromHeaderValue for SessionMode { + type Error = http::ParseHeaderError; + fn try_from_header_value(val: &http::HeaderValue) -> Result { + let val = val.to_str().map_err(|_| http::ParseHeaderError::Enum)?; + Ok(Self::from(val.to_owned())) + } +} + impl http::TryFromHeaderValue for StorageClass { type Error = http::ParseHeaderError; fn try_from_header_value(val: &http::HeaderValue) -> Result { @@ -1084,6 +1103,70 @@ impl super::Operation for CreateMultipartUpload { } } +pub struct CreateSession; + +impl CreateSession { + pub fn deserialize_http(req: &mut http::Request) -> S3Result { + let bucket = http::unwrap_bucket(req); + + let bucket_key_enabled: Option = + http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION_BUCKET_KEY_ENABLED)?; + + let ssekms_encryption_context: Option = + http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT)?; + + let ssekms_key_id: Option = http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID)?; + + let server_side_encryption: Option = http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION)?; + + let session_mode: Option = http::parse_opt_header(req, &X_AMZ_CREATE_SESSION_MODE)?; + + Ok(CreateSessionInput { + bucket, + bucket_key_enabled, + ssekms_encryption_context, + ssekms_key_id, + server_side_encryption, + session_mode, + }) + } + + pub fn serialize_http(x: CreateSessionOutput) -> S3Result { + let mut res = http::Response::with_status(http::StatusCode::OK); + http::set_xml_body(&mut res, &x)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION_BUCKET_KEY_ENABLED, x.bucket_key_enabled)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT, x.ssekms_encryption_context)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, x.ssekms_key_id)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION, x.server_side_encryption)?; + Ok(res) + } +} + +#[async_trait::async_trait] +impl super::Operation for CreateSession { + fn name(&self) -> &'static str { + "CreateSession" + } + + async fn call(&self, ccx: &CallContext<'_>, req: &mut http::Request) -> S3Result { + let input = Self::deserialize_http(req)?; + let mut s3_req = super::build_s3_request(input, req); + let s3 = ccx.s3; + if let Some(access) = ccx.access { + access.create_session(&mut s3_req).await?; + } + let result = s3.create_session(s3_req).await; + let s3_resp = match result { + Ok(val) => val, + Err(err) => return super::serialize_error(err, false), + }; + let mut resp = Self::serialize_http(s3_resp.output)?; + resp.headers.extend(s3_resp.headers); + resp.extensions.extend(s3_resp.extensions); + Ok(resp) + } +} + pub struct DeleteBucket; impl DeleteBucket { @@ -6711,6 +6794,9 @@ pub fn resolve_route( if qs.has("metrics") && qs.has("id") { return Ok((&GetBucketMetricsConfiguration as &'static dyn super::Operation, false)); } + if qs.has("session") { + return Ok((&CreateSession as &'static dyn super::Operation, false)); + } if qs.has("accelerate") { return Ok((&GetBucketAccelerateConfiguration as &'static dyn super::Operation, false)); } diff --git a/crates/s3s/src/ops/generated_minio.rs b/crates/s3s/src/ops/generated_minio.rs index 4c98f0a1..bcd16bcc 100644 --- a/crates/s3s/src/ops/generated_minio.rs +++ b/crates/s3s/src/ops/generated_minio.rs @@ -6,6 +6,7 @@ // CreateBucket // CreateBucketMetadataTableConfiguration // CreateMultipartUpload +// CreateSession // DeleteBucket // DeleteBucketAnalyticsConfiguration // DeleteBucketCors @@ -284,6 +285,16 @@ impl http::TryIntoHeaderValue for ServerSideEncryption { } } +impl http::TryIntoHeaderValue for SessionMode { + type Error = http::InvalidHeaderValue; + fn try_into_header_value(self) -> Result { + match Cow::from(self) { + Cow::Borrowed(s) => http::HeaderValue::try_from(s), + Cow::Owned(s) => http::HeaderValue::try_from(s), + } + } +} + impl http::TryIntoHeaderValue for StorageClass { type Error = http::InvalidHeaderValue; fn try_into_header_value(self) -> Result { @@ -450,6 +461,14 @@ impl http::TryFromHeaderValue for ServerSideEncryption { } } +impl http::TryFromHeaderValue for SessionMode { + type Error = http::ParseHeaderError; + fn try_from_header_value(val: &http::HeaderValue) -> Result { + let val = val.to_str().map_err(|_| http::ParseHeaderError::Enum)?; + Ok(Self::from(val.to_owned())) + } +} + impl http::TryFromHeaderValue for StorageClass { type Error = http::ParseHeaderError; fn try_from_header_value(val: &http::HeaderValue) -> Result { @@ -1090,6 +1109,70 @@ impl super::Operation for CreateMultipartUpload { } } +pub struct CreateSession; + +impl CreateSession { + pub fn deserialize_http(req: &mut http::Request) -> S3Result { + let bucket = http::unwrap_bucket(req); + + let bucket_key_enabled: Option = + http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION_BUCKET_KEY_ENABLED)?; + + let ssekms_encryption_context: Option = + http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT)?; + + let ssekms_key_id: Option = http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID)?; + + let server_side_encryption: Option = http::parse_opt_header(req, &X_AMZ_SERVER_SIDE_ENCRYPTION)?; + + let session_mode: Option = http::parse_opt_header(req, &X_AMZ_CREATE_SESSION_MODE)?; + + Ok(CreateSessionInput { + bucket, + bucket_key_enabled, + ssekms_encryption_context, + ssekms_key_id, + server_side_encryption, + session_mode, + }) + } + + pub fn serialize_http(x: CreateSessionOutput) -> S3Result { + let mut res = http::Response::with_status(http::StatusCode::OK); + http::set_xml_body(&mut res, &x)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION_BUCKET_KEY_ENABLED, x.bucket_key_enabled)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION_CONTEXT, x.ssekms_encryption_context)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, x.ssekms_key_id)?; + http::add_opt_header(&mut res, X_AMZ_SERVER_SIDE_ENCRYPTION, x.server_side_encryption)?; + Ok(res) + } +} + +#[async_trait::async_trait] +impl super::Operation for CreateSession { + fn name(&self) -> &'static str { + "CreateSession" + } + + async fn call(&self, ccx: &CallContext<'_>, req: &mut http::Request) -> S3Result { + let input = Self::deserialize_http(req)?; + let mut s3_req = super::build_s3_request(input, req); + let s3 = ccx.s3; + if let Some(access) = ccx.access { + access.create_session(&mut s3_req).await?; + } + let result = s3.create_session(s3_req).await; + let s3_resp = match result { + Ok(val) => val, + Err(err) => return super::serialize_error(err, false), + }; + let mut resp = Self::serialize_http(s3_resp.output)?; + resp.headers.extend(s3_resp.headers); + resp.extensions.extend(s3_resp.extensions); + Ok(resp) + } +} + pub struct DeleteBucket; impl DeleteBucket { @@ -6726,6 +6809,9 @@ pub fn resolve_route( if qs.has("metrics") && qs.has("id") { return Ok((&GetBucketMetricsConfiguration as &'static dyn super::Operation, false)); } + if qs.has("session") { + return Ok((&CreateSession as &'static dyn super::Operation, false)); + } if qs.has("accelerate") { return Ok((&GetBucketAccelerateConfiguration as &'static dyn super::Operation, false)); } diff --git a/crates/s3s/src/ops/tests.rs b/crates/s3s/src/ops/tests.rs index b00e0bd1..3e65c67b 100644 --- a/crates/s3s/src/ops/tests.rs +++ b/crates/s3s/src/ops/tests.rs @@ -1575,3 +1575,74 @@ async fn post_policy_file_size_is_total_bytes_not_chunk_count() { Ok(_) => panic!("POST object with 50-byte file should fail content-length-range [100, 50000] validation"), } } + +#[test] +fn create_session_route_resolved() { + use crate::http::{Body, OrderedQs}; + use crate::path::S3Path; + + let req = crate::http::Request::from( + hyper::Request::builder() + .method(Method::GET) + .uri("http://localhost/my-bucket?session") + .body(Body::empty()) + .unwrap(), + ); + + let s3_path = S3Path::Bucket { + bucket: "my-bucket".into(), + }; + let qs = OrderedQs::parse("session").unwrap(); + let (op, needs_full_body) = generated::resolve_route(&req, &s3_path, Some(&qs)).unwrap(); + + assert_eq!(op.name(), "CreateSession"); + assert!(!needs_full_body); +} + +#[test] +fn create_session_deserialize_http() { + use crate::http::Body; + use crate::path::S3Path; + + let mut req = crate::http::Request::from( + hyper::Request::builder() + .method(Method::GET) + .uri("http://localhost/my-bucket?session") + .header("x-amz-create-session-mode", "ReadWrite") + .body(Body::empty()) + .unwrap(), + ); + + req.s3ext.s3_path = Some(S3Path::Bucket { + bucket: "my-bucket".into(), + }); + + let input = generated::CreateSession::deserialize_http(&mut req).unwrap(); + + assert_eq!(input.bucket, "my-bucket"); + assert_eq!(input.session_mode.as_ref().map(crate::dto::SessionMode::as_str), Some("ReadWrite")); + assert!(input.server_side_encryption.is_none()); + assert!(input.ssekms_key_id.is_none()); + assert!(input.ssekms_encryption_context.is_none()); + assert!(input.bucket_key_enabled.is_none()); +} + +#[test] +fn create_session_serialize_http() { + use crate::dto::{CreateSessionOutput, SessionCredentials, Timestamp, TimestampFormat}; + + let creds = SessionCredentials { + access_key_id: "AKIAIOSFODNN7EXAMPLE".to_owned(), + expiration: Timestamp::parse(TimestampFormat::DateTime, "2024-01-01T00:05:00.000Z").unwrap(), + secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_owned(), + session_token: "FwoGZXIvYXdzEBYaDHqa0A".to_owned(), + }; + + let output = CreateSessionOutput { + credentials: creds, + ..Default::default() + }; + + let resp = generated::CreateSession::serialize_http(output).unwrap(); + assert_eq!(resp.status, hyper::StatusCode::OK); +} diff --git a/crates/s3s/src/s3_trait.rs b/crates/s3s/src/s3_trait.rs index 12463fc6..884f577b 100644 --- a/crates/s3s/src/s3_trait.rs +++ b/crates/s3s/src/s3_trait.rs @@ -979,6 +979,119 @@ pub trait S3: Send + Sync + 'static { Err(s3_error!(NotImplemented, "CreateMultipartUpload is not implemented yet")) } + ///

Creates a session that establishes temporary security credentials to support fast + /// authentication and authorization for the Zonal endpoint API operations on directory buckets. For more + /// information about Zonal endpoint API operations that include the Availability Zone in the request endpoint, see S3 Express One Zone + /// APIs in the Amazon S3 User Guide.

+ ///

To make Zonal endpoint API requests on a directory bucket, use the CreateSession + /// API operation. Specifically, you grant s3express:CreateSession permission to a + /// bucket in a bucket policy or an IAM identity-based policy. Then, you use IAM credentials to make the + /// CreateSession API request on the bucket, which returns temporary security + /// credentials that include the access key ID, secret access key, session token, and + /// expiration. These credentials have associated permissions to access the Zonal endpoint API operations. After + /// the session is created, you don’t need to use other policies to grant permissions to each + /// Zonal endpoint API individually. Instead, in your Zonal endpoint API requests, you sign your requests by + /// applying the temporary security credentials of the session to the request headers and + /// following the SigV4 protocol for authentication. You also apply the session token to the + /// x-amz-s3session-token request header for authorization. Temporary security + /// credentials are scoped to the bucket and expire after 5 minutes. After the expiration time, + /// any calls that you make with those credentials will fail. You must use IAM credentials + /// again to make a CreateSession API request that generates a new set of + /// temporary credentials for use. Temporary credentials cannot be extended or refreshed beyond + /// the original specified interval.

+ ///

If you use Amazon Web Services SDKs, SDKs handle the session token refreshes automatically to avoid + /// service interruptions when a session expires. We recommend that you use the Amazon Web Services SDKs to + /// initiate and manage requests to the CreateSession API. For more information, see Performance guidelines and design patterns in the + /// Amazon S3 User Guide.

+ /// + ///
    + ///
  • + ///

    You must make requests for this API operation to the Zonal endpoint. These endpoints support virtual-hosted-style requests in the format https://bucket-name.s3express-zone-id.region-code.amazonaws.com. Path-style requests are not supported. For more information about endpoints in Availability Zones, see Regional and Zonal endpoints for directory buckets in Availability Zones in the + /// Amazon S3 User Guide. For more information about endpoints in Local Zones, see Concepts for directory buckets in Local Zones in the + /// Amazon S3 User Guide.

    + ///
  • + ///
  • + ///

    + /// + /// CopyObject API operation - + /// Unlike other Zonal endpoint API operations, the CopyObject API operation doesn't use + /// the temporary security credentials returned from the CreateSession + /// API operation for authentication and authorization. For information about + /// authentication and authorization of the CopyObject API operation on + /// directory buckets, see CopyObject.

    + ///
  • + ///
  • + ///

    + /// + /// HeadBucket API operation - + /// Unlike other Zonal endpoint API operations, the HeadBucket API operation doesn't use + /// the temporary security credentials returned from the CreateSession + /// API operation for authentication and authorization. For information about + /// authentication and authorization of the HeadBucket API operation on + /// directory buckets, see HeadBucket.

    + ///
  • + ///
+ ///
+ ///
+ ///
Permissions
+ ///
+ ///

To obtain temporary security credentials, you must create + /// a bucket policy or an IAM identity-based policy that grants s3express:CreateSession + /// permission to the bucket. In a policy, you can have the + /// s3express:SessionMode condition key to control who can create a + /// ReadWrite or ReadOnly session. For more information + /// about ReadWrite or ReadOnly sessions, see + /// x-amz-create-session-mode + /// . For example policies, see + /// Example bucket policies for S3 Express One Zone and Amazon Web Services Identity and Access Management (IAM) identity-based policies for + /// S3 Express One Zone in the Amazon S3 User Guide.

+ ///

To grant cross-account access to Zonal endpoint API operations, the bucket policy should also + /// grant both accounts the s3express:CreateSession permission.

+ ///

If you want to encrypt objects with SSE-KMS, you must also have the + /// kms:GenerateDataKey and the kms:Decrypt permissions + /// in IAM identity-based policies and KMS key policies for the target KMS + /// key.

+ ///
+ ///
Encryption
+ ///
+ ///

For directory buckets, there are only two supported options for server-side encryption: server-side encryption with Amazon S3 managed keys (SSE-S3) (AES256) and server-side encryption with KMS keys (SSE-KMS) (aws:kms). We recommend that the bucket's default encryption uses the desired encryption configuration and you don't override the bucket default encryption in your + /// CreateSession requests or PUT object requests. Then, new objects + /// are automatically encrypted with the desired encryption settings. For more + /// information, see Protecting data with server-side encryption in the Amazon S3 User Guide. For more information about the encryption overriding behaviors in directory buckets, see Specifying server-side encryption with KMS for new object uploads.

+ ///

For Zonal endpoint (object-level) API operations except CopyObject and UploadPartCopy, + /// you authenticate and authorize requests through CreateSession for low latency. + /// To encrypt new objects in a directory bucket with SSE-KMS, you must specify SSE-KMS as the directory bucket's default encryption configuration with a KMS key (specifically, a customer managed key). Then, when a session is created for Zonal endpoint API operations, new objects are automatically encrypted and decrypted with SSE-KMS and S3 Bucket Keys during the session.

+ /// + ///

+ /// Only 1 customer managed key is supported per directory bucket for the lifetime of the bucket. The Amazon Web Services managed key (aws/s3) isn't supported. + /// After you specify SSE-KMS as your bucket's default encryption configuration with a customer managed key, you can't change the customer managed key for the bucket's SSE-KMS configuration. + ///

+ ///
+ ///

In the Zonal endpoint API calls (except CopyObject and UploadPartCopy) using the REST API, + /// you can't override the values of the encryption settings (x-amz-server-side-encryption, x-amz-server-side-encryption-aws-kms-key-id, x-amz-server-side-encryption-context, and x-amz-server-side-encryption-bucket-key-enabled) from the CreateSession request. + /// You don't need to explicitly specify these encryption settings values in Zonal endpoint API calls, and + /// Amazon S3 will use the encryption settings values from the CreateSession request to protect new objects in the directory bucket. + ///

+ /// + ///

When you use the CLI or the Amazon Web Services SDKs, for CreateSession, the session token refreshes automatically to avoid service interruptions when a session expires. The CLI or the Amazon Web Services SDKs use the bucket's default encryption configuration for the + /// CreateSession request. It's not supported to override the encryption settings values in the CreateSession request. + /// Also, in the Zonal endpoint API calls (except CopyObject and UploadPartCopy), + /// it's not supported to override the values of the encryption settings from the CreateSession request. + /// + ///

+ ///
+ ///
+ ///
HTTP Host header syntax
+ ///
+ ///

+ /// Directory buckets - The HTTP Host header syntax is + /// Bucket-name.s3express-zone-id.region-code.amazonaws.com.

+ ///
+ ///
+ async fn create_session(&self, _req: S3Request) -> S3Result> { + Err(s3_error!(NotImplemented, "CreateSession is not implemented yet")) + } + ///

Deletes the S3 bucket. All objects (including all object versions and delete markers) in /// the bucket must be deleted before the bucket itself can be deleted.

/// diff --git a/crates/s3s/src/xml/generated.rs b/crates/s3s/src/xml/generated.rs index 7175e6e4..c995b0ca 100644 --- a/crates/s3s/src/xml/generated.rs +++ b/crates/s3s/src/xml/generated.rs @@ -30,6 +30,7 @@ use std::io::Write; // Serialize: CreateBucketConfiguration // Deserialize: CreateBucketConfiguration // Serialize: CreateMultipartUploadOutput +// Serialize: CreateSessionOutput // Serialize: Delete // Deserialize: Delete // Serialize: DeleteObjectsOutput @@ -126,6 +127,8 @@ use std::io::Write; // DeserializeContent: AccessControlTranslation // SerializeContent: AccessKeyIdType // DeserializeContent: AccessKeyIdType +// SerializeContent: AccessKeyIdValue +// DeserializeContent: AccessKeyIdValue // SerializeContent: AccessKeySecretType // DeserializeContent: AccessKeySecretType // SerializeContent: AccessPointArn @@ -238,6 +241,7 @@ use std::io::Write; // SerializeContent: CreateBucketConfiguration // DeserializeContent: CreateBucketConfiguration // SerializeContent: CreateMultipartUploadOutput +// SerializeContent: CreateSessionOutput // SerializeContent: CreationDate // DeserializeContent: CreationDate // SerializeContent: Credentials @@ -728,6 +732,12 @@ use std::io::Write; // DeserializeContent: ServerSideEncryptionConfiguration // SerializeContent: ServerSideEncryptionRule // DeserializeContent: ServerSideEncryptionRule +// SerializeContent: SessionCredentialValue +// DeserializeContent: SessionCredentialValue +// SerializeContent: SessionCredentials +// DeserializeContent: SessionCredentials +// SerializeContent: SessionExpiration +// DeserializeContent: SessionExpiration // SerializeContent: Setting // DeserializeContent: Setting // SerializeContent: SimplePrefix @@ -939,6 +949,12 @@ impl Serialize for CreateMultipartUploadOutput { } } +impl Serialize for CreateSessionOutput { + fn serialize(&self, s: &mut Serializer) -> SerResult { + s.content_with_ns("CreateSessionResult", XMLNS_S3, self) + } +} + impl Serialize for Delete { fn serialize(&self, s: &mut Serializer) -> SerResult { s.content("Delete", self) @@ -3021,6 +3037,13 @@ impl SerializeContent for CreateMultipartUploadOutput { } } +impl SerializeContent for CreateSessionOutput { + fn serialize_content(&self, s: &mut Serializer) -> SerResult { + s.content("Credentials", &self.credentials)?; + Ok(()) + } +} + impl SerializeContent for Credentials { fn serialize_content(&self, s: &mut Serializer) -> SerResult { s.content("AccessKeyId", &self.access_key_id)?; @@ -9333,6 +9356,61 @@ impl<'xml> DeserializeContent<'xml> for ServerSideEncryptionRule { }) } } +impl SerializeContent for SessionCredentials { + fn serialize_content(&self, s: &mut Serializer) -> SerResult { + s.content("AccessKeyId", &self.access_key_id)?; + s.timestamp("Expiration", &self.expiration, TimestampFormat::DateTime)?; + s.content("SecretAccessKey", &self.secret_access_key)?; + s.content("SessionToken", &self.session_token)?; + Ok(()) + } +} + +impl<'xml> DeserializeContent<'xml> for SessionCredentials { + fn deserialize_content(d: &mut Deserializer<'xml>) -> DeResult { + let mut access_key_id: Option = None; + let mut expiration: Option = None; + let mut secret_access_key: Option = None; + let mut session_token: Option = None; + d.for_each_element(|d, x| match x { + b"AccessKeyId" => { + if access_key_id.is_some() { + return Err(DeError::DuplicateField); + } + access_key_id = Some(d.content()?); + Ok(()) + } + b"Expiration" => { + if expiration.is_some() { + return Err(DeError::DuplicateField); + } + expiration = Some(d.timestamp(TimestampFormat::DateTime)?); + Ok(()) + } + b"SecretAccessKey" => { + if secret_access_key.is_some() { + return Err(DeError::DuplicateField); + } + secret_access_key = Some(d.content()?); + Ok(()) + } + b"SessionToken" => { + if session_token.is_some() { + return Err(DeError::DuplicateField); + } + session_token = Some(d.content()?); + Ok(()) + } + _ => Err(DeError::UnexpectedTagName), + })?; + Ok(Self { + access_key_id: access_key_id.ok_or(DeError::MissingField)?, + expiration: expiration.ok_or(DeError::MissingField)?, + secret_access_key: secret_access_key.ok_or(DeError::MissingField)?, + session_token: session_token.ok_or(DeError::MissingField)?, + }) + } +} impl SerializeContent for SimplePrefix { fn serialize_content(&self, _: &mut Serializer) -> SerResult { Ok(()) diff --git a/crates/s3s/src/xml/generated_minio.rs b/crates/s3s/src/xml/generated_minio.rs index a2b50607..b5a1c55d 100644 --- a/crates/s3s/src/xml/generated_minio.rs +++ b/crates/s3s/src/xml/generated_minio.rs @@ -30,6 +30,7 @@ use std::io::Write; // Serialize: CreateBucketConfiguration // Deserialize: CreateBucketConfiguration // Serialize: CreateMultipartUploadOutput +// Serialize: CreateSessionOutput // Serialize: Delete // Deserialize: Delete // Serialize: DeleteObjectsOutput @@ -126,6 +127,8 @@ use std::io::Write; // DeserializeContent: AccessControlTranslation // SerializeContent: AccessKeyIdType // DeserializeContent: AccessKeyIdType +// SerializeContent: AccessKeyIdValue +// DeserializeContent: AccessKeyIdValue // SerializeContent: AccessKeySecretType // DeserializeContent: AccessKeySecretType // SerializeContent: AccessPointArn @@ -238,6 +241,7 @@ use std::io::Write; // SerializeContent: CreateBucketConfiguration // DeserializeContent: CreateBucketConfiguration // SerializeContent: CreateMultipartUploadOutput +// SerializeContent: CreateSessionOutput // SerializeContent: CreationDate // DeserializeContent: CreationDate // SerializeContent: Credentials @@ -736,6 +740,12 @@ use std::io::Write; // DeserializeContent: ServerSideEncryptionConfiguration // SerializeContent: ServerSideEncryptionRule // DeserializeContent: ServerSideEncryptionRule +// SerializeContent: SessionCredentialValue +// DeserializeContent: SessionCredentialValue +// SerializeContent: SessionCredentials +// DeserializeContent: SessionCredentials +// SerializeContent: SessionExpiration +// DeserializeContent: SessionExpiration // SerializeContent: Setting // DeserializeContent: Setting // SerializeContent: SimplePrefix @@ -947,6 +957,12 @@ impl Serialize for CreateMultipartUploadOutput { } } +impl Serialize for CreateSessionOutput { + fn serialize(&self, s: &mut Serializer) -> SerResult { + s.content_with_ns("CreateSessionResult", XMLNS_S3, self) + } +} + impl Serialize for Delete { fn serialize(&self, s: &mut Serializer) -> SerResult { s.content("Delete", self) @@ -3029,6 +3045,13 @@ impl SerializeContent for CreateMultipartUploadOutput { } } +impl SerializeContent for CreateSessionOutput { + fn serialize_content(&self, s: &mut Serializer) -> SerResult { + s.content("Credentials", &self.credentials)?; + Ok(()) + } +} + impl SerializeContent for Credentials { fn serialize_content(&self, s: &mut Serializer) -> SerResult { s.content("AccessKeyId", &self.access_key_id)?; @@ -9426,6 +9449,61 @@ impl<'xml> DeserializeContent<'xml> for ServerSideEncryptionRule { }) } } +impl SerializeContent for SessionCredentials { + fn serialize_content(&self, s: &mut Serializer) -> SerResult { + s.content("AccessKeyId", &self.access_key_id)?; + s.timestamp("Expiration", &self.expiration, TimestampFormat::DateTime)?; + s.content("SecretAccessKey", &self.secret_access_key)?; + s.content("SessionToken", &self.session_token)?; + Ok(()) + } +} + +impl<'xml> DeserializeContent<'xml> for SessionCredentials { + fn deserialize_content(d: &mut Deserializer<'xml>) -> DeResult { + let mut access_key_id: Option = None; + let mut expiration: Option = None; + let mut secret_access_key: Option = None; + let mut session_token: Option = None; + d.for_each_element(|d, x| match x { + b"AccessKeyId" => { + if access_key_id.is_some() { + return Err(DeError::DuplicateField); + } + access_key_id = Some(d.content()?); + Ok(()) + } + b"Expiration" => { + if expiration.is_some() { + return Err(DeError::DuplicateField); + } + expiration = Some(d.timestamp(TimestampFormat::DateTime)?); + Ok(()) + } + b"SecretAccessKey" => { + if secret_access_key.is_some() { + return Err(DeError::DuplicateField); + } + secret_access_key = Some(d.content()?); + Ok(()) + } + b"SessionToken" => { + if session_token.is_some() { + return Err(DeError::DuplicateField); + } + session_token = Some(d.content()?); + Ok(()) + } + _ => Err(DeError::UnexpectedTagName), + })?; + Ok(Self { + access_key_id: access_key_id.ok_or(DeError::MissingField)?, + expiration: expiration.ok_or(DeError::MissingField)?, + secret_access_key: secret_access_key.ok_or(DeError::MissingField)?, + session_token: session_token.ok_or(DeError::MissingField)?, + }) + } +} impl SerializeContent for SimplePrefix { fn serialize_content(&self, _: &mut Serializer) -> SerResult { Ok(()) diff --git a/crates/s3s/src/xml/mod.rs b/crates/s3s/src/xml/mod.rs index 493c59b2..0c846ac9 100644 --- a/crates/s3s/src/xml/mod.rs +++ b/crates/s3s/src/xml/mod.rs @@ -137,4 +137,36 @@ mod tests { assert!(xml.contains(&expected), "Long ETag must use literal quotes; got: {xml}"); assert!(!xml.contains("""), "Long ETag must not use "; got: {xml}"); } + + #[test] + fn create_session_output_xml_serialization() { + use crate::dto::{CreateSessionOutput, SessionCredentials, Timestamp, TimestampFormat}; + + let creds = SessionCredentials { + access_key_id: "AKIAIOSFODNN7EXAMPLE".to_owned(), + expiration: Timestamp::parse(TimestampFormat::DateTime, "2024-01-01T00:05:00.000Z").unwrap(), + secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_owned(), + session_token: "FwoGZXIvYXdzEBYaDHqa0A".to_owned(), + }; + + let output = CreateSessionOutput { + credentials: creds, + ..Default::default() + }; + + let mut buf = Vec::new(); + let mut ser = Serializer::new(Cursor::new(&mut buf)); + output.serialize(&mut ser).unwrap(); + let xml = String::from_utf8(buf).unwrap(); + + assert!(xml.contains("CreateSessionResult"), "root element must be CreateSessionResult: {xml}"); + assert!(xml.contains(""), "must contain Credentials element: {xml}"); + assert!( + xml.contains("AKIAIOSFODNN7EXAMPLE"), + "must contain AccessKeyId: {xml}" + ); + assert!(xml.contains(""), "must contain SecretAccessKey: {xml}"); + assert!(xml.contains(""), "must contain SessionToken: {xml}"); + assert!(xml.contains(""), "must contain Expiration: {xml}"); + } }