From 0617663b249f9ca488e5de652108b17d67fbaf45 Mon Sep 17 00:00:00 2001 From: Vika Date: Sat, 29 Jul 2023 21:59:56 +0300 Subject: Moved the entire Kittybox tree into the root --- indieauth/src/scopes.rs | 208 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 indieauth/src/scopes.rs (limited to 'indieauth/src/scopes.rs') diff --git a/indieauth/src/scopes.rs b/indieauth/src/scopes.rs new file mode 100644 index 0000000..d74878e --- /dev/null +++ b/indieauth/src/scopes.rs @@ -0,0 +1,208 @@ +use std::str::FromStr; + +use serde::{ + Serialize, Serializer, + Deserialize, + de::{ + Deserializer, Visitor, + Error as DeserializeError + } +}; + +/// Various scopes that can be requested through IndieAuth. +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Scope { + /// Allows to create posts using Micropub. + Create, + /// Allows to edit posts using Micropub. + Update, + /// Allows to delete posts using Micropub. + Delete, + /// Allows to upload blobs to the media endpoint. + Media, + /// Allows to read feeds via Microsub. + Read, + /// Allows to manage follows via Microsub. + Follow, + /// Allows to mute and unmute users in feeds via Microsub. + Mute, + /// Allows to block and unblock users. + Block, + /// Allows to create and manage feeds via Microsub. + Channels, + /// Allows to request profile information (except email, see Email) + Profile, + /// Allows to receive email in the profile information. + Email, + /// Custom scope not included above. + Custom(String) +} +impl Scope { + /// Create a custom scope from a string slice. + pub fn custom(scope: &str) -> Scope { + Scope::Custom(scope.to_string()) + } +} + +// TODO consider relying on serde_variant for these conversions +impl AsRef for Scope { + fn as_ref(&self) -> &str { + use Scope::*; + match self { + Create => "create", + Update => "update", + Delete => "delete", + Media => "media", + Read => "read", + Follow => "follow", + Mute => "mute", + Block => "block", + Channels => "channels", + Profile => "profile", + Email => "email", + Custom(s) => s.as_ref() + } + } +} +impl From<&str> for Scope { + fn from(scope: &str) -> Self { + match scope { + "create" => Scope::Create, + "update" => Scope::Update, + "delete" => Scope::Delete, + "media" => Scope::Media, + "read" => Scope::Read, + "follow" => Scope::Follow, + "mute" => Scope::Mute, + "block" => Scope::Block, + "channels" => Scope::Channels, + "profile" => Scope::Profile, + "email" => Scope::Email, + other => Scope::custom(other) + } + } +} +impl FromStr for Scope { + type Err = std::convert::Infallible; + fn from_str(s: &str) -> Result { + Ok(s.into()) + } +} + +/// A list of scopes that serializes to a space-separated string instead of a list. +/// +/// OAuth2 is weird, don't ask me why it's a thing. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct Scopes(Vec); +impl Scopes { + /// Create a list of scopes from a vector of scopes. + pub fn new(scopes: Vec) -> Self { + Self(scopes) + } + /// Ensure a certain scope is listed in the scope list. + pub fn has(&self, scope: &Scope) -> bool { + self.0.iter().any(|s| s == scope) + } + /// Ensure all of the requested scopes are in the list. + pub fn has_all(&self, scopes: &[Scope]) -> bool { + scopes.iter() + .map(|s1| self.iter().any(|s2| s1 == s2)) + .all(|s| s) + } + /// Transform this into an iterator over individual scopes. + pub fn iter(&self) -> std::slice::Iter<'_, Scope> { + self.0.iter() + } +} +impl AsRef<[Scope]> for Scopes { + fn as_ref(&self) -> &[Scope] { + self.0.as_ref() + } +} +impl ToString for Scopes { + fn to_string(&self) -> String { + self.0.iter() + .map(|s| s.as_ref()) + .fold(String::new(), |a, s| if a.is_empty() { + s.to_string() + } else { + a + " " + s + }) + } +} +impl FromStr for Scopes { + type Err = std::convert::Infallible; + + fn from_str(value: &str) -> Result { + Ok(Self(value.split_ascii_whitespace() + .map(Scope::from) + .collect::>())) + } +} +impl Serialize for Scopes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer + { + serializer.serialize_str(&self.to_string()) + } +} +struct ScopeVisitor; +impl<'de> Visitor<'de> for ScopeVisitor { + type Value = Scopes; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string of space-separated OAuth2 scopes") + } + + fn visit_str(self, value: &str) -> Result + where + E: DeserializeError + { + Ok(Scopes::from_str(value).unwrap()) + } +} +impl<'de> Deserialize<'de> for Scopes { + + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de> + { + deserializer.deserialize_str(ScopeVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serde_vec_scope() { + let scopes = vec![ + Scope::Create, Scope::Update, Scope::Delete, + Scope::Media, + Scope::custom("kittybox_internal_access") + ]; + + let scope_serialized = serde_json::to_value( + Scopes::new(scopes.clone()) + ).unwrap(); + let scope_str = scope_serialized.as_str().unwrap(); + assert_eq!(scope_str, "create update delete media kittybox_internal_access"); + + assert!(serde_json::from_value::(scope_serialized).unwrap().has_all(&scopes)) + } + + #[test] + fn test_scope_has_all() { + let scopes = Scopes(vec![ + Scope::Create, Scope::Update, Scope::custom("draft") + ]); + + assert!(scopes.has_all(&[Scope::Create, Scope::custom("draft")])); + + assert!(!scopes.has_all(&[Scope::Read, Scope::custom("kittybox_internal_access")])); + } + +} -- cgit 1.4.1