use super::{AccessGrant, QuotaUsage, evaluate_workspace_quota};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageResource {
Blob,
CommentAttachment,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StorageReservationRequest {
pub resource: StorageResource,
pub size: i64,
pub has_key: bool,
pub has_doc_id: bool,
pub has_name: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StorageAuthorizationFacts {
pub workspace_upload: bool,
pub doc_read: bool,
pub doc_comment_create: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageObjectFacts {
NotChecked,
Missing,
Matching,
MetadataMismatch,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageLedgerFacts {
Missing,
Pending {
live: bool,
size_matches: bool,
mime_matches: bool,
owner_matches: bool,
},
Completed {
size_matches: bool,
mime_matches: bool,
owner_matches: bool,
object: StorageObjectFacts,
},
Deleted,
InvalidStatus,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StorageReservationFacts {
pub authorization: StorageAuthorizationFacts,
pub grant: AccessGrant,
pub usage: QuotaUsage,
pub ledger: StorageLedgerFacts,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageMutationIntent {
Create,
Resume,
ReplaceExpired,
RepairMissingObject,
AlreadyUploaded,
InspectCompletedObject,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageReservationDenial {
Forbidden,
WorkspaceReadonly,
BlobLimitExceeded { limit: i64 },
StorageLimitExceeded { limit: i64 },
ArithmeticOverflow,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageReservationPlan {
Mutate {
intent: StorageMutationIntent,
requested_increment: i64,
},
Deny(StorageReservationDenial),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StorageReservationError {
InvalidRequest,
MissingAttachmentFields,
Deleted,
SizeMismatch,
MimeMismatch,
OwnerMismatch,
InvalidLedgerStatus,
ObjectMetadataMismatch,
}
pub fn plan_storage_reservation(
request: StorageReservationRequest,
facts: StorageReservationFacts,
) -> Result<StorageReservationPlan, StorageReservationError> {
if request.size <= 0 || !request.has_key {
return Err(StorageReservationError::InvalidRequest);
}
if request.resource == StorageResource::CommentAttachment && (!request.has_doc_id || !request.has_name) {
return Err(StorageReservationError::MissingAttachmentFields);
}
match request.resource {
StorageResource::Blob if !facts.authorization.workspace_upload => {
return Ok(StorageReservationPlan::Deny(StorageReservationDenial::Forbidden));
}
StorageResource::CommentAttachment if !facts.authorization.doc_read || !facts.authorization.doc_comment_create => {
return Ok(StorageReservationPlan::Deny(StorageReservationDenial::Forbidden));
}
_ => {}
}
if request.size > facts.grant.limits.blob_limit {
return Ok(StorageReservationPlan::Deny(
StorageReservationDenial::BlobLimitExceeded {
limit: facts.grant.limits.blob_limit,
},
));
}
let workspace_readonly = !evaluate_workspace_quota(&facts.grant, facts.usage)
.readonly_reasons
.is_empty();
let (intent, requested_increment) = match facts.ledger {
StorageLedgerFacts::Missing => (StorageMutationIntent::Create, request.size),
StorageLedgerFacts::Deleted => return Err(StorageReservationError::Deleted),
StorageLedgerFacts::InvalidStatus => return Err(StorageReservationError::InvalidLedgerStatus),
StorageLedgerFacts::Pending {
live,
size_matches,
mime_matches,
owner_matches,
} => {
validate_metadata(size_matches, mime_matches, owner_matches)?;
if live {
if workspace_readonly {
return Ok(StorageReservationPlan::Deny(
StorageReservationDenial::WorkspaceReadonly,
));
}
return Ok(StorageReservationPlan::Mutate {
intent: StorageMutationIntent::Resume,
requested_increment: 0,
});
}
(StorageMutationIntent::ReplaceExpired, request.size)
}
StorageLedgerFacts::Completed {
size_matches,
mime_matches,
owner_matches,
object,
} => {
validate_metadata(size_matches, mime_matches, owner_matches)?;
match object {
StorageObjectFacts::NotChecked => {
return Ok(StorageReservationPlan::Mutate {
intent: StorageMutationIntent::InspectCompletedObject,
requested_increment: 0,
});
}
StorageObjectFacts::Matching => {
return Ok(StorageReservationPlan::Mutate {
intent: StorageMutationIntent::AlreadyUploaded,
requested_increment: 0,
});
}
StorageObjectFacts::Missing => (StorageMutationIntent::RepairMissingObject, 0),
StorageObjectFacts::MetadataMismatch => return Err(StorageReservationError::ObjectMetadataMismatch),
}
}
};
let Some(next_storage_bytes) = facts.usage.storage_bytes.checked_add(requested_increment) else {
return Ok(StorageReservationPlan::Deny(
StorageReservationDenial::ArithmeticOverflow,
));
};
if workspace_readonly {
return Ok(StorageReservationPlan::Deny(
StorageReservationDenial::WorkspaceReadonly,
));
}
match next_storage_bytes {
next if next <= facts.grant.limits.storage_quota => Ok(StorageReservationPlan::Mutate {
intent,
requested_increment,
}),
_ => Ok(StorageReservationPlan::Deny(
StorageReservationDenial::StorageLimitExceeded {
limit: facts.grant.limits.storage_quota,
},
)),
}
}
fn validate_metadata(
size_matches: bool,
mime_matches: bool,
owner_matches: bool,
) -> Result<(), StorageReservationError> {
if !size_matches {
Err(StorageReservationError::SizeMismatch)
} else if !mime_matches {
Err(StorageReservationError::MimeMismatch)
} else if !owner_matches {
Err(StorageReservationError::OwnerMismatch)
} else {
Ok(())
}
}
#[cfg(test)]
#[path = "../tests/access_control/storage/tests.rs"]
mod tests;