1use super::package::PackageIssue;
7use serde_json::Value;
8
9pub const RECORD_POLICY_OPS: &[&str] = &["read", "create", "update", "delete"];
10pub const RECORD_POLICY_LEVELS: &[&str] = &["member", "author", "owners"];
11pub const APP_ACCESS_LEVELS: &[&str] = &["none", "read", "write"];
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum RecordOp {
16 Read,
17 Create,
18 Update,
19 Delete,
20}
21
22impl RecordOp {
23 pub fn parse(s: &str) -> Option<Self> {
24 Some(match s {
25 "read" => Self::Read,
26 "create" => Self::Create,
27 "update" => Self::Update,
28 "delete" => Self::Delete,
29 _ => return None,
30 })
31 }
32 pub fn as_str(self) -> &'static str {
33 match self {
34 Self::Read => "read",
35 Self::Create => "create",
36 Self::Update => "update",
37 Self::Delete => "delete",
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct RecordPolicy {
45 pub read: &'static str,
46 pub create: &'static str,
47 pub update: &'static str,
48 pub delete: &'static str,
49}
50
51pub const DEFAULT_RECORD_POLICY: RecordPolicy = RecordPolicy { read: "member", create: "member", update: "author", delete: "author" };
52
53impl RecordPolicy {
54 pub fn level(&self, op: RecordOp) -> &'static str {
55 match op {
56 RecordOp::Read => self.read,
57 RecordOp::Create => self.create,
58 RecordOp::Update => self.update,
59 RecordOp::Delete => self.delete,
60 }
61 }
62 fn set(&mut self, op: RecordOp, level: &'static str) {
63 match op {
64 RecordOp::Read => self.read = level,
65 RecordOp::Create => self.create = level,
66 RecordOp::Update => self.update = level,
67 RecordOp::Delete => self.delete = level,
68 }
69 }
70 pub fn to_json(&self) -> Value {
71 serde_json::json!({"read": self.read, "create": self.create, "update": self.update, "delete": self.delete})
72 }
73}
74
75pub const COPY_NONE: &str = "You have no access to this app";
76pub const COPY_READ_ONLY: &str = "You have read-only access to this app";
77pub const COPY_AUTHOR_READ: &str = "Only the record's author or a team owner can read it";
78pub const COPY_AUTHOR_CHANGE: &str = "Only the record's author or a team owner can change it";
79pub const COPY_OWNERS_READ: &str = "Only team owners can read these records";
80pub const COPY_OWNERS_CREATE: &str = "Only team owners can add these records";
81pub const COPY_OWNERS_CHANGE: &str = "Only team owners can change these records";
82
83pub fn record_policy_copy() -> Value {
85 serde_json::json!({
86 "none": COPY_NONE,
87 "readOnly": COPY_READ_ONLY,
88 "author": {"read": COPY_AUTHOR_READ, "update": COPY_AUTHOR_CHANGE, "delete": COPY_AUTHOR_CHANGE},
89 "owners": {"read": COPY_OWNERS_READ, "create": COPY_OWNERS_CREATE, "update": COPY_OWNERS_CHANGE, "delete": COPY_OWNERS_CHANGE},
90 })
91}
92
93#[derive(Clone, Copy, Debug, Default)]
97pub struct RecordViewer<'a> {
98 pub id: Option<&'a str>,
99 pub role: Option<&'a str>,
100 pub access: Option<&'a str>,
101}
102
103fn level_of(v: &str) -> Option<&'static str> {
104 RECORD_POLICY_LEVELS.iter().copied().find(|l| *l == v)
105}
106
107pub fn validate_record_policy(policy: &Value, at: &str) -> Vec<PackageIssue> {
109 let Some(map) = policy.as_object() else {
110 return vec![PackageIssue { path: at.into(), message: "policy is {read?, create?, update?, delete?}".into() }];
111 };
112 let mut keys: Vec<&String> = map.keys().collect();
113 keys.sort_by(|a, b| super::utf16_cmp(a, b));
114 let mut issues = Vec::new();
115 for key in keys {
116 let value = &map[key];
117 let issue = |message: String| PackageIssue { path: format!("{at}/{key}"), message };
118 if RecordOp::parse(key).is_none() {
119 issues.push(issue(format!("unknown policy op {key}: read, create, update or delete")));
120 } else if key == "create" && value.as_str() == Some("author") {
121 issues.push(issue("create is member or owners: a new record has no author yet".into()));
122 } else if value.as_str().and_then(level_of).is_none() {
123 issues.push(issue(format!("{key} is member, author or owners")));
124 }
125 }
126 issues
127}
128
129pub fn record_policy(kind: Option<&Value>) -> RecordPolicy {
132 let mut out = DEFAULT_RECORD_POLICY;
133 let Some(policy) = kind.and_then(|k| k.get("policy")).and_then(Value::as_object) else { return out };
134 for name in RECORD_POLICY_OPS {
135 let op = RecordOp::parse(name).unwrap();
136 if let Some(level) = policy.get(*name).and_then(Value::as_str).and_then(level_of) {
137 if !(op == RecordOp::Create && level == "author") {
138 out.set(op, level);
139 }
140 }
141 }
142 out
143}
144
145pub fn record_author_of(row: Option<&Value>) -> Option<&str> {
147 let row = row?;
148 row.get("authorId").and_then(Value::as_str).or_else(|| row.get("createdBy").and_then(Value::as_str))
149}
150
151pub fn record_refusal(policy: &RecordPolicy, op: RecordOp, viewer: &RecordViewer, row: Option<&Value>) -> Option<&'static str> {
154 let access = viewer.access.unwrap_or("write");
155 if viewer.id.is_none() || viewer.role.is_none() || access == "none" {
156 return Some(COPY_NONE);
157 }
158 if access == "read" && op != RecordOp::Read {
159 return Some(COPY_READ_ONLY);
160 }
161 if viewer.role == Some("owner") {
162 return None;
163 }
164 let level = policy.level(op);
165 if level == "member" {
166 return None;
167 }
168 if level == "owners" || op == RecordOp::Create {
169 return Some(match op {
170 RecordOp::Read => COPY_OWNERS_READ,
171 RecordOp::Create => COPY_OWNERS_CREATE,
172 _ => COPY_OWNERS_CHANGE,
173 });
174 }
175 match record_author_of(row) {
176 Some(author) if Some(author) == viewer.id => None,
177 _ => Some(if op == RecordOp::Read { COPY_AUTHOR_READ } else { COPY_AUTHOR_CHANGE }),
178 }
179}
180
181pub fn record_allowed(policy: &RecordPolicy, op: RecordOp, viewer: &RecordViewer, row: Option<&Value>) -> bool {
183 record_refusal(policy, op, viewer, row).is_none()
184}
185
186pub fn readable_rows<'r>(rows: &'r [Value], kinds: Option<&Value>, viewer: &RecordViewer) -> Vec<&'r Value> {
189 rows.iter()
190 .filter(|r| {
191 let kind = r.get("kind").and_then(Value::as_str).unwrap_or("");
192 record_allowed(&record_policy(kinds.and_then(|k| k.get(kind))), RecordOp::Read, viewer, Some(r))
193 })
194 .collect()
195}