spacedb_access/
authorize.rs1use serde::{Deserialize, Serialize};
13
14use crate::capability::{Capability, Ops, Scope, SignedCapability};
15use crate::chain::CapabilityChain;
16use crate::directory::KeyDirectory;
17use crate::error::AccessResult;
18use crate::identity::{verify_sec1, Did};
19use crate::revocation::RevocationSet;
20
21pub struct AccessRequest<'a> {
23 pub bearer: &'a Did,
24 pub scope: &'a Scope,
25 pub op: Ops,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub enum DelegationError {
31 IssuerNotDelegator,
33 ParentNotDelegable,
35 DepthExceeded,
37 ScopeEscalation,
39 OpsEscalation,
41 ExpiryExtension,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47pub enum DenyReason {
48 UnknownIssuer,
49 BadSignature,
50 BearerMismatch,
51 OutOfScope,
52 OpNotGranted,
53 Expired,
54 Revoked,
56 Delegation(DelegationError),
58 EmptyChain,
60 NoCapability,
63 NotAccountable,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum Decision {
71 Allow,
72 Deny(DenyReason),
73}
74
75impl Decision {
76 pub fn is_allowed(&self) -> bool {
77 matches!(self, Decision::Allow)
78 }
79}
80
81enum LinkSig {
82 Ok,
83 UnknownIssuer,
84 BadSignature,
85}
86
87fn verify_link_signature(
88 link: &SignedCapability,
89 directory: &dyn KeyDirectory,
90) -> AccessResult<LinkSig> {
91 let key = match directory.published_key(&link.capability.issuer)? {
92 Some(k) => k,
93 None => return Ok(LinkSig::UnknownIssuer),
94 };
95 let canonical = link.capability.canonical_bytes()?;
96 if verify_sec1(&key, &canonical, &link.issuer_signature) {
97 Ok(LinkSig::Ok)
98 } else {
99 Ok(LinkSig::BadSignature)
100 }
101}
102
103fn check_request(cap: &Capability, request: &AccessRequest, now_unix: u64) -> Option<DenyReason> {
105 if &cap.bearer != request.bearer {
106 return Some(DenyReason::BearerMismatch);
107 }
108 if !cap.scope.covers(request.scope) {
109 return Some(DenyReason::OutOfScope);
110 }
111 if !cap.ops.contains(request.op) {
112 return Some(DenyReason::OpNotGranted);
113 }
114 if let Some(expiry) = cap.expiry {
115 if now_unix >= expiry {
116 return Some(DenyReason::Expired);
117 }
118 }
119 None
120}
121
122fn expiry_within(parent: Option<u64>, sub: Option<u64>) -> bool {
123 match (parent, sub) {
124 (None, _) => true, (Some(_), None) => false, (Some(p), Some(s)) => s <= p, }
128}
129
130fn check_narrowing(parent: &Capability, sub: &Capability) -> Option<DelegationError> {
132 if sub.issuer != parent.bearer {
133 return Some(DelegationError::IssuerNotDelegator);
134 }
135 if parent.delegation_depth == 0 {
136 return Some(DelegationError::ParentNotDelegable);
137 }
138 if sub.delegation_depth > parent.delegation_depth - 1 {
139 return Some(DelegationError::DepthExceeded);
140 }
141 if !parent.scope.covers(&sub.scope) {
142 return Some(DelegationError::ScopeEscalation);
143 }
144 if !sub.ops.is_subset_of(parent.ops) {
145 return Some(DelegationError::OpsEscalation);
146 }
147 if !expiry_within(parent.expiry, sub.expiry) {
148 return Some(DelegationError::ExpiryExtension);
149 }
150 None
151}
152
153pub fn authorize(
155 signed: &SignedCapability,
156 request: &AccessRequest,
157 directory: &dyn KeyDirectory,
158 now_unix: u64,
159 revocations: &RevocationSet,
160) -> AccessResult<Decision> {
161 match verify_link_signature(signed, directory)? {
162 LinkSig::UnknownIssuer => return Ok(Decision::Deny(DenyReason::UnknownIssuer)),
163 LinkSig::BadSignature => return Ok(Decision::Deny(DenyReason::BadSignature)),
164 LinkSig::Ok => {}
165 }
166 if revocations.is_revoked(&signed.capability.id) {
167 return Ok(Decision::Deny(DenyReason::Revoked));
168 }
169 if let Some(reason) = check_request(&signed.capability, request, now_unix) {
170 return Ok(Decision::Deny(reason));
171 }
172 Ok(Decision::Allow)
173}
174
175pub fn authorize_chain(
179 chain: &CapabilityChain,
180 request: &AccessRequest,
181 directory: &dyn KeyDirectory,
182 now_unix: u64,
183 revocations: &RevocationSet,
184) -> AccessResult<Decision> {
185 let links = chain.links();
186 if links.is_empty() {
187 return Ok(Decision::Deny(DenyReason::EmptyChain));
188 }
189
190 let mut parent: Option<&Capability> = None;
191 for link in links {
192 match verify_link_signature(link, directory)? {
193 LinkSig::UnknownIssuer => return Ok(Decision::Deny(DenyReason::UnknownIssuer)),
194 LinkSig::BadSignature => return Ok(Decision::Deny(DenyReason::BadSignature)),
195 LinkSig::Ok => {}
196 }
197 if revocations.is_revoked(&link.capability.id) {
198 return Ok(Decision::Deny(DenyReason::Revoked));
199 }
200 if let Some(p) = parent {
201 if let Some(err) = check_narrowing(p, &link.capability) {
202 return Ok(Decision::Deny(DenyReason::Delegation(err)));
203 }
204 }
205 parent = Some(&link.capability);
206 }
207
208 let leaf = &links.last().unwrap().capability;
210 if let Some(reason) = check_request(leaf, request, now_unix) {
211 return Ok(Decision::Deny(reason));
212 }
213 Ok(Decision::Allow)
214}