use parse_rust_core::{ErrorCode, ErrorDetail, ParseError};
pub fn enforce_class_security(
class_name: &str,
privileged: bool,
operation: &str,
detail: ErrorDetail,
) -> Result<(), ParseError> {
if privileged {
return Ok(());
}
if class_name == "_Installation" && matches!(operation, "delete" | "find") {
return Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!(
"Clients aren't allowed to perform the {operation} operation on the installation collection."
),
detail,
));
}
let forbidden = MASTER_ONLY_CLASSES.contains(&class_name)
|| class_name.starts_with("_Join:")
|| (class_name == crate::pipeline::SESSION_CLASS && matches!(operation, "create" | "update" | "delete"))
|| (class_name == crate::pipeline::USER_CLASS
&& matches!(operation, "create" | "delete"));
if forbidden {
return Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!(
"Clients aren't allowed to perform the {operation} operation on the {class_name} collection."
),
detail,
));
}
Ok(())
}
const MASTER_ONLY_CLASSES: [&str; 8] = [
"_JobStatus",
"_PushStatus",
"_Hooks",
"_GlobalConfig",
"_GraphQLConfig",
"_JobSchedule",
"_Audience",
"_Idempotency",
];
#[cfg(test)]
mod tests {
use super::*;
const WITHHELD: ErrorDetail = ErrorDetail::Withheld;
const DISCLOSED: ErrorDetail = ErrorDetail::Disclosed;
const CLIENT: bool = false;
const MASTER: bool = true;
#[test]
fn role_and_session_reads_are_no_longer_denylisted() {
assert!(enforce_class_security("_Role", CLIENT, "find", WITHHELD).is_ok());
assert!(enforce_class_security("_Role", CLIENT, "create", WITHHELD).is_ok());
assert!(enforce_class_security("_Session", CLIENT, "find", WITHHELD).is_ok());
assert!(enforce_class_security("_Session", CLIENT, "get", WITHHELD).is_ok());
}
#[test]
fn the_master_only_list_is_upstreams() {
for class in MASTER_ONLY_CLASSES {
let e = enforce_class_security(class, CLIENT, "find", DISCLOSED).unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(
e.message,
format!("Clients aren't allowed to perform the find operation on the {class} collection.")
);
let withheld = enforce_class_security(class, CLIENT, "find", WITHHELD).unwrap_err();
assert_eq!(withheld.code, ErrorCode::OperationForbidden);
assert_eq!(withheld.message, "Permission denied");
assert!(enforce_class_security(class, MASTER, "find", WITHHELD).is_ok());
}
}
#[test]
fn installation_is_restricted_on_two_operations_only() {
for op in ["delete", "find"] {
let e = enforce_class_security("_Installation", CLIENT, op, DISCLOSED).unwrap_err();
assert_eq!(
e.message,
format!(
"Clients aren't allowed to perform the {op} operation on the installation collection."
),
"the message names the collection in lower case, not the class"
);
assert_eq!(
enforce_class_security("_Installation", CLIENT, op, WITHHELD)
.unwrap_err()
.message,
"Permission denied"
);
}
for op in ["get", "create", "update"] {
assert!(enforce_class_security("_Installation", CLIENT, op, WITHHELD).is_ok());
}
}
#[test]
fn join_tables_are_never_client_addressable() {
let e = enforce_class_security("_Join:users:_Role", CLIENT, "find", WITHHELD).unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
}
#[test]
fn session_and_user_writes_are_refused_but_reads_are_not() {
for op in ["create", "update", "delete"] {
assert_eq!(
enforce_class_security(crate::pipeline::SESSION_CLASS, CLIENT, op, WITHHELD)
.unwrap_err()
.code,
ErrorCode::OperationForbidden,
"_Session/{op}"
);
}
for op in ["create", "delete"] {
assert_eq!(
enforce_class_security(crate::pipeline::USER_CLASS, CLIENT, op, WITHHELD)
.unwrap_err()
.code,
ErrorCode::OperationForbidden,
"_User/{op}"
);
}
assert!(
enforce_class_security(crate::pipeline::USER_CLASS, CLIENT, "update", WITHHELD).is_ok(),
"a client may save its own user row"
);
for class in [crate::pipeline::SESSION_CLASS, crate::pipeline::USER_CLASS] {
for op in ["find", "get"] {
assert!(
enforce_class_security(class, CLIENT, op, WITHHELD).is_ok(),
"{class}/{op}"
);
}
assert!(enforce_class_security(class, MASTER, "create", WITHHELD).is_ok());
}
}
}