#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorkspaceDatabaseTable {
Folders,
DocProperties,
DocCustomPropertyInfo,
PinnedCollections,
ExplorerIcon,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UserdataTable {
Favorite,
Settings,
DocIntegrationRef,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReservedDocumentClassification<'a> {
Ordinary,
WorkspaceDatabase { table: WorkspaceDatabaseTable },
Userdata { owner_id: &'a str, table: UserdataTable },
InvalidReserved,
}
impl ReservedDocumentClassification<'_> {
pub fn is_valid_reserved(self) -> bool {
matches!(self, Self::WorkspaceDatabase { .. } | Self::Userdata { .. })
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReservedDocumentAccessDecision {
NotApplicable,
Allowed,
Denied,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum ReservedDocumentError {
#[error("invalid userdata owner")]
InvalidOwner,
#[error("invalid workspace")]
InvalidWorkspace,
}
pub fn classify_reserved_document<'a>(workspace_id: &str, doc_id: &'a str) -> ReservedDocumentClassification<'a> {
if let Some(rest) = doc_id.strip_prefix("db$") {
let mut parts = rest.split('$');
let (Some(encoded_workspace_id), Some(table), None) = (parts.next(), parts.next(), parts.next()) else {
return ReservedDocumentClassification::InvalidReserved;
};
if workspace_id.is_empty() || encoded_workspace_id != workspace_id {
return ReservedDocumentClassification::InvalidReserved;
}
return parse_workspace_table(table).map_or(ReservedDocumentClassification::InvalidReserved, |table| {
ReservedDocumentClassification::WorkspaceDatabase { table }
});
}
if let Some(rest) = doc_id.strip_prefix("userdata$") {
let mut parts = rest.split('$');
let (Some(owner_id), Some(encoded_workspace_id), Some(table), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return ReservedDocumentClassification::InvalidReserved;
};
if !valid_owner(owner_id) || workspace_id.is_empty() || encoded_workspace_id != workspace_id {
return ReservedDocumentClassification::InvalidReserved;
}
return parse_userdata_table(table).map_or(ReservedDocumentClassification::InvalidReserved, |table| {
ReservedDocumentClassification::Userdata { owner_id, table }
});
}
ReservedDocumentClassification::Ordinary
}
pub fn authorize_reserved_document(
actor_user_id: &str,
classification: ReservedDocumentClassification<'_>,
) -> ReservedDocumentAccessDecision {
match classification {
ReservedDocumentClassification::Ordinary => ReservedDocumentAccessDecision::NotApplicable,
ReservedDocumentClassification::WorkspaceDatabase { .. } => ReservedDocumentAccessDecision::Allowed,
ReservedDocumentClassification::Userdata { owner_id, .. } if owner_id == actor_user_id => {
ReservedDocumentAccessDecision::Allowed
}
ReservedDocumentClassification::Userdata { .. } | ReservedDocumentClassification::InvalidReserved => {
ReservedDocumentAccessDecision::Denied
}
}
}
pub fn userdata_document_id(
user_id: &str,
workspace_id: &str,
table: UserdataTable,
) -> Result<String, ReservedDocumentError> {
if !valid_owner(user_id) {
return Err(ReservedDocumentError::InvalidOwner);
}
if workspace_id.is_empty() || workspace_id.contains('$') {
return Err(ReservedDocumentError::InvalidWorkspace);
}
Ok(format!(
"userdata${user_id}${workspace_id}${}",
userdata_table_name(table)
))
}
fn valid_owner(owner_id: &str) -> bool {
!owner_id.is_empty() && owner_id != "__local__" && !owner_id.contains('$')
}
fn parse_workspace_table(table: &str) -> Option<WorkspaceDatabaseTable> {
match table {
"folders" => Some(WorkspaceDatabaseTable::Folders),
"docProperties" => Some(WorkspaceDatabaseTable::DocProperties),
"docCustomPropertyInfo" => Some(WorkspaceDatabaseTable::DocCustomPropertyInfo),
"pinnedCollections" => Some(WorkspaceDatabaseTable::PinnedCollections),
"explorerIcon" => Some(WorkspaceDatabaseTable::ExplorerIcon),
_ => None,
}
}
fn parse_userdata_table(table: &str) -> Option<UserdataTable> {
match table {
"favorite" => Some(UserdataTable::Favorite),
"settings" => Some(UserdataTable::Settings),
"docIntegrationRef" => Some(UserdataTable::DocIntegrationRef),
_ => None,
}
}
fn userdata_table_name(table: UserdataTable) -> &'static str {
match table {
UserdataTable::Favorite => "favorite",
UserdataTable::Settings => "settings",
UserdataTable::DocIntegrationRef => "docIntegrationRef",
}
}
#[cfg(test)]
#[path = "../tests/access_control/reserved_document/tests.rs"]
mod tests;