use crate::error::{DbError, DbResult};
pub const PROTECTED_COLLECTIONS: [&str; 5] =
["_env", "_admins", "_api_keys", "_roles", "_user_roles"];
pub const WRITE_PROTECTED_COLLECTIONS: [&str; 7] = [
"_scripts",
"_services",
"_triggers",
"_views",
"_graphs",
"_config",
"_rag_pipelines",
];
pub const ADMIN_WRITE_COLLECTIONS: [&str; 1] = ["_jobs"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteActor {
Client { can_admin: bool },
Server,
}
impl WriteActor {
pub fn client(can_admin: bool) -> Self {
Self::Client { can_admin }
}
}
#[inline]
fn bare_name(name: &str) -> &str {
name.rsplit(':').next().unwrap_or(name)
}
pub fn is_protected_collection(name: &str) -> bool {
PROTECTED_COLLECTIONS.contains(&bare_name(name))
}
pub fn is_write_protected_collection(name: &str) -> bool {
WRITE_PROTECTED_COLLECTIONS.contains(&bare_name(name))
}
pub fn is_write_denied_collection(name: &str) -> bool {
is_protected_collection(name) || is_write_protected_collection(name)
}
pub fn is_admin_write_collection(name: &str) -> bool {
ADMIN_WRITE_COLLECTIONS.contains(&bare_name(name))
}
pub fn check_write_access(name: &str, actor: WriteActor) -> DbResult<()> {
if is_write_denied_collection(name) {
return Err(write_denied_collection_error(name));
}
if is_admin_write_collection(name) && actor == (WriteActor::Client { can_admin: false }) {
return Err(admin_write_collection_error(name));
}
Ok(())
}
pub fn admin_write_collection_error(name: &str) -> DbError {
DbError::Forbidden(format!(
"Access denied: '{}' is executed by the server and is writable only \
with an Admin credential",
bare_name(name)
))
}
pub fn protected_collection_error(name: &str) -> DbError {
DbError::Forbidden(format!(
"Access denied: '{}' stores credentials and is not readable or \
writable through this API; use the admin-only endpoints",
bare_name(name)
))
}
pub fn write_protected_collection_error(name: &str) -> DbError {
DbError::Forbidden(format!(
"Access denied: '{}' is managed by the server and is not writable \
through this API; use the dedicated admin endpoints",
bare_name(name)
))
}
pub fn write_denied_collection_error(name: &str) -> DbError {
if is_protected_collection(name) {
protected_collection_error(name)
} else {
write_protected_collection_error(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_names_are_protected() {
assert!(is_protected_collection("_env"));
assert!(is_protected_collection("_admins"));
assert!(is_protected_collection("_api_keys"));
}
#[test]
fn authorization_state_is_protected() {
assert!(is_protected_collection("_roles"));
assert!(is_protected_collection("_user_roles"));
assert!(is_protected_collection("_system:_user_roles"));
}
#[test]
fn qualified_names_are_protected() {
assert!(is_protected_collection("mydb:_env"));
assert!(is_protected_collection("_system:_admins"));
assert!(is_protected_collection("victim:_api_keys"));
}
#[test]
fn ordinary_collections_are_not() {
assert!(!is_protected_collection("users"));
assert!(!is_protected_collection("mydb:users"));
assert!(!is_write_denied_collection("users"));
assert!(!is_protected_collection("_slow_queries"));
}
#[test]
fn executable_collections_are_readable_but_not_writable() {
for name in ["_scripts", "_services", "_triggers", "_views"] {
assert!(
!is_protected_collection(name),
"{name} should stay readable"
);
assert!(is_write_protected_collection(name), "{name} write guard");
assert!(is_write_denied_collection(name), "{name} write denied");
}
assert!(is_write_denied_collection("tenant:_scripts"));
}
#[test]
fn credential_collections_are_also_write_denied() {
assert!(is_write_denied_collection("_env"));
assert!(is_write_denied_collection("_user_roles"));
}
#[test]
fn near_misses_are_not_protected() {
assert!(!is_protected_collection("_environment"));
assert!(!is_protected_collection("my_env"));
assert!(!is_protected_collection("_env2"));
assert!(!is_write_denied_collection("_scripts_backup"));
assert!(!is_write_denied_collection("my_jobs"));
}
#[test]
fn jobs_is_writable_by_admins_only() {
assert!(!is_write_denied_collection("_jobs"));
assert!(is_admin_write_collection("_jobs"));
assert!(is_admin_write_collection("app:_jobs"));
assert!(!is_admin_write_collection("my_jobs"));
assert!(check_write_access("_jobs", WriteActor::client(true)).is_ok());
assert!(check_write_access("_jobs", WriteActor::Server).is_ok());
let err = check_write_access("_jobs", WriteActor::client(false)).unwrap_err();
assert!(err.to_string().contains("Admin"), "{err}");
assert!(check_write_access("_scripts", WriteActor::client(true)).is_err());
assert!(check_write_access("_env", WriteActor::Server).is_err());
assert!(check_write_access("users", WriteActor::client(false)).is_ok());
}
}