1use serde_json::{Map, Value};
8use sha2::{Digest, Sha256};
9
10use super::{Policy, Refusal, Request};
11use crate::record::procedure_org;
12
13#[derive(Debug, PartialEq, Eq)]
16enum Grant<'a> {
17 Realm(&'a str),
18 Org(&'a str, &'a str),
19 Proc(&'a str, &'a str),
20}
21
22impl<'a> Grant<'a> {
23 fn realm(&self) -> &'a str {
24 match self {
25 Grant::Realm(realm) | Grant::Org(realm, _) | Grant::Proc(realm, _) => realm,
26 }
27 }
28}
29
30fn grant(with: &str) -> Result<Grant<'_>, Refusal> {
32 let canonical = |realm| match canonical_realm_name(realm) {
33 true => Ok(()),
34 false => Err(Refusal::RealmNameNotCanonical),
35 };
36 if let Some(realm) = with.strip_prefix("mri:realm:") {
37 canonical(realm)?;
38 return Ok(Grant::Realm(realm));
39 }
40 if let Some(rest) = with.strip_prefix("mri:org:") {
41 let (realm, org) = rest.split_once('/').ok_or(Refusal::Malformed)?;
42 if org.is_empty() {
43 return Err(Refusal::Malformed);
44 }
45 canonical(realm)?;
46 return Ok(Grant::Org(realm, org));
47 }
48 if let Some(rest) = with.strip_prefix("mri:proc:") {
49 let (realm, procedure) = rest.split_once('/').ok_or(Refusal::Malformed)?;
50 canonical(realm)?;
51 org_of(procedure)?;
52 return Ok(Grant::Proc(realm, procedure));
53 }
54 Err(Refusal::Malformed)
55}
56
57fn org_of(procedure: &str) -> Result<&str, Refusal> {
59 match procedure_org(procedure) {
60 Ok(Some(org)) => Ok(org),
61 _ => Err(Refusal::ProcedureWithoutOrg),
62 }
63}
64
65pub fn covers(parent: &str, child: &str) -> bool {
71 let (Ok(p), Ok(c)) = (grant(parent), grant(child)) else {
72 return false;
73 };
74 match (p, c) {
75 (Grant::Realm(realm), c) => c.realm() == realm,
76 (Grant::Org(realm, org), Grant::Org(c_realm, c_org)) => c_realm == realm && c_org == org,
77 (Grant::Org(realm, org), Grant::Proc(c_realm, procedure)) => {
78 c_realm == realm && org_of(procedure) == Ok(org)
79 }
80 (Grant::Proc(realm, procedure), Grant::Proc(c_realm, c_procedure)) => {
81 c_realm == realm && c_procedure == procedure
82 }
83 _ => false,
84 }
85}
86
87fn canonical_realm_name(name: &str) -> bool {
90 !name.is_empty()
91 && name.split('.').all(|segment| {
92 !segment.is_empty()
93 && segment
94 .bytes()
95 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
96 })
97}
98
99fn realm_id(realm: &str) -> [u8; 32] {
100 Sha256::digest(realm.as_bytes()).into()
101}
102
103fn with_and_can(entry: &Value) -> Option<(&str, &Value)> {
106 let cap = entry.as_object()?;
107 Some((cap.get("with")?.as_str()?, cap.get("can")?))
108}
109
110pub(super) fn grants(
116 p: &Policy,
117 request: Option<&Request>,
118 claims: &Map<String, Value>,
119) -> Result<Vec<Value>, Refusal> {
120 let caps = claims
121 .get("cap")
122 .and_then(Value::as_array)
123 .map(Vec::as_slice)
124 .unwrap_or(&[]);
125 if let Some(r) = request {
126 return requested(p, r, caps);
127 }
128 match p {
129 Policy::UcanRequired { .. } => Ok(caps.to_vec()),
130 Policy::RealmMemberRequired { can, .. } => caps
131 .iter()
132 .find(|entry| {
133 entry.as_object().and_then(|c| c.get("can")) == Some(&Value::String(can.clone()))
134 })
135 .map(|cap| vec![cap.clone()])
136 .ok_or_else(|| why_not_granted(caps, None)),
137 }
138}
139
140fn requested(p: &Policy, r: &Request, caps: &[Value]) -> Result<Vec<Value>, Refusal> {
141 org_of(&r.procedure)?;
142 let answers = |entry: &Value| {
143 let Some((with, can)) = with_and_can(entry) else {
144 return false;
145 };
146 let can_matches = match p {
147 Policy::RealmMemberRequired { can: wanted, .. } => {
148 can == &Value::String(wanted.clone())
149 }
150 Policy::UcanRequired { .. } => true,
151 };
152 let Ok(g) = grant(with) else {
153 return false;
154 };
155 can_matches
156 && realm_id(g.realm()) == r.realm
157 && covers(with, &format!("mri:proc:{}/{}", g.realm(), r.procedure))
158 };
159 caps.iter()
160 .find(|entry| answers(entry))
161 .map(|cap| vec![cap.clone()])
162 .ok_or_else(|| why_not_granted(caps, Some(&r.realm)))
163}
164
165fn why_not_granted(caps: &[Value], realm: Option<&[u8; 32]>) -> Refusal {
169 let first = caps
170 .iter()
171 .find_map(|entry| entry.as_object()?.get("with")?.as_str());
172 let Some(with) = first else {
173 return Refusal::MissingCapability;
174 };
175 match (grant(with), realm) {
176 (Err(Refusal::Malformed), _) => Refusal::MissingCapability,
177 (Err(refusal), _) => refusal,
178 (Ok(_), None) => Refusal::MissingCapability,
179 (Ok(g), Some(realm)) if realm_id(g.realm()) == *realm => Refusal::MissingCapability,
180 (Ok(_), Some(_)) => Refusal::WrongRealm,
181 }
182}
183
184pub(super) fn narrowed(parent_caps: &[Value], children: &[Value]) -> Result<Vec<Value>, Refusal> {
188 children
189 .iter()
190 .map(|child| covering(parent_caps, child))
191 .collect()
192}
193
194fn covering(caps: &[Value], child: &Value) -> Result<Value, Refusal> {
195 let child_cap = child.as_object().ok_or(Refusal::Malformed)?;
196 let (Some(child_with), Some(child_can)) = (child_cap.get("with"), child_cap.get("can")) else {
197 return Err(Refusal::Malformed);
198 };
199 let found = caps
200 .iter()
201 .find(|entry| match (with_and_can(entry), child_with.as_str()) {
202 (Some((with, can)), Some(child_with)) => can == child_can && covers(with, child_with),
203 _ => false,
204 });
205 found
206 .cloned()
207 .ok_or_else(|| why_not_narrowed(caps, child_with))
208}
209
210fn why_not_narrowed(caps: &[Value], child_with: &Value) -> Refusal {
214 let Some((with, _)) = caps.first().and_then(with_and_can) else {
215 return Refusal::GrantsMoreThanProof;
216 };
217 match child_with.as_str() {
218 Some(child) if same_realm(with, child) && covers(with, child) => Refusal::CanChanged,
219 Some(child) if same_realm(with, child) => Refusal::GrantsMoreThanProof,
220 _ => Refusal::WrongRealm,
221 }
222}
223
224fn same_realm(with: &str, other: &str) -> bool {
225 matches!((grant(with), grant(other)), (Ok(a), Ok(b)) if a.realm() == b.realm())
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn a_realm_name_is_dotted_lowercase_segments() {
234 for good in ["io.macula", "a", "a-b_c.0"] {
235 assert!(canonical_realm_name(good), "{good}");
236 }
237 for bad in [
238 "",
239 ".",
240 "io..macula",
241 "io.macula.",
242 "IO.macula",
243 "io macula",
244 "é",
245 ] {
246 assert!(!canonical_realm_name(bad), "{bad}");
247 }
248 }
249
250 #[test]
251 fn a_with_is_one_of_three_mris() {
252 assert_eq!(grant("mri:realm:io.macula"), Ok(Grant::Realm("io.macula")));
253 assert_eq!(
254 grant("mri:org:io.macula/acme"),
255 Ok(Grant::Org("io.macula", "acme"))
256 );
257 assert_eq!(
258 grant("mri:proc:io.macula/acme/count_v1"),
259 Ok(Grant::Proc("io.macula", "acme/count_v1"))
260 );
261 assert_eq!(grant("mri:org:io.macula/"), Err(Refusal::Malformed));
262 assert_eq!(grant("mri:org:io.macula"), Err(Refusal::Malformed));
263 assert_eq!(grant("mri:realm:IO"), Err(Refusal::RealmNameNotCanonical));
264 assert_eq!(
265 grant("mri:proc:io.macula/count"),
266 Err(Refusal::ProcedureWithoutOrg)
267 );
268 assert_eq!(grant("https://x"), Err(Refusal::Malformed));
269 }
270}