feat(common): simplify Capabilities

This commit is contained in:
nazeh
2024-09-01 19:33:04 +03:00
parent 5d9be5c0f0
commit 0fe5e1aa74
4 changed files with 86 additions and 96 deletions

View File

@@ -207,7 +207,7 @@ mod tests {
fn v0_id_signable() {
let signer = Keypair::random();
let audience = Keypair::random().public_key();
let capabilities = vec![Capability::pubky_root()];
let capabilities = vec![Capability::root()];
let token = AuthToken::sign(&signer, &audience, capabilities.clone());
@@ -228,7 +228,7 @@ mod tests {
fn sign_verify() {
let signer = Keypair::random();
let audience = Keypair::random().public_key();
let capabilities = vec![Capability::pubky_root()];
let capabilities = vec![Capability::root()];
let verifier = AuthVerifier::new(audience.clone());
@@ -245,7 +245,7 @@ mod tests {
fn expired() {
let signer = Keypair::random();
let audience = Keypair::random().public_key();
let capabilities = vec![Capability::pubky_root()];
let capabilities = vec![Capability::root()];
let verifier = AuthVerifier::new(audience.clone());
@@ -278,7 +278,7 @@ mod tests {
fn already_used() {
let signer = Keypair::random();
let audience = Keypair::random().public_key();
let capabilities = vec![Capability::pubky_root()];
let capabilities = vec![Capability::root()];
let verifier = AuthVerifier::new(audience.clone());

View File

@@ -2,56 +2,50 @@ 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),
pub struct Capability {
pub resource: String,
pub abilities: Vec<Ability>,
}
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',
/// Create a root [Capability] at the `/` path with all the available [PubkyAbility]
pub fn root() -> Self {
Capability {
resource: "/".to_string(),
abilities: vec![Ability::Read, Ability::Write],
}
}
}
impl TryFrom<char> for PubkyAbility {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ability {
/// 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,
/// Unknown ability
Unknown(char),
}
impl From<&Ability> for char {
fn from(value: &Ability) -> Self {
match value {
Ability::Read => 'r',
Ability::Write => 'w',
Ability::Unknown(char) => char.to_owned(),
}
}
}
impl TryFrom<char> for Ability {
type Error = Error;
fn try_from(value: char) -> Result<Self, Error> {
match value {
'r' => Ok(Self::Read),
'w' => Ok(Self::Write),
_ => Err(Error::InvalidPubkyAbility),
_ => Err(Error::InvalidAbility),
}
}
}
@@ -66,16 +60,12 @@ impl TryFrom<String> for Capability {
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}"),
}
write!(
f,
"{}:{}",
self.resource,
self.abilities.iter().map(char::from).collect::<String>()
)
}
}
@@ -83,48 +73,46 @@ 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 }));
if value.matches(':').count() != 1 {
return Err(Error::InvalidFormat);
}
Ok(Capability::Unknown(value.to_string()))
if !value.starts_with('/') {
return Err(Error::InvalidResource);
}
let abilities_str = value.rsplit(':').next().unwrap_or("");
let mut abilities = Vec::new();
for char in abilities_str.chars() {
let ability = Ability::try_from(char)?;
match abilities.binary_search_by(|element| char::from(element).cmp(&char)) {
Ok(_) => {}
Err(index) => {
abilities.insert(index, ability);
}
}
}
let resource = value[0..value.len() - abilities_str.len() - 1].to_string();
Ok(Capability {
resource,
abilities,
})
}
}
#[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,
#[error("Capability: Invalid resource path: does not start with `/`")]
InvalidResource,
#[error("Capability: Invalid format should be <resource>:<abilities>")]
InvalidFormat,
#[error("Capability: Invalid Ability")]
InvalidAbility,
}
impl Serialize for Capability {
@@ -155,13 +143,13 @@ mod tests {
#[test]
fn pubky_caps() {
let cap = Capability::Pubky(PubkyCap {
path: "/pub/pubky.app/".to_string(),
abilities: vec![PubkyAbility::Read, PubkyAbility::Write],
});
let cap = Capability {
resource: "/pub/pubky.app/".to_string(),
abilities: vec![Ability::Read, Ability::Write],
};
// Read and write withing directory `/pub/pubky.app/`.
let expected_string = "pk!/pub/pubky.app/:rw";
let expected_string = "/pub/pubky.app/:rw";
assert_eq!(cap.to_string(), expected_string);

View File

@@ -66,6 +66,8 @@ impl Session {
Ok(from_bytes(bytes)?)
}
// TODO: add `can_read()`, `can_write()` and `is_root()` methods
}
pub type Result<T> = core::result::Result<T, Error>;
@@ -86,7 +88,7 @@ mod tests {
fn serialize() {
let session = Session {
user_agent: "foo".to_string(),
capabilities: vec![Capability::pubky_root()],
capabilities: vec![Capability::root()],
..Default::default()
};
@@ -94,7 +96,7 @@ mod tests {
assert_eq!(
serialized,
[0, 0, 0, 3, 102, 111, 111, 1, 7, 112, 107, 33, 47, 58, 114, 119]
[0, 0, 0, 3, 102, 111, 111, 1, 4, 47, 58, 114, 119]
);
let deseiralized = Session::deserialize(&serialized).unwrap();

View File

@@ -26,7 +26,7 @@ impl PubkyClient {
url.set_path("/signup");
let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize();
let body = AuthToken::sign(keypair, &audience, vec![Capability::root()]).serialize();
let response = self
.request(Method::POST, url.clone())
@@ -89,7 +89,7 @@ impl PubkyClient {
url.set_path("/session");
let body = AuthToken::sign(keypair, &audience, vec![Capability::pubky_root()]).serialize();
let body = AuthToken::sign(keypair, &audience, vec![Capability::root()]).serialize();
let response = self.request(Method::POST, url).body(body).send().await?;
@@ -125,7 +125,7 @@ mod tests {
.unwrap()
.unwrap();
assert!(session.capabilities.contains(&Capability::pubky_root()));
assert!(session.capabilities.contains(&Capability::root()));
client.signout(&keypair.public_key()).await.unwrap();
@@ -144,7 +144,7 @@ mod tests {
.unwrap()
.unwrap();
assert!(session.capabilities.contains(&Capability::pubky_root()));
assert!(session.capabilities.contains(&Capability::root()));
}
}
}