feat(homeserver): store and return session capabilities

This commit is contained in:
nazeh
2024-08-29 11:48:21 +03:00
parent 8118e94430
commit 16dfc09687
7 changed files with 247 additions and 48 deletions

View File

@@ -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<String>,
// Variable length capabilities
capabilities: Vec<Capability>,
}
impl AuthToken {
pub fn sign(signer: &Keypair, audience: &PublicKey, scopes: Vec<String>) -> Self {
pub fn sign(signer: &Keypair, audience: &PublicKey, capabilities: Vec<Capability>) -> 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));
}

View File

@@ -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<PubkyAbility>,
}
#[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<char> for PubkyAbility {
type Error = Error;
fn try_from(value: char) -> Result<Self, Error> {
match value {
'r' => Ok(Self::Read),
'w' => Ok(Self::Write),
_ => Err(Error::InvalidPubkyAbility),
}
}
}
impl TryFrom<String> for Capability {
type Error = Error;
fn try_from(value: String) -> Result<Self, Error> {
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::<String>()
),
Self::Unknown(string) => write!(f, "{string}"),
}
}
}
impl TryFrom<&str> for Capability {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Error> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let string = self.to_string();
string.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Capability {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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))
}
}

View File

@@ -1,4 +1,5 @@
pub mod auth;
pub mod capabilities;
pub mod crypto;
pub mod namespaces;
pub mod session;

View File

@@ -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<Capability>,
}
impl Session {
pub fn new() -> Self {
Self {
pub fn new(token: &AuthToken, user_agent: Option<String>) -> 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<Capability>) -> &mut Self {
self.capabilities = capabilities;
self
}
// === Public Methods ===
pub fn serialize(&self) -> Vec<u8> {
@@ -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();

View File

@@ -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<Self> {
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"),

View File

@@ -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);

View File

@@ -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()));
}
}
}