about summary refs log blame commit diff
path: root/indieauth/src/scopes.rs
blob: e1df3713eb9236e0d01eca5c0230aab94e5b137e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
                      







                                 
                                                           



























                                                                       
                                                  


                                         
                                                               



































                                              




                                                     


                                                                                   
                                      
                              
                                                        

                                            
                                                           

                                              
                                                           
                                                     
                                                     
                       
                                                               

                                                       




                                  

                                                                        









                                                         







                                                         


















                                                                                  
                                            

                                       
 
























                                                                                     
                                                                                             







                                                                          
 
                                                                                            

     
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<str> 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<Self, Self::Err> {
        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<Scope>);
impl Scopes {
    /// Create a list of scopes from a vector of scopes.
    pub fn new(scopes: Vec<Scope>) -> 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()
    }
}

// Explicit implementation of ToString because of specific requirements.
#[allow(clippy::to_string_trait_impl)]
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<Self, Self::Err> {
        Ok(Self(value.split_ascii_whitespace()
                .map(Scope::from)
                .collect::<Vec<Scope>>()))
    }
}
impl Serialize for Scopes {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    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<E>(self, value: &str) -> Result<Self::Value, E>
    where
        E: DeserializeError
    {
        Ok(Scopes::from_str(value).unwrap())
    }
}
impl<'de> Deserialize<'de> for Scopes {

    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    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::<Scopes>(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")]));
    }

}