use std::collections::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DriveOperation {
Read,
Create,
Upload,
Edit,
SheetsWrite,
SheetsStructure,
SheetsDelete,
SheetsProtection,
DocsWrite,
Trash,
SlidesWrite,
DocsDelete,
DocsFormat,
DocsStructure,
DocsTableDelete,
}
impl std::fmt::Display for DriveOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Read => "read",
Self::Create => "create",
Self::Upload => "upload",
Self::Edit => "edit",
Self::SheetsWrite => "sheets-write",
Self::SheetsStructure => "sheets-structure",
Self::SheetsDelete => "sheets-delete",
Self::SheetsProtection => "sheets-protection",
Self::DocsWrite => "docs-write",
Self::Trash => "trash",
Self::SlidesWrite => "slides-write",
Self::DocsDelete => "docs-delete",
Self::DocsFormat => "docs-format",
Self::DocsStructure => "docs-structure",
Self::DocsTableDelete => "docs-table-delete",
};
write!(f, "{s}")
}
}
impl DriveOperation {
pub(crate) fn default_policy(self) -> Verdict {
match self {
Self::Read => Verdict::Allow,
Self::Create
| Self::Upload
| Self::Edit
| Self::SheetsWrite
| Self::SheetsStructure
| Self::SheetsDelete
| Self::SheetsProtection
| Self::DocsWrite
| Self::Trash
| Self::SlidesWrite
| Self::DocsStructure
| Self::DocsTableDelete
| Self::DocsDelete
| Self::DocsFormat => Verdict::Deny,
}
}
#[must_use]
pub const fn ever_requires_lease(self) -> bool {
match self {
Self::Read | Self::Create | Self::Upload | Self::Trash => false,
Self::Edit
| Self::SheetsWrite
| Self::SheetsStructure
| Self::SheetsDelete
| Self::SheetsProtection
| Self::DocsWrite
| Self::SlidesWrite
| Self::DocsStructure
| Self::DocsTableDelete
| Self::DocsDelete
| Self::DocsFormat => true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "RawPermissionRule")]
pub struct FolderPermissionRule {
#[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
pub recursive: bool,
pub allow: HashSet<DriveOperation>,
pub deny: HashSet<DriveOperation>,
#[serde(default = "default_require_lease")]
pub require_lease: bool,
}
fn default_require_lease() -> bool {
true
}
impl FolderPermissionRule {
#[must_use]
pub fn folder(folder_id: impl Into<String>) -> Self {
Self {
folder_id: Some(folder_id.into()),
file_id: None,
recursive: false,
allow: HashSet::new(),
deny: HashSet::new(),
require_lease: default_require_lease(),
}
}
#[must_use]
pub fn file(file_id: impl Into<String>) -> Self {
Self {
folder_id: None,
file_id: Some(file_id.into()),
recursive: false,
allow: HashSet::new(),
deny: HashSet::new(),
require_lease: default_require_lease(),
}
}
#[must_use]
pub fn recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
#[must_use]
pub fn requiring_lease(mut self, require_lease: bool) -> Self {
self.require_lease = require_lease;
self
}
#[must_use]
pub fn allowing(mut self, ops: impl IntoIterator<Item = DriveOperation>) -> Self {
self.allow = ops.into_iter().collect();
self
}
#[must_use]
pub fn denying(mut self, ops: impl IntoIterator<Item = DriveOperation>) -> Self {
self.deny = ops.into_iter().collect();
self
}
}
#[derive(Debug, Deserialize)]
struct RawPermissionRule {
#[serde(default)]
folder_id: Option<String>,
#[serde(default)]
file_id: Option<String>,
#[serde(default)]
recursive: bool,
#[serde(default)]
allow: HashSet<DriveOperation>,
#[serde(default)]
deny: HashSet<DriveOperation>,
#[serde(default = "default_require_lease")]
require_lease: bool,
}
impl TryFrom<RawPermissionRule> for FolderPermissionRule {
type Error = String;
fn try_from(raw: RawPermissionRule) -> Result<Self, Self::Error> {
match (&raw.folder_id, &raw.file_id) {
(None, None) => {
return Err(
"a write-permission rule must set either `folder_id` or `file_id`".to_string(),
)
}
(Some(_), Some(_)) => {
return Err(
"a write-permission rule must set `folder_id` or `file_id`, not both \
— a rule keys on one target"
.to_string(),
)
}
_ => {}
}
if raw.recursive && raw.file_id.is_some() {
return Err(
"`recursive` is meaningless on a `file_id` rule — a file has no descendants; \
drop it, or use `folder_id` to grant a whole subtree"
.to_string(),
);
}
Ok(Self {
folder_id: raw.folder_id,
file_id: raw.file_id,
recursive: raw.recursive,
allow: raw.allow,
deny: raw.deny,
require_lease: raw.require_lease,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum DecidingRule {
Folder {
folder_id: String,
depth: usize,
},
File {
file_id: String,
},
}
impl DecidingRule {
#[must_use]
pub fn id(&self) -> &str {
match self {
Self::Folder { folder_id, .. } => folder_id,
Self::File { file_id } => file_id,
}
}
#[must_use]
pub const fn kind_label(&self) -> &'static str {
match self {
Self::Folder { .. } => "folder",
Self::File { .. } => "file",
}
}
#[must_use]
pub fn depth_suffix(&self) -> String {
match self {
Self::Folder { depth, .. } => format!(" (depth {depth})"),
Self::File { .. } => String::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decision {
pub verdict: Verdict,
pub decided_by: Option<DecidingRule>,
}
#[must_use]
pub fn resolve_file_rule(
file_id: &str,
op: DriveOperation,
rules: &[FolderPermissionRule],
) -> Option<Decision> {
let mut deny_here = false;
let mut allow_here = false;
for rule in rules {
if rule.file_id.as_deref() != Some(file_id) {
continue;
}
deny_here |= rule.deny.contains(&op);
allow_here |= rule.allow.contains(&op);
}
if !deny_here && !allow_here {
return None;
}
Some(Decision {
verdict: if deny_here {
Verdict::Deny
} else {
Verdict::Allow
},
decided_by: Some(DecidingRule::File {
file_id: file_id.to_string(),
}),
})
}
#[must_use]
pub fn resolve_for_file(
file_id: &str,
chain: &[String],
op: DriveOperation,
rules: &[FolderPermissionRule],
) -> Decision {
resolve_file_rule(file_id, op, rules).unwrap_or_else(|| resolve(chain, op, rules))
}
#[must_use]
pub fn resolve(chain: &[String], op: DriveOperation, rules: &[FolderPermissionRule]) -> Decision {
let mut best: Option<(usize, Verdict)> = None;
for (depth, folder_id) in chain.iter().enumerate() {
let mut deny_here = false;
let mut allow_here = false;
for rule in rules {
if rule.folder_id.as_deref() != Some(folder_id.as_str()) {
continue;
}
if depth > 0 && !rule.recursive {
continue;
}
deny_here |= rule.deny.contains(&op);
allow_here |= rule.allow.contains(&op);
}
if !deny_here && !allow_here {
continue;
}
let verdict = if deny_here {
Verdict::Deny
} else {
Verdict::Allow
};
let is_closer = match best {
Some((best_depth, _)) => depth < best_depth,
None => true,
};
if is_closer {
best = Some((depth, verdict));
}
}
match best {
Some((depth, verdict)) => Decision {
verdict,
decided_by: Some(DecidingRule::Folder {
folder_id: chain[depth].clone(),
depth,
}),
},
None => Decision {
verdict: op.default_policy(),
decided_by: None,
},
}
}
#[must_use]
pub fn combine_across_parents(
first: Decision,
rest: impl IntoIterator<Item = Decision>,
) -> Decision {
rest.into_iter().fold(first, |acc, next| {
if acc.verdict == Verdict::Deny {
acc
} else {
next
}
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DecidedByLogFields {
pub folder_id: Option<String>,
pub file_id: Option<String>,
pub depth: Option<usize>,
}
#[must_use]
pub fn decided_by_log_fields(decided_by: Option<&DecidingRule>) -> DecidedByLogFields {
match decided_by {
Some(DecidingRule::Folder { folder_id, depth }) => DecidedByLogFields {
folder_id: Some(folder_id.clone()),
file_id: None,
depth: Some(*depth),
},
Some(DecidingRule::File { file_id }) => DecidedByLogFields {
folder_id: None,
file_id: Some(file_id.clone()),
depth: None,
},
None => DecidedByLogFields::default(),
}
}
#[must_use]
pub fn decided_rule_requires_lease(
decided_by: Option<&DecidingRule>,
op: DriveOperation,
rules: &[FolderPermissionRule],
) -> bool {
let governs_op =
|rule: &&FolderPermissionRule| rule.allow.contains(&op) || rule.deny.contains(&op);
let matching: Vec<&FolderPermissionRule> = match decided_by {
None => return true,
Some(DecidingRule::Folder { folder_id, depth }) => rules
.iter()
.filter(|rule| {
rule.folder_id.as_deref() == Some(folder_id.as_str())
&& (*depth == 0 || rule.recursive)
})
.filter(governs_op)
.collect(),
Some(DecidingRule::File { file_id }) => rules
.iter()
.filter(|rule| rule.file_id.as_deref() == Some(file_id.as_str()))
.filter(governs_op)
.collect(),
};
if matching.is_empty() {
return true;
}
matching.iter().any(|rule| rule.require_lease)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn rule(
folder_id: &str,
recursive: bool,
allow: &[DriveOperation],
deny: &[DriveOperation],
) -> FolderPermissionRule {
FolderPermissionRule::folder(folder_id)
.recursive(recursive)
.allowing(allow.iter().copied())
.denying(deny.iter().copied())
}
fn file_rule(
file_id: &str,
allow: &[DriveOperation],
deny: &[DriveOperation],
) -> FolderPermissionRule {
FolderPermissionRule::file(file_id)
.allowing(allow.iter().copied())
.denying(deny.iter().copied())
}
fn folder_id_of(decision: &Decision) -> &str {
match decision.decided_by.as_ref().unwrap() {
DecidingRule::Folder { folder_id, .. } => folder_id,
DecidingRule::File { .. } => panic!("expected a folder rule, got a file rule"),
}
}
fn chain(ids: &[&str]) -> Vec<String> {
ids.iter().copied().map(ToString::to_string).collect()
}
#[test]
fn default_policy_allows_read_with_no_rules() {
let decision = resolve(&chain(&["a"]), DriveOperation::Read, &[]);
assert_eq!(decision.verdict, Verdict::Allow);
assert_eq!(decision.decided_by, None);
}
#[test]
fn default_policy_denies_create_upload_edit_trash_with_no_rules() {
for op in [
DriveOperation::Create,
DriveOperation::Upload,
DriveOperation::Edit,
DriveOperation::Trash,
] {
let decision = resolve(&chain(&["a"]), op, &[]);
assert_eq!(
decision.verdict,
Verdict::Deny,
"{op:?} should default-deny"
);
assert_eq!(decision.decided_by, None);
}
}
#[test]
fn ever_requires_lease_is_false_for_read_create_upload_and_trash() {
for op in [
DriveOperation::Read,
DriveOperation::Create,
DriveOperation::Upload,
DriveOperation::Trash,
] {
assert!(
!op.ever_requires_lease(),
"{op:?} should never take a lease"
);
}
}
#[test]
fn ever_requires_lease_is_true_for_every_content_mutating_operation() {
for op in [
DriveOperation::Edit,
DriveOperation::SheetsWrite,
DriveOperation::SheetsStructure,
DriveOperation::SheetsDelete,
DriveOperation::SheetsProtection,
DriveOperation::DocsWrite,
DriveOperation::DocsDelete,
DriveOperation::DocsStructure,
DriveOperation::DocsTableDelete,
] {
assert!(op.ever_requires_lease(), "{op:?} should be lease-eligible");
}
}
#[test]
fn trash_permission_serde_round_trip_does_not_widen_other_grants() {
let rule: FolderPermissionRule = serde_json::from_value(serde_json::json!({
"folder_id": "a", "allow": ["trash"]
}))
.unwrap();
assert!(rule.allow.contains(&DriveOperation::Trash));
assert_eq!(DriveOperation::Trash.to_string(), "trash");
let serialized = serde_json::to_value(&rule).unwrap();
assert_eq!(serialized["allow"], serde_json::json!(["trash"]));
for op in [
DriveOperation::Create,
DriveOperation::Edit,
DriveOperation::SheetsDelete,
] {
let rule = FolderPermissionRule::folder("a").allowing([op]);
assert_eq!(
resolve(&chain(&["a"]), DriveOperation::Trash, &[rule]).verdict,
Verdict::Deny
);
}
}
#[test]
fn recursive_rule_matches_deep_descendant() {
let rules = [rule("root", true, &[DriveOperation::Create], &[])];
let decision = resolve(
&chain(&["child", "grandchild", "root"]),
DriveOperation::Create,
&rules,
);
assert_eq!(decision.verdict, Verdict::Allow);
assert_eq!(folder_id_of(&decision), "root");
}
#[test]
fn non_recursive_rule_matches_own_folder_only() {
let rules = [rule("target", false, &[DriveOperation::Create], &[])];
let decision = resolve(&chain(&["target"]), DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Allow);
}
#[test]
fn non_recursive_rule_does_not_match_child() {
let rules = [rule("parent", false, &[DriveOperation::Create], &[])];
let decision = resolve(&chain(&["child", "parent"]), DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(decision.decided_by, None);
}
#[test]
fn closest_ancestor_wins_deny_over_broader_allow() {
let rules = [
rule("child", true, &[], &[DriveOperation::Create]),
rule("parent", true, &[DriveOperation::Create], &[]),
];
let decision = resolve(&chain(&["child", "parent"]), DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(folder_id_of(&decision), "child");
}
#[test]
fn closest_ancestor_wins_allow_over_broader_deny() {
let rules = [
rule("child", true, &[DriveOperation::Create], &[]),
rule("parent", true, &[], &[DriveOperation::Create]),
];
let decision = resolve(&chain(&["child", "parent"]), DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Allow);
assert_eq!(folder_id_of(&decision), "child");
}
#[test]
fn deny_beats_allow_at_equal_depth() {
let rules = [
rule("target", false, &[DriveOperation::Create], &[]),
rule("target", false, &[], &[DriveOperation::Create]),
];
let decision = resolve(&chain(&["target"]), DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Deny);
}
#[test]
fn rule_on_unrelated_folder_does_not_apply() {
let rules = [rule("unrelated", true, &[DriveOperation::Create], &[])];
let decision = resolve(
&chain(&["target", "parent"]),
DriveOperation::Create,
&rules,
);
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(decision.decided_by, None);
}
#[test]
fn empty_chain_orphan_file_uses_default_policy_only() {
let rules = [rule("some-folder", true, &[DriveOperation::Create], &[])];
let decision = resolve(&[], DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(decision.decided_by, None);
}
#[test]
fn display_matches_the_serde_kebab_case_wire_form() {
for op in [
DriveOperation::Read,
DriveOperation::Create,
DriveOperation::Upload,
DriveOperation::Edit,
DriveOperation::SheetsWrite,
DriveOperation::SheetsStructure,
DriveOperation::SheetsDelete,
DriveOperation::SheetsProtection,
DriveOperation::DocsWrite,
DriveOperation::Trash,
DriveOperation::DocsDelete,
DriveOperation::DocsStructure,
DriveOperation::DocsTableDelete,
] {
let wire = serde_json::to_string(&op).unwrap();
assert_eq!(
format!("\"{op}\""),
wire,
"Display drifted from serde for {op:?}"
);
}
assert_eq!(DriveOperation::Read.to_string(), "read");
assert_eq!(DriveOperation::Create.to_string(), "create");
assert_eq!(DriveOperation::Upload.to_string(), "upload");
assert_eq!(DriveOperation::Edit.to_string(), "edit");
assert_eq!(DriveOperation::SheetsWrite.to_string(), "sheets-write");
assert_eq!(
DriveOperation::SheetsStructure.to_string(),
"sheets-structure"
);
assert_eq!(DriveOperation::SheetsDelete.to_string(), "sheets-delete");
assert_eq!(
DriveOperation::SheetsProtection.to_string(),
"sheets-protection"
);
assert_eq!(DriveOperation::DocsWrite.to_string(), "docs-write");
}
#[test]
fn sheets_write_defaults_to_deny_like_every_other_write() {
let decision = resolve(&chain(&["f"]), DriveOperation::SheetsWrite, &[]);
assert_eq!(decision.verdict, Verdict::Deny);
assert!(decision.decided_by.is_none());
}
#[test]
fn an_edit_rule_does_not_grant_sheets_write() {
let rules = [rule("target", true, &[DriveOperation::Edit], &[])];
let edit = resolve(&chain(&["target"]), DriveOperation::Edit, &rules);
let sheets = resolve(&chain(&["target"]), DriveOperation::SheetsWrite, &rules);
assert_eq!(edit.verdict, Verdict::Allow);
assert_eq!(sheets.verdict, Verdict::Deny);
}
#[test]
fn sheets_structure_defaults_to_deny_like_every_other_write() {
let decision = resolve(&chain(&["f"]), DriveOperation::SheetsStructure, &[]);
assert_eq!(decision.verdict, Verdict::Deny);
assert!(decision.decided_by.is_none());
}
#[test]
fn slides_write_is_a_distinct_default_deny_leased_operation() {
assert_eq!(DriveOperation::SlidesWrite.to_string(), "slides-write");
assert!(DriveOperation::SlidesWrite.ever_requires_lease());
assert_eq!(
serde_json::to_string(&DriveOperation::SlidesWrite).unwrap(),
"\"slides-write\""
);
assert_eq!(
resolve(&chain(&["target"]), DriveOperation::SlidesWrite, &[]).verdict,
Verdict::Deny
);
for granted in [
DriveOperation::Edit,
DriveOperation::DocsWrite,
DriveOperation::SheetsWrite,
] {
let rules = [rule("target", true, &[granted], &[])];
assert_eq!(
resolve(&chain(&["target"]), DriveOperation::SlidesWrite, &rules).verdict,
Verdict::Deny
);
}
let rules = [rule("target", true, &[DriveOperation::SlidesWrite], &[])];
assert_eq!(
resolve(&chain(&["target"]), DriveOperation::SlidesWrite, &rules).verdict,
Verdict::Allow
);
for other in [
DriveOperation::Edit,
DriveOperation::DocsWrite,
DriveOperation::SheetsWrite,
] {
assert_eq!(
resolve(&chain(&["target"]), other, &rules).verdict,
Verdict::Deny
);
}
}
#[test]
fn docs_write_defaults_to_deny_like_every_other_write() {
let decision = resolve(&chain(&["f"]), DriveOperation::DocsWrite, &[]);
assert_eq!(decision.verdict, Verdict::Deny);
assert!(decision.decided_by.is_none());
}
#[test]
fn a_sheets_write_rule_does_not_grant_sheets_structure() {
let rules = [rule("target", true, &[DriveOperation::SheetsWrite], &[])];
let write = resolve(&chain(&["target"]), DriveOperation::SheetsWrite, &rules);
let structure = resolve(&chain(&["target"]), DriveOperation::SheetsStructure, &rules);
assert_eq!(write.verdict, Verdict::Allow);
assert_eq!(structure.verdict, Verdict::Deny);
}
#[test]
fn an_edit_rule_does_not_grant_sheets_structure() {
let rules = [rule("target", true, &[DriveOperation::Edit], &[])];
let structure = resolve(&chain(&["target"]), DriveOperation::SheetsStructure, &rules);
assert_eq!(structure.verdict, Verdict::Deny);
}
#[test]
fn neither_an_edit_nor_a_sheets_write_nor_a_sheets_structure_rule_grants_docs_write() {
for granted in [
DriveOperation::Edit,
DriveOperation::SheetsWrite,
DriveOperation::SheetsStructure,
] {
let rules = [rule("target", true, &[granted], &[])];
let held = resolve(&chain(&["target"]), granted, &rules);
let docs = resolve(&chain(&["target"]), DriveOperation::DocsWrite, &rules);
assert_eq!(held.verdict, Verdict::Allow, "{granted:?}");
assert_eq!(
docs.verdict,
Verdict::Deny,
"an allow:[{granted}] rule must not leak into docs-write"
);
}
}
#[test]
fn a_docs_write_rule_grants_nothing_else() {
let rules = [rule("target", true, &[DriveOperation::DocsWrite], &[])];
assert_eq!(
resolve(&chain(&["target"]), DriveOperation::DocsWrite, &rules).verdict,
Verdict::Allow
);
for other in [
DriveOperation::Edit,
DriveOperation::SheetsWrite,
DriveOperation::SheetsStructure,
DriveOperation::Create,
DriveOperation::Upload,
DriveOperation::Trash,
] {
assert_eq!(
resolve(&chain(&["target"]), other, &rules).verdict,
Verdict::Deny,
"an allow:[docs-write] rule must not leak into {other}"
);
}
}
#[test]
fn operations_on_one_rule_are_independent() {
let rules = [rule("target", false, &[DriveOperation::Create], &[])];
let create = resolve(&chain(&["target"]), DriveOperation::Create, &rules);
let upload = resolve(&chain(&["target"]), DriveOperation::Upload, &rules);
assert_eq!(create.verdict, Verdict::Allow);
assert_eq!(
upload.verdict,
Verdict::Deny,
"an allow:[create] rule must not leak into upload"
);
}
#[test]
fn deny_list_and_allow_list_on_same_rule_apply_to_different_ops_independently() {
let rules = [rule(
"target",
false,
&[DriveOperation::Create],
&[DriveOperation::Edit],
)];
let create = resolve(&chain(&["target"]), DriveOperation::Create, &rules);
let edit = resolve(&chain(&["target"]), DriveOperation::Edit, &rules);
let upload = resolve(&chain(&["target"]), DriveOperation::Upload, &rules);
assert_eq!(create.verdict, Verdict::Allow);
assert_eq!(edit.verdict, Verdict::Deny);
assert_eq!(
upload.verdict,
Verdict::Deny,
"no rule named upload; falls to default policy"
);
}
fn decision(verdict: Verdict) -> Decision {
Decision {
verdict,
decided_by: None,
}
}
#[test]
fn combine_across_parents_single_decision_returns_it_unchanged() {
let combined = combine_across_parents(decision(Verdict::Allow), []);
assert_eq!(combined.verdict, Verdict::Allow);
}
#[test]
fn combine_across_parents_deny_beats_allow_deny_first() {
let combined = combine_across_parents(decision(Verdict::Deny), [decision(Verdict::Allow)]);
assert_eq!(combined.verdict, Verdict::Deny);
}
#[test]
fn combine_across_parents_deny_beats_allow_allow_first() {
let combined = combine_across_parents(decision(Verdict::Allow), [decision(Verdict::Deny)]);
assert_eq!(combined.verdict, Verdict::Deny);
}
#[test]
fn decided_by_log_fields_none_yields_all_none() {
assert_eq!(decided_by_log_fields(None), DecidedByLogFields::default());
}
#[test]
fn decided_by_log_fields_some_extracts_folder_id_and_depth() {
let rule = DecidingRule::Folder {
folder_id: "folder-1".to_string(),
depth: 2,
};
assert_eq!(
decided_by_log_fields(Some(&rule)),
DecidedByLogFields {
folder_id: Some("folder-1".to_string()),
file_id: None,
depth: Some(2),
}
);
}
#[test]
fn decided_by_log_fields_never_puts_a_file_id_in_the_folder_field() {
let rule = DecidingRule::File {
file_id: "file-1".to_string(),
};
assert_eq!(
decided_by_log_fields(Some(&rule)),
DecidedByLogFields {
folder_id: None,
file_id: Some("file-1".to_string()),
depth: None,
}
);
}
#[test]
fn decided_rule_requires_lease_defaults_true_with_no_decided_by() {
assert!(decided_rule_requires_lease(None, DriveOperation::Edit, &[]));
}
#[test]
fn decided_rule_requires_lease_true_when_the_folder_rule_says_so() {
let rules = [FolderPermissionRule::folder("f1").allowing([DriveOperation::Edit])];
let decided_by = DecidingRule::Folder {
folder_id: "f1".to_string(),
depth: 0,
};
assert!(decided_rule_requires_lease(
Some(&decided_by),
DriveOperation::Edit,
&rules
));
}
#[test]
fn decided_rule_requires_lease_false_when_the_folder_rule_opts_out() {
let rules = [FolderPermissionRule::folder("f1")
.allowing([DriveOperation::Edit])
.requiring_lease(false)];
let decided_by = DecidingRule::Folder {
folder_id: "f1".to_string(),
depth: 0,
};
assert!(!decided_rule_requires_lease(
Some(&decided_by),
DriveOperation::Edit,
&rules
));
}
#[test]
fn decided_rule_requires_lease_true_when_the_file_rule_says_so() {
let rules = [FolderPermissionRule::file("file-1").allowing([DriveOperation::Edit])];
let decided_by = DecidingRule::File {
file_id: "file-1".to_string(),
};
assert!(decided_rule_requires_lease(
Some(&decided_by),
DriveOperation::Edit,
&rules
));
}
#[test]
fn decided_rule_requires_lease_any_matching_rule_opting_in_wins() {
let rules = [
FolderPermissionRule::folder("f1")
.allowing([DriveOperation::Edit])
.requiring_lease(false),
FolderPermissionRule::folder("f1").allowing([DriveOperation::Edit]),
];
let decided_by = DecidingRule::Folder {
folder_id: "f1".to_string(),
depth: 0,
};
assert!(decided_rule_requires_lease(
Some(&decided_by),
DriveOperation::Edit,
&rules
));
}
#[test]
fn decided_rule_requires_lease_ignores_a_non_recursive_rule_on_an_ancestor() {
let rules = [FolderPermissionRule::folder("f1")
.allowing([DriveOperation::Edit])
.requiring_lease(false)];
let decided_by = DecidingRule::Folder {
folder_id: "f1".to_string(),
depth: 1,
};
assert!(decided_rule_requires_lease(
Some(&decided_by),
DriveOperation::Edit,
&rules
));
}
#[test]
fn decided_rule_requires_lease_ignores_a_rule_for_a_different_operation() {
let rules = [
FolderPermissionRule::folder("f1")
.allowing([DriveOperation::Edit])
.requiring_lease(false),
FolderPermissionRule::folder("f1").allowing([DriveOperation::SheetsWrite]),
];
let decided_by = DecidingRule::Folder {
folder_id: "f1".to_string(),
depth: 0,
};
assert!(!decided_rule_requires_lease(
Some(&decided_by),
DriveOperation::Edit,
&rules
));
}
#[test]
fn combine_across_parents_all_allow_returns_allow() {
let combined = combine_across_parents(
decision(Verdict::Allow),
[decision(Verdict::Allow), decision(Verdict::Allow)],
);
assert_eq!(combined.verdict, Verdict::Allow);
}
#[test]
fn a_file_rule_grants_a_target_with_no_chain_at_all() {
let rules = [file_rule("shared", &[DriveOperation::SheetsWrite], &[])];
let decision = resolve_for_file("shared", &[], DriveOperation::SheetsWrite, &rules);
assert_eq!(decision.verdict, Verdict::Allow);
assert_eq!(
decision.decided_by,
Some(DecidingRule::File {
file_id: "shared".to_string()
})
);
}
#[test]
fn file_deny_beats_a_recursive_folder_allow() {
let rules = [
rule("root", true, &[DriveOperation::Edit], &[]),
file_rule("target", &[], &[DriveOperation::Edit]),
];
let decision = resolve_for_file(
"target",
&chain(&["parent", "root"]),
DriveOperation::Edit,
&rules,
);
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(decision.decided_by.unwrap().kind_label(), "file");
}
#[test]
fn file_allow_beats_a_folder_deny_on_the_immediate_parent() {
let rules = [
rule("parent", false, &[], &[DriveOperation::SheetsWrite]),
file_rule("target", &[DriveOperation::SheetsWrite], &[]),
];
let decision = resolve_for_file(
"target",
&chain(&["parent"]),
DriveOperation::SheetsWrite,
&rules,
);
assert_eq!(decision.verdict, Verdict::Allow);
}
#[test]
fn deny_beats_allow_among_file_rules_for_the_same_id() {
let rules = [
file_rule("target", &[DriveOperation::Edit], &[]),
file_rule("target", &[], &[DriveOperation::Edit]),
];
let decision = resolve_for_file("target", &[], DriveOperation::Edit, &rules);
assert_eq!(decision.verdict, Verdict::Deny);
}
#[test]
fn resolve_file_rule_returns_none_rather_than_the_default_policy() {
let rules = [file_rule("target", &[DriveOperation::Edit], &[])];
assert!(resolve_file_rule("target", DriveOperation::SheetsWrite, &rules).is_none());
assert!(resolve_file_rule("other", DriveOperation::Edit, &rules).is_none());
}
#[test]
fn a_file_rule_is_invisible_to_the_ancestor_walk() {
let rules = [file_rule("target", &[DriveOperation::Create], &[])];
let decision = resolve(&chain(&["target"]), DriveOperation::Create, &rules);
assert_eq!(decision.verdict, Verdict::Deny);
assert_eq!(decision.decided_by, None);
}
#[test]
fn a_folder_rule_is_invisible_to_the_file_lookup() {
let rules = [rule("target", true, &[DriveOperation::Create], &[])];
assert!(resolve_file_rule("target", DriveOperation::Create, &rules).is_none());
}
#[test]
fn resolve_for_file_falls_through_to_the_chain_when_no_file_rule_matches() {
let rules = [rule("parent", true, &[DriveOperation::Edit], &[])];
let decision =
resolve_for_file("target", &chain(&["parent"]), DriveOperation::Edit, &rules);
assert_eq!(decision.verdict, Verdict::Allow);
assert_eq!(folder_id_of(&decision), "parent");
}
fn parse_rule(json: &str) -> Result<FolderPermissionRule, serde_json::Error> {
serde_json::from_str(json)
}
#[test]
fn a_folder_rule_round_trips_byte_identically_to_the_pre_1612_shape() {
let parsed =
parse_rule(r#"{"folder_id":"f1","recursive":true,"allow":["create"]}"#).unwrap();
assert_eq!(parsed.folder_id.as_deref(), Some("f1"));
assert_eq!(parsed.file_id, None);
assert!(parsed.recursive);
let json: serde_json::Value = serde_json::to_value(&parsed).unwrap();
assert_eq!(json["folder_id"], "f1");
assert_eq!(json["recursive"], true);
assert!(
json.get("file_id").is_none(),
"an absent file_id must not be serialized"
);
}
#[test]
fn a_pre_lease_rule_with_no_require_lease_key_defaults_to_requiring_one() {
let parsed =
parse_rule(r#"{"folder_id":"f1","recursive":true,"allow":["create"]}"#).unwrap();
assert!(parsed.require_lease);
}
#[test]
fn a_file_rule_parses_and_serializes_without_a_folder_id() {
let parsed = parse_rule(r#"{"file_id":"x1","allow":["sheets-write"]}"#).unwrap();
assert_eq!(parsed.file_id.as_deref(), Some("x1"));
assert_eq!(parsed.folder_id, None);
let json: serde_json::Value = serde_json::to_value(&parsed).unwrap();
assert_eq!(json["file_id"], "x1");
assert!(json.get("folder_id").is_none());
}
#[test]
fn a_rule_naming_neither_target_is_a_load_error() {
let err = parse_rule(r#"{"allow":["create"]}"#)
.unwrap_err()
.to_string();
assert!(err.contains("folder_id"), "{err}");
assert!(err.contains("file_id"), "{err}");
}
#[test]
fn a_rule_naming_both_targets_is_a_load_error() {
let err = parse_rule(r#"{"folder_id":"f","file_id":"x"}"#)
.unwrap_err()
.to_string();
assert!(err.contains("not both"), "{err}");
}
#[test]
fn recursive_true_on_a_file_rule_is_a_load_error() {
let err = parse_rule(r#"{"file_id":"x","recursive":true}"#)
.unwrap_err()
.to_string();
assert!(err.contains("recursive"), "{err}");
}
#[test]
fn recursive_false_on_a_file_rule_is_accepted() {
let parsed = parse_rule(r#"{"file_id":"x","recursive":false}"#).unwrap();
assert!(!parsed.recursive);
}
#[test]
fn deciding_rule_accessors_render_both_kinds() {
let folder = DecidingRule::Folder {
folder_id: "f1".to_string(),
depth: 2,
};
assert_eq!(folder.id(), "f1");
assert_eq!(folder.kind_label(), "folder");
assert_eq!(folder.depth_suffix(), " (depth 2)");
let file = DecidingRule::File {
file_id: "x1".to_string(),
};
assert_eq!(file.id(), "x1");
assert_eq!(file.kind_label(), "file");
assert_eq!(file.depth_suffix(), "");
}
#[test]
fn docs_format_is_default_denied_lease_eligible_and_isolated() {
let format: DriveOperation = serde_json::from_str("\"docs-format\"").unwrap();
assert_eq!(format, DriveOperation::DocsFormat);
assert_eq!(format.to_string(), "docs-format");
assert_eq!(format.default_policy(), Verdict::Deny);
assert!(format.ever_requires_lease());
for other in [
DriveOperation::DocsWrite,
DriveOperation::DocsDelete,
DriveOperation::Edit,
DriveOperation::SheetsWrite,
DriveOperation::SlidesWrite,
] {
assert_eq!(
resolve(
&chain(&["target"]),
format,
&[rule("target", true, &[other], &[])]
)
.verdict,
Verdict::Deny
);
assert_eq!(
resolve(
&chain(&["target"]),
other,
&[rule("target", true, &[format], &[])]
)
.verdict,
Verdict::Deny
);
}
assert_eq!(
resolve(
&chain(&["target"]),
format,
&[rule("target", true, &[format], &[])]
)
.verdict,
Verdict::Allow
);
}
}