lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

permission.rs (8529B)


      1 //! Canonical NIP-46 permissions and bounded permission sets.
      2 
      3 use crate::error::RadrootsNostrConnectError;
      4 use crate::method::Method;
      5 use serde::{Deserialize, Deserializer, Serialize, Serializer};
      6 use std::fmt;
      7 use std::str::FromStr;
      8 
      9 /// Maximum UTF-8 byte length of one permission parameter.
     10 pub const PERMISSION_PARAMETER_MAX_BYTES: usize = 64;
     11 /// Maximum number of permissions accepted from one wire value.
     12 pub const PERMISSION_COUNT_MAX: usize = 64;
     13 /// Maximum UTF-8 byte length of the comma-separated permission wire value.
     14 pub const PERMISSIONS_MAX_BYTES: usize = 4_096;
     15 
     16 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
     17 pub struct Permission {
     18     #[doc(hidden)]
     19     pub method: Method,
     20     #[doc(hidden)]
     21     pub parameter: Option<String>,
     22 }
     23 
     24 impl Permission {
     25     #[must_use]
     26     pub fn new(method: Method) -> Self {
     27         Self {
     28             method,
     29             parameter: None,
     30         }
     31     }
     32 
     33     /// Creates a permission with a bounded canonical parameter.
     34     pub fn try_with_parameter(
     35         method: Method,
     36         parameter: impl Into<String>,
     37     ) -> Result<Self, RadrootsNostrConnectError> {
     38         let parameter = parameter.into();
     39         validate_parameter(&parameter)?;
     40         Ok(Self {
     41             method,
     42             parameter: Some(parameter),
     43         })
     44     }
     45 
     46     /// Compatibility constructor retained until the Step 141 consumer cutover.
     47     #[doc(hidden)]
     48     #[must_use]
     49     pub fn with_parameter(method: Method, parameter: impl Into<String>) -> Self {
     50         Self {
     51             method,
     52             parameter: Some(parameter.into()),
     53         }
     54     }
     55 
     56     /// Returns the permission method.
     57     #[must_use]
     58     pub fn method(&self) -> &Method {
     59         &self.method
     60     }
     61 
     62     /// Returns the optional method-specific parameter.
     63     #[must_use]
     64     pub fn parameter(&self) -> Option<&str> {
     65         self.parameter.as_deref()
     66     }
     67 
     68     pub fn matches_request(&self, method: &Method, parameter: Option<&str>) -> bool {
     69         if self.method != *method {
     70             return false;
     71         }
     72         match (&self.method, self.parameter.as_deref(), parameter) {
     73             (Method::SignEvent, None, _) => true,
     74             (Method::SignEvent, Some(configured), Some(requested)) => {
     75                 match (
     76                     sign_event_kind_parameter(configured),
     77                     sign_event_kind_parameter(requested),
     78                 ) {
     79                     (Some(configured), Some(requested)) => configured == requested,
     80                     _ => false,
     81                 }
     82             }
     83             (_, None, None) => true,
     84             (_, Some(configured), Some(requested)) => configured == requested,
     85             _ => false,
     86         }
     87     }
     88 
     89     pub fn matches_sign_event_kind(&self, event_kind: u32) -> bool {
     90         let event_kind = event_kind.to_string();
     91         self.matches_request(&Method::SignEvent, Some(event_kind.as_str()))
     92     }
     93 }
     94 
     95 impl fmt::Display for Permission {
     96     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     97         match self.parameter.as_deref() {
     98             Some(parameter) => write!(f, "{}:{parameter}", self.method),
     99             None => write!(f, "{}", self.method),
    100         }
    101     }
    102 }
    103 
    104 impl FromStr for Permission {
    105     type Err = RadrootsNostrConnectError;
    106 
    107     fn from_str(value: &str) -> Result<Self, Self::Err> {
    108         let trimmed = value.trim();
    109         if trimmed.is_empty() {
    110             return Err(RadrootsNostrConnectError::InvalidPermission(
    111                 value.to_owned(),
    112             ));
    113         }
    114 
    115         let (method, parameter) = match trimmed.split_once(':') {
    116             Some((method, parameter)) if !parameter.is_empty() => (method, Some(parameter)),
    117             Some(_) => {
    118                 return Err(RadrootsNostrConnectError::InvalidPermission(
    119                     value.to_owned(),
    120                 ));
    121             }
    122             None => (trimmed, None),
    123         };
    124 
    125         let method = Method::from_str(method)?;
    126         match parameter {
    127             Some(parameter) => Self::try_with_parameter(method, parameter),
    128             None => Ok(Self::new(method)),
    129         }
    130     }
    131 }
    132 
    133 #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
    134 pub struct Permissions(Vec<Permission>);
    135 
    136 impl Permissions {
    137     pub fn new() -> Self {
    138         Self::default()
    139     }
    140 
    141     pub fn as_slice(&self) -> &[Permission] {
    142         self.0.as_slice()
    143     }
    144 
    145     pub fn into_vec(self) -> Vec<Permission> {
    146         self.0
    147     }
    148 
    149     pub fn is_empty(&self) -> bool {
    150         self.0.is_empty()
    151     }
    152 
    153     /// Validates and canonicalizes a permission collection.
    154     pub fn try_from_vec(value: Vec<Permission>) -> Result<Self, RadrootsNostrConnectError> {
    155         let value = canonicalize(value);
    156         validate_permissions(&value)?;
    157         Ok(Self(value))
    158     }
    159 
    160     pub fn allows_request(&self, method: &Method, parameter: Option<&str>) -> bool {
    161         self.0
    162             .iter()
    163             .any(|permission| permission.matches_request(method, parameter))
    164     }
    165 
    166     pub fn allows_sign_event_kind(&self, event_kind: u32) -> bool {
    167         self.0
    168             .iter()
    169             .any(|permission| permission.matches_sign_event_kind(event_kind))
    170     }
    171 }
    172 
    173 fn sign_event_kind_parameter(value: &str) -> Option<u32> {
    174     let value = value.strip_prefix("kind:").unwrap_or(value);
    175     if value.is_empty() || !value.chars().all(|character| character.is_ascii_digit()) {
    176         return None;
    177     }
    178     value.parse().ok()
    179 }
    180 
    181 impl From<Vec<Permission>> for Permissions {
    182     fn from(value: Vec<Permission>) -> Self {
    183         Self(canonicalize(value))
    184     }
    185 }
    186 
    187 impl fmt::Display for Permissions {
    188     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    189         let rendered = self
    190             .0
    191             .iter()
    192             .map(ToString::to_string)
    193             .collect::<Vec<_>>()
    194             .join(",");
    195         f.write_str(&rendered)
    196     }
    197 }
    198 
    199 impl FromStr for Permissions {
    200     type Err = RadrootsNostrConnectError;
    201 
    202     fn from_str(value: &str) -> Result<Self, Self::Err> {
    203         let trimmed = value.trim();
    204         if trimmed.is_empty() {
    205             return Ok(Self::default());
    206         }
    207         if trimmed.len() > PERMISSIONS_MAX_BYTES {
    208             return Err(invalid_permissions(
    209                 "serialized permission set exceeds its byte limit",
    210             ));
    211         }
    212 
    213         let permissions = trimmed
    214             .split(',')
    215             .map(Permission::from_str)
    216             .collect::<Result<Vec<_>, _>>()?;
    217         Self::try_from_vec(permissions)
    218     }
    219 }
    220 
    221 impl Serialize for Permissions {
    222     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    223     where
    224         S: Serializer,
    225     {
    226         validate_permissions(&self.0).map_err(serde::ser::Error::custom)?;
    227         serializer.serialize_str(&self.to_string())
    228     }
    229 }
    230 
    231 impl<'de> Deserialize<'de> for Permissions {
    232     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    233     where
    234         D: Deserializer<'de>,
    235     {
    236         let value = String::deserialize(deserializer)?;
    237         Self::from_str(&value).map_err(serde::de::Error::custom)
    238     }
    239 }
    240 
    241 fn canonicalize(mut permissions: Vec<Permission>) -> Vec<Permission> {
    242     permissions.sort_by_key(ToString::to_string);
    243     permissions.dedup();
    244     permissions
    245 }
    246 
    247 fn validate_permissions(permissions: &[Permission]) -> Result<(), RadrootsNostrConnectError> {
    248     if permissions.len() > PERMISSION_COUNT_MAX {
    249         return Err(invalid_permissions("permission count exceeds its limit"));
    250     }
    251     for permission in permissions {
    252         Method::from_str(permission.method.as_str())?;
    253         if let Some(parameter) = permission.parameter.as_deref() {
    254             validate_parameter(parameter)?;
    255         }
    256     }
    257     let rendered = permissions
    258         .iter()
    259         .map(ToString::to_string)
    260         .collect::<Vec<_>>()
    261         .join(",");
    262     if rendered.len() > PERMISSIONS_MAX_BYTES {
    263         return Err(invalid_permissions(
    264             "serialized permission set exceeds its byte limit",
    265         ));
    266     }
    267     Ok(())
    268 }
    269 
    270 fn validate_parameter(value: &str) -> Result<(), RadrootsNostrConnectError> {
    271     if value.is_empty()
    272         || value.len() > PERMISSION_PARAMETER_MAX_BYTES
    273         || value.trim() != value
    274         || value.contains(',')
    275         || value.chars().any(char::is_control)
    276     {
    277         return Err(RadrootsNostrConnectError::InvalidPermission(
    278             value.to_owned(),
    279         ));
    280     }
    281     Ok(())
    282 }
    283 
    284 fn invalid_permissions(reason: &str) -> RadrootsNostrConnectError {
    285     RadrootsNostrConnectError::InvalidPermission(reason.to_owned())
    286 }