nip46_authorization.rs (8120B)
1 //! Configuration-bound NIP-46 connection and challenge policy. 2 3 use serde_json::Value; 4 5 use crate::{ 6 MycAuthorizationChallengeRecord, MycAuthorizationChallengeRequest, 7 MycAuthorizationChallengeState, MycAuthorizationChallengeUrl, MycConnectionAdmissionPolicy, 8 MycConnectionAdmissionRequest, MycConnectionOperatorDecision, MycConnectionPermission, 9 MycConnectionPermissionSet, MycConnectionTimeUnixMs, MycNip46ClientPublicKey, 10 }; 11 12 #[derive(Clone, PartialEq, Eq)] 13 pub(crate) struct MycConnectionPolicies { 14 trusted_clients: Box<[MycNip46ClientPublicKey]>, 15 denied_clients: Box<[MycNip46ClientPublicKey]>, 16 permission_ceiling: MycConnectionPermissionSet, 17 challenge: Option<MycChallengePolicy>, 18 retention: MycAuthorizationRetentionPolicy, 19 } 20 21 #[derive(Clone, PartialEq, Eq)] 22 struct MycChallengePolicy { 23 url: MycAuthorizationChallengeUrl, 24 pending_lifetime_ms: u64, 25 authorized_lifetime_ms: u64, 26 } 27 28 #[derive(Clone, Copy, PartialEq, Eq)] 29 struct MycAuthorizationRetentionPolicy { 30 terminal_connections_ms: u64, 31 terminal_challenges_ms: u64, 32 request_dedup_ms: u64, 33 completed_outbox_ms: u64, 34 } 35 36 impl MycConnectionPolicies { 37 pub(crate) fn from_normalized(document: &Value) -> Result<Self, ()> { 38 let invalid = || (); 39 let strings = |pointer: &str| { 40 document 41 .pointer(pointer) 42 .and_then(Value::as_array) 43 .ok_or_else(invalid)? 44 .iter() 45 .map(|value| value.as_str().ok_or_else(invalid)) 46 .collect::<Result<Vec<_>, _>>() 47 }; 48 let clients = |pointer: &str| { 49 strings(pointer)? 50 .into_iter() 51 .map(|value| MycNip46ClientPublicKey::new(value).map_err(|_| invalid())) 52 .collect::<Result<Vec<_>, _>>() 53 .map(Vec::into_boxed_slice) 54 }; 55 let trusted_clients = clients("/policy/trusted_clients")?; 56 let denied_clients = clients("/policy/denied_clients")?; 57 if trusted_clients 58 .iter() 59 .any(|trusted| denied_clients.contains(trusted)) 60 { 61 return Err(()); 62 } 63 let permission_ceiling = MycConnectionPermissionSet::new( 64 &strings("/policy/permission_ceiling")? 65 .into_iter() 66 .map(|value| MycConnectionPermission::parse(value).ok_or_else(invalid)) 67 .collect::<Result<Vec<_>, _>>()?, 68 ) 69 .map_err(|_| ())?; 70 let integer = |pointer: &str| { 71 document 72 .pointer(pointer) 73 .and_then(Value::as_u64) 74 .filter(|value| *value > 0 && i64::try_from(*value).is_ok()) 75 .ok_or_else(invalid) 76 }; 77 let challenge = document 78 .pointer("/policy/challenges/enabled") 79 .and_then(Value::as_bool) 80 .ok_or_else(invalid)? 81 .then(|| { 82 let url = document 83 .pointer("/policy/challenges/url") 84 .and_then(Value::as_str) 85 .ok_or_else(invalid) 86 .and_then(|value| MycAuthorizationChallengeUrl::new(value).map_err(|_| ()))?; 87 let pending_lifetime_ms = integer("/policy/challenges/pending_lifetime_ms")?; 88 let authorized_lifetime_ms = integer("/policy/challenges/authorized_lifetime_ms")?; 89 if authorized_lifetime_ms < pending_lifetime_ms { 90 return Err(()); 91 } 92 Ok(MycChallengePolicy { 93 url, 94 pending_lifetime_ms, 95 authorized_lifetime_ms, 96 }) 97 }) 98 .transpose()?; 99 let retention = MycAuthorizationRetentionPolicy { 100 terminal_connections_ms: integer("/policy/retention/terminal_connections_ms")?, 101 terminal_challenges_ms: integer("/policy/retention/terminal_challenges_ms")?, 102 request_dedup_ms: integer("/policy/retention/request_dedup_ms")?, 103 completed_outbox_ms: integer("/policy/retention/completed_outbox_ms")?, 104 }; 105 Ok(Self { 106 trusted_clients, 107 denied_clients, 108 permission_ceiling, 109 challenge, 110 retention, 111 }) 112 } 113 114 fn expected_admission(&self, client: &MycNip46ClientPublicKey) -> MycConnectionAdmissionPolicy { 115 if self.denied_clients.contains(client) { 116 MycConnectionAdmissionPolicy::Denied 117 } else if self.trusted_clients.contains(client) { 118 MycConnectionAdmissionPolicy::Trusted 119 } else { 120 MycConnectionAdmissionPolicy::ExplicitApproval 121 } 122 } 123 124 pub(crate) fn admits_connection_request( 125 &self, 126 request: &MycConnectionAdmissionRequest, 127 ) -> bool { 128 let expected = self.expected_admission(request.client_public_key()); 129 if request.policy() != expected { 130 return false; 131 } 132 if expected == MycConnectionAdmissionPolicy::Denied { 133 return true; 134 } 135 if !request 136 .requested_permissions() 137 .is_subset_of(&self.permission_ceiling) 138 { 139 return false; 140 } 141 match expected { 142 MycConnectionAdmissionPolicy::Trusted => { 143 self.admits_authorized_until(request.observed_at(), request.authorized_until()) 144 } 145 MycConnectionAdmissionPolicy::ExplicitApproval => request.authorized_until().is_none(), 146 MycConnectionAdmissionPolicy::Denied => true, 147 } 148 } 149 150 pub(crate) fn admits_operator_decision( 151 &self, 152 observed_at: MycConnectionTimeUnixMs, 153 decision: &MycConnectionOperatorDecision, 154 ) -> bool { 155 match decision { 156 MycConnectionOperatorDecision::Approve { 157 granted_permissions, 158 authorized_until, 159 } => { 160 granted_permissions.is_subset_of(&self.permission_ceiling) 161 && self.admits_authorized_until(observed_at, *authorized_until) 162 } 163 MycConnectionOperatorDecision::Deny => true, 164 } 165 } 166 167 pub(crate) fn admits_challenge_request( 168 &self, 169 request: &MycAuthorizationChallengeRequest, 170 ) -> bool { 171 let Some(policy) = &self.challenge else { 172 return false; 173 }; 174 request.url() == &policy.url 175 && request 176 .expires_at() 177 .get() 178 .checked_sub(request.issued_at().get()) 179 .is_some_and(|lifetime| lifetime <= policy.pending_lifetime_ms) 180 } 181 182 pub(crate) fn challenge_is_current( 183 &self, 184 record: &MycAuthorizationChallengeRecord, 185 observed_at: MycConnectionTimeUnixMs, 186 ) -> bool { 187 let Some(policy) = &self.challenge else { 188 return false; 189 }; 190 record.state() == MycAuthorizationChallengeState::Authorized 191 && record.resolved_at().is_some_and(|resolved| { 192 observed_at >= resolved 193 && observed_at 194 .get() 195 .checked_sub(resolved.get()) 196 .is_some_and(|age| age <= policy.authorized_lifetime_ms) 197 }) 198 } 199 200 fn admits_authorized_until( 201 &self, 202 observed_at: MycConnectionTimeUnixMs, 203 authorized_until: Option<MycConnectionTimeUnixMs>, 204 ) -> bool { 205 match (&self.challenge, authorized_until) { 206 (None, None) => true, 207 (Some(policy), Some(until)) => until 208 .get() 209 .checked_sub(observed_at.get()) 210 .is_some_and(|lifetime| lifetime > 0 && lifetime <= policy.authorized_lifetime_ms), 211 _ => false, 212 } 213 } 214 215 pub(crate) const fn retention_is_bounded(&self) -> bool { 216 self.retention.terminal_connections_ms > 0 217 && self.retention.terminal_challenges_ms > 0 218 && self.retention.request_dedup_ms > 0 219 && self.retention.completed_outbox_ms > 0 220 } 221 }