bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
use super::*;

impl BucketWarden {
    pub fn get_bucket_abac(
        &mut self,
        principal: &str,
        bucket: &str,
    ) -> Result<BucketAbacStatus, RuntimeError> {
        self.authorize(principal, S3Action::GetBucketAbac, bucket)?;
        let status = if self.require_bucket(bucket)?.abac_enabled {
            "Enabled"
        } else {
            "Disabled"
        }
        .to_string();
        self.audit_allowed(
            principal,
            S3Action::GetBucketAbac,
            bucket,
            Some(status.clone()),
        );
        Ok(BucketAbacStatus {
            bucket: bucket.to_string(),
            status,
        })
    }

    pub fn put_bucket_abac(
        &mut self,
        principal: &str,
        bucket: &str,
        status: String,
    ) -> Result<BucketAbacStatus, RuntimeError> {
        self.authorize(principal, S3Action::PutBucketAbac, bucket)?;
        if crate::bucket_basics::is_directory_bucket_name(bucket) {
            return Err(RuntimeError::InvalidBucketName(
                "PutBucketAbac is only supported for general purpose buckets".to_string(),
            ));
        }
        let abac_enabled = match status.as_str() {
            "Enabled" => true,
            "Disabled" => false,
            other => return Err(RuntimeError::InvalidBucketAbac(other.to_string())),
        };
        self.require_bucket_mut(bucket)?.abac_enabled = abac_enabled;
        self.audit_allowed(
            principal,
            S3Action::PutBucketAbac,
            bucket,
            Some(status.clone()),
        );
        Ok(BucketAbacStatus {
            bucket: bucket.to_string(),
            status,
        })
    }

    pub fn create_session(
        &mut self,
        principal: &str,
        request: CreateSessionRequest,
    ) -> Result<CreateSessionResult, RuntimeError> {
        self.authorize(principal, S3Action::CreateSession, &request.bucket)?;
        let bucket_state = self.require_bucket(&request.bucket)?.clone();
        let session_mode = request
            .session_mode
            .unwrap_or_else(|| "ReadWrite".to_string());
        let scope = create_session_scope(&request.bucket, &session_mode)?;
        let encryption = create_session_encryption(
            bucket_state.encryption.as_ref(),
            &request.server_side_encryption,
            &request.kms_key_id,
            &request.encryption_context,
            request.bucket_key_enabled,
        )?;
        self.auth.create_local_user(principal.to_string());
        let request_id = self.next_request_id();
        let parent_access_key_id = format!("create-session-parent:{principal}");
        if self.auth.credential(&parent_access_key_id).is_none() {
            self.auth
                .put_access_key(AccessKey::active(
                    principal.to_string(),
                    parent_access_key_id.clone(),
                    format!("bucketwarden-create-session-parent-{principal}"),
                ))
                .map_err(RuntimeError::Auth)?;
        }
        let access_key_id = format!(
            "ASIABW{}",
            request_id.trim_start_matches("bwreq").to_ascii_uppercase()
        );
        let secret_access_key = format!("bucketwarden-secret-{request_id}");
        let session_token = format!("bucketwarden-session-{request_id}");
        let expires_at_epoch_seconds = self.clock_epoch_seconds.saturating_add(900);
        self.auth
            .put_session(
                SessionCredential::new(
                    principal.to_string(),
                    parent_access_key_id,
                    access_key_id.clone(),
                    secret_access_key.clone(),
                    session_token.clone(),
                    expires_at_epoch_seconds,
                )
                .with_scope(scope),
            )
            .map_err(RuntimeError::Auth)?;
        self.audit_allowed(
            principal,
            S3Action::CreateSession,
            &request.bucket,
            Some(format!("{session_mode}:{access_key_id}")),
        );
        Ok(CreateSessionResult {
            bucket: request.bucket,
            session_mode,
            credentials: bucketwarden_s3::CreateSessionCredentials {
                access_key_id,
                expiration: epoch_seconds_to_iso8601(expires_at_epoch_seconds),
                secret_access_key,
                session_token,
            },
            server_side_encryption: encryption.as_ref().map(|value| value.algorithm.clone()),
            kms_key_id: encryption
                .as_ref()
                .and_then(|value| value.kms_key_id.clone()),
            encryption_context: request.encryption_context,
            bucket_key_enabled: None,
        })
    }
}

fn create_session_scope(bucket: &str, session_mode: &str) -> Result<CredentialScope, RuntimeError> {
    match session_mode {
        "ReadWrite" => Ok(CredentialScope::new(
            vec!["s3:*".to_string(), "s3express:CreateSession".to_string()],
            vec![bucket.to_string(), format!("{bucket}/")],
        )),
        "ReadOnly" => Ok(CredentialScope::new(
            vec![
                "s3:GetObject".to_string(),
                "s3:GetObjectAttributes".to_string(),
                "s3:ListBucket".to_string(),
                "s3:ListBucketMultipartUploads".to_string(),
                "s3:ListMultipartUploadParts".to_string(),
            ],
            vec![bucket.to_string(), format!("{bucket}/")],
        )),
        other => Err(RuntimeError::InvalidCreateSession(format!(
            "unsupported x-amz-create-session-mode: {other}"
        ))),
    }
}

fn create_session_encryption(
    bucket_default: Option<&ServerSideEncryption>,
    requested_algorithm: &Option<String>,
    requested_kms_key_id: &Option<String>,
    requested_encryption_context: &Option<String>,
    requested_bucket_key_enabled: Option<bool>,
) -> Result<Option<ServerSideEncryption>, RuntimeError> {
    let Some(bucket_default) = bucket_default.cloned() else {
        if requested_algorithm.is_some()
            || requested_kms_key_id.is_some()
            || requested_encryption_context.is_some()
            || requested_bucket_key_enabled.is_some()
        {
            return Err(RuntimeError::InvalidCreateSession(
                "CreateSession encryption overrides require a bucket default encryption configuration"
                    .to_string(),
            ));
        }
        return Ok(None);
    };
    if let Some(algorithm) = requested_algorithm.as_ref() {
        if algorithm != &bucket_default.algorithm {
            return Err(RuntimeError::InvalidCreateSession(
                "CreateSession encryption overrides must match the bucket default encryption"
                    .to_string(),
            ));
        }
    }
    if requested_kms_key_id.is_some()
        && requested_kms_key_id.as_ref() != bucket_default.kms_key_id.as_ref()
    {
        return Err(RuntimeError::InvalidCreateSession(
            "CreateSession KMS key overrides must match the bucket default encryption".to_string(),
        ));
    }
    if requested_bucket_key_enabled.is_some() {
        return Err(RuntimeError::InvalidCreateSession(
            "CreateSession does not support bucket key override headers".to_string(),
        ));
    }
    if requested_encryption_context.is_some() {
        return Err(RuntimeError::InvalidCreateSession(
            "CreateSession does not support overriding encryption context".to_string(),
        ));
    }
    Ok(Some(bucket_default))
}

fn epoch_seconds_to_iso8601(epoch_seconds: u64) -> String {
    let days = epoch_seconds / 86_400;
    let seconds_of_day = epoch_seconds % 86_400;
    let hour = seconds_of_day / 3_600;
    let minute = (seconds_of_day % 3_600) / 60;
    let second = seconds_of_day % 60;

    let z = days as i64 + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
    let year = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let day = doy - (153 * mp + 2) / 5 + 1;
    let month = mp + if mp < 10 { 3 } else { -9 };
    let year = year + i64::from(month <= 2);

    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
}