From 16dfc096874b3ba8c8de88b61fc311faef899f4c Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 29 Aug 2024 11:48:21 +0300 Subject: [PATCH] feat(homeserver): store and return session capabilities --- pubky-common/src/auth.rs | 41 +++--- pubky-common/src/capabilities.rs | 170 ++++++++++++++++++++++++ pubky-common/src/lib.rs | 1 + pubky-common/src/session.rs | 27 +++- pubky-homeserver/src/database/tables.rs | 13 ++ pubky-homeserver/src/routes/auth.rs | 27 ++-- pubky/src/shared/auth.rs | 16 +-- 7 files changed, 247 insertions(+), 48 deletions(-) create mode 100644 pubky-common/src/capabilities.rs diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 8f95fbc..71e69a5 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use serde::{Deserialize, Serialize}; use crate::{ + capabilities::Capability, crypto::{Keypair, PublicKey, Signature}, timestamp::Timestamp, }; @@ -30,12 +31,12 @@ pub struct AuthToken { subject: PublicKey, /// The Pubky of the party verifying the [AuthToken], for example a web server. audience: PublicKey, - // Variable length scopes - scopes: Vec, + // Variable length capabilities + capabilities: Vec, } impl AuthToken { - pub fn sign(signer: &Keypair, audience: &PublicKey, scopes: Vec) -> Self { + pub fn sign(signer: &Keypair, audience: &PublicKey, capabilities: Vec) -> Self { let timestamp = Timestamp::now(); let mut token = Self { @@ -43,7 +44,7 @@ impl AuthToken { subject: signer.public_key(), audience: audience.to_owned(), timestamp, - scopes, + capabilities, signature: Signature::from_bytes(&[0; 64]), }; @@ -54,6 +55,10 @@ impl AuthToken { token } + pub fn capabilities(&self) -> &[Capability] { + &self.capabilities + } + /// Authenticate signer to an audience directly with [] capailities. /// /// @@ -150,7 +155,7 @@ impl AuthVerifier { match seen.binary_search_by(|element| element.cmp(&id)) { Ok(_) => Err(Error::AlreadyUsed), Err(index) => { - seen.insert(index, id.into()); + seen.insert(index, id); Ok(token) } } @@ -192,7 +197,9 @@ pub enum Error { #[cfg(test)] mod tests { - use crate::{auth::TIMESTAMP_WINDOW, crypto::Keypair, timestamp::Timestamp}; + use crate::{ + auth::TIMESTAMP_WINDOW, capabilities::Capability, crypto::Keypair, timestamp::Timestamp, + }; use super::{AuthToken, AuthVerifier, Error}; @@ -200,9 +207,9 @@ mod tests { fn v0_id_signable() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; - let token = AuthToken::sign(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, capabilities.clone()); let serialized = &token.serialize(); @@ -221,24 +228,24 @@ mod tests { fn sign_verify() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; let verifier = AuthVerifier::new(audience.clone()); - let token = AuthToken::sign(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, capabilities.clone()); let serialized = &token.serialize(); verifier.verify(serialized).unwrap(); - assert_eq!(token.scopes, scopes); + assert_eq!(token.capabilities, capabilities); } #[test] fn expired() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; let verifier = AuthVerifier::new(audience.clone()); @@ -247,7 +254,7 @@ mod tests { let mut signable = vec![]; signable.extend_from_slice(signer.public_key().as_bytes()); signable.extend_from_slice(audience.as_bytes()); - signable.extend_from_slice(&postcard::to_allocvec(&scopes).unwrap()); + signable.extend_from_slice(&postcard::to_allocvec(&capabilities).unwrap()); let signature = signer.sign(&signable); @@ -257,7 +264,7 @@ mod tests { audience, timestamp, signature, - scopes, + capabilities, }; let serialized = token.serialize(); @@ -271,17 +278,17 @@ mod tests { fn already_used() { let signer = Keypair::random(); let audience = Keypair::random().public_key(); - let scopes = vec!["*:*".to_string()]; + let capabilities = vec![Capability::pubky_root()]; let verifier = AuthVerifier::new(audience.clone()); - let token = AuthToken::sign(&signer, &audience, scopes.clone()); + let token = AuthToken::sign(&signer, &audience, capabilities.clone()); let serialized = &token.serialize(); verifier.verify(serialized).unwrap(); - assert_eq!(token.scopes, scopes); + assert_eq!(token.capabilities, capabilities); assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed)); } diff --git a/pubky-common/src/capabilities.rs b/pubky-common/src/capabilities.rs new file mode 100644 index 0000000..2a46cde --- /dev/null +++ b/pubky-common/src/capabilities.rs @@ -0,0 +1,170 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +const PUBKY_CAP_PREFIX: &str = "pk!"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Capability { + /// Pubky Homeserver's capabilities + Pubky(PubkyCap), + Unknown(String), +} + +impl Capability { + /// Create a [PubkyCap] at the root path `/` with all the available [PubkyAbility] + pub fn pubky_root() -> Self { + Capability::Pubky(PubkyCap { + path: "/".to_string(), + abilities: vec![PubkyAbility::Read, PubkyAbility::Write], + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PubkyCap { + pub path: String, + pub abilities: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PubkyAbility { + /// Can read the resource at the specified path (GET requests). + Read, + /// Can write to the resource at the specified path (PUT/POST/DELETE requests). + Write, +} + +impl From<&PubkyAbility> for char { + fn from(value: &PubkyAbility) -> Self { + match value { + PubkyAbility::Read => 'r', + PubkyAbility::Write => 'w', + } + } +} + +impl TryFrom for PubkyAbility { + type Error = Error; + + fn try_from(value: char) -> Result { + match value { + 'r' => Ok(Self::Read), + 'w' => Ok(Self::Write), + _ => Err(Error::InvalidPubkyAbility), + } + } +} + +impl TryFrom for Capability { + type Error = Error; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pubky(cap) => write!( + f, + "{}{}:{}", + PUBKY_CAP_PREFIX, + cap.path, + cap.abilities.iter().map(char::from).collect::() + ), + Self::Unknown(string) => write!(f, "{string}"), + } + } +} + +impl TryFrom<&str> for Capability { + type Error = Error; + + fn try_from(value: &str) -> Result { + if value.starts_with(PUBKY_CAP_PREFIX) { + let mut rsplit = value.rsplit(':'); + + let mut abilities = Vec::new(); + + for char in rsplit + .next() + .ok_or(Error::MissingField("abilities"))? + .chars() + { + let ability = PubkyAbility::try_from(char)?; + + match abilities.binary_search_by(|element| char::from(element).cmp(&char)) { + Ok(_) => {} + Err(index) => { + abilities.insert(index, ability); + } + } + } + + let path = rsplit.next().ok_or(Error::MissingField("path"))?[PUBKY_CAP_PREFIX.len()..] + .to_string(); + + if !path.starts_with('/') { + return Err(Error::InvalidPath); + } + + return Ok(Capability::Pubky(PubkyCap { path, abilities })); + } + + Ok(Capability::Unknown(value.to_string())) + } +} + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum Error { + #[error("PubkyCap: Missing field {0}")] + MissingField(&'static str), + #[error("PubkyCap: InvalidPath does not start with `/`")] + InvalidPath, + #[error("Invalid PubkyAbility")] + InvalidPubkyAbility, +} + +impl Serialize for Capability { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let string = self.to_string(); + + string.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Capability { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + + string.try_into().map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pubky_caps() { + let cap = Capability::Pubky(PubkyCap { + path: "/pub/pubky.app/".to_string(), + abilities: vec![PubkyAbility::Read, PubkyAbility::Write], + }); + + // Read and write withing directory `/pub/pubky.app/`. + let expected_string = "pk!/pub/pubky.app/:rw"; + + assert_eq!(cap.to_string(), expected_string); + + assert_eq!(Capability::try_from(expected_string), Ok(cap)) + } +} diff --git a/pubky-common/src/lib.rs b/pubky-common/src/lib.rs index cedc227..5234c51 100644 --- a/pubky-common/src/lib.rs +++ b/pubky-common/src/lib.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod capabilities; pub mod crypto; pub mod namespaces; pub mod session; diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 5a35e14..83be3a7 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; extern crate alloc; use alloc::vec::Vec; -use crate::timestamp::Timestamp; +use crate::{auth::AuthToken, capabilities::Capability, timestamp::Timestamp}; // TODO: add IP address? // TODO: use https://crates.io/crates/user-agent-parser to parse the session @@ -16,14 +16,23 @@ pub struct Session { /// User specified name, defaults to the user-agent. pub name: String, pub user_agent: String, + pub capabilities: Vec, } impl Session { - pub fn new() -> Self { - Self { + pub fn new(token: &AuthToken, user_agent: Option) -> Self { + let mut session = Self { created_at: Timestamp::now().into_inner(), ..Default::default() + }; + + session.set_capabilities(token.capabilities().to_vec()); + + if let Some(user_agent) = user_agent { + session.set_user_agent(user_agent); } + + session } // === Setters === @@ -38,6 +47,12 @@ impl Session { self } + pub fn set_capabilities(&mut self, capabilities: Vec) -> &mut Self { + self.capabilities = capabilities; + + self + } + // === Public Methods === pub fn serialize(&self) -> Vec { @@ -71,12 +86,16 @@ mod tests { fn serialize() { let session = Session { user_agent: "foo".to_string(), + capabilities: vec![Capability::pubky_root()], ..Default::default() }; let serialized = session.serialize(); - assert_eq!(serialized, [0, 0, 0, 3, 102, 111, 111,]); + assert_eq!( + serialized, + [0, 0, 0, 3, 102, 111, 111, 1, 7, 112, 107, 33, 47, 58, 114, 119] + ); let deseiralized = Session::deserialize(&serialized).unwrap(); diff --git a/pubky-homeserver/src/database/tables.rs b/pubky-homeserver/src/database/tables.rs index a019fbe..0f5f287 100644 --- a/pubky-homeserver/src/database/tables.rs +++ b/pubky-homeserver/src/database/tables.rs @@ -8,10 +8,17 @@ use heed::{Env, RwTxn}; use blobs::{BlobsTable, BLOBS_TABLE}; use entries::{EntriesTable, ENTRIES_TABLE}; +use self::{ + sessions::{SessionsTable, SESSIONS_TABLE}, + users::{UsersTable, USERS_TABLE}, +}; + pub const TABLES_COUNT: u32 = 4; #[derive(Debug, Clone)] pub struct Tables { + pub users: UsersTable, + pub sessions: SessionsTable, pub blobs: BlobsTable, pub entries: EntriesTable, } @@ -19,6 +26,12 @@ pub struct Tables { impl Tables { pub fn new(env: &Env, wtxn: &mut RwTxn) -> anyhow::Result { Ok(Self { + users: env + .open_database(wtxn, Some(USERS_TABLE))? + .expect("Users table already created"), + sessions: env + .open_database(wtxn, Some(SESSIONS_TABLE))? + .expect("Sessions table already created"), blobs: env .open_database(wtxn, Some(BLOBS_TABLE))? .expect("Blobs table already created"), diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 5f035e2..9bfe5f8 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -13,7 +13,7 @@ use pubky_common::{crypto::random_bytes, session::Session, timestamp::Timestamp} use crate::{ database::tables::{ sessions::{SessionsTable, SESSIONS_TABLE}, - users::{User, UsersTable, USERS_TABLE}, + users::User, }, error::{Error, Result}, extractors::Pubky, @@ -97,12 +97,8 @@ pub async fn signin( let public_key = token.subject(); let mut wtxn = state.db.env.write_txn()?; - let users: UsersTable = state - .db - .env - .open_database(&wtxn, Some(USERS_TABLE))? - .expect("Users table already created"); + let users = state.db.tables.users; if let Some(existing) = users.get(&wtxn, public_key)? { users.put(&mut wtxn, public_key, &existing)?; } else { @@ -117,21 +113,14 @@ pub async fn signin( let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); - let sessions: SessionsTable = state - .db - .env - .open_database(&wtxn, Some(SESSIONS_TABLE))? - .expect("Sessions table already created"); - - let mut session = Session::new(); - - if let Some(user_agent) = user_agent { - session.set_user_agent(user_agent.to_string()); - } - - sessions.put(&mut wtxn, &session_secret, &session.serialize())?; + state.db.tables.sessions.put( + &mut wtxn, + &session_secret, + &Session::new(&token, user_agent.map(|ua| ua.to_string())).serialize(), + )?; let mut cookie = Cookie::new(public_key.to_string(), session_secret); + cookie.set_path("/"); if *uri.scheme().unwrap_or(&Scheme::HTTP) == Scheme::HTTPS { cookie.set_secure(true); diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 072461c..6cbb65d 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,7 +1,7 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; -use pubky_common::{auth::AuthToken, session::Session}; +use pubky_common::{auth::AuthToken, capabilities::Capability, session::Session}; use crate::{error::Result, PubkyClient}; @@ -24,9 +24,9 @@ impl PubkyClient { mut url, } = self.resolve_endpoint(&homeserver).await?; - url.set_path(&format!("/signup")); + url.set_path("/signup"); - let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); + let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize(); let response = self .request(Method::POST, url.clone()) @@ -87,9 +87,9 @@ impl PubkyClient { mut url, } = self.resolve_pubky_homeserver(&pubky).await?; - url.set_path(&format!("/session")); + url.set_path("/session"); - let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); + let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize(); let response = self.request(Method::POST, url).body(body).send().await?; @@ -105,7 +105,7 @@ mod tests { use crate::*; use pkarr::{mainline::Testnet, Keypair}; - use pubky_common::session::Session; + use pubky_common::capabilities::Capability; use pubky_homeserver::Homeserver; #[tokio::test] @@ -125,7 +125,7 @@ mod tests { .unwrap() .unwrap(); - assert_eq!(session, Session { ..session.clone() }); + assert!(session.capabilities.contains(&Capability::pubky_root())); client.signout(&keypair.public_key()).await.unwrap(); @@ -144,7 +144,7 @@ mod tests { .unwrap() .unwrap(); - assert_eq!(session, Session { ..session.clone() }); + assert!(session.capabilities.contains(&Capability::pubky_root())); } } }