Skip to content

Commit

Permalink
add scope primitive
Browse files Browse the repository at this point in the history
  • Loading branch information
aumetra committed Dec 19, 2024
1 parent ca4c5af commit e99002a
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 43 deletions.
18 changes: 17 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ chrono = { version = "0.4.39", default-features = false }
clap = { version = "4.5.23", features = ["derive", "wrap_help"] }
color-eyre = "0.6.3"
colored_json = "5.0.0"
compact_str = { version = "0.8.0", features = ["serde"] }
const_format = "0.2.34"
const-oid = { version = "0.9.6", features = ["db"] }
cookie = { version = "0.18.1", features = ["percent-encode"] }
Expand Down
1 change: 1 addition & 0 deletions lib/komainu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ harness = false
[dependencies]
base64-simd.workspace = true
bytes.workspace = true
compact_str.workspace = true
http.workspace = true
memchr.workspace = true
serde.workspace = true
Expand Down
23 changes: 7 additions & 16 deletions lib/komainu/src/code_grant.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
use crate::{
error::Error, flow::pkce, params::ParamStorage, AuthInstruction, Client, ClientExtractor,
};
use std::{
borrow::{Borrow, Cow},
collections::HashSet,
future::Future,
ops::Deref,
str::FromStr,
error::Error, flow::pkce, params::ParamStorage, primitive::Scopes, AuthInstruction, Client,
ClientExtractor,
};
use std::{borrow::Cow, future::Future, ops::Deref, str::FromStr};
use strum::{AsRefStr, Display};
use thiserror::Error;

Expand Down Expand Up @@ -88,15 +83,11 @@ where
return Err(GrantError::AccessDenied);
}

let request_scopes = scope.split_whitespace().collect::<HashSet<_>>();
let client_scopes = client
.scopes
.iter()
.map(Borrow::borrow)
.collect::<HashSet<_>>();
let request_scopes = scope.split_whitespace().collect::<Scopes>();
let client_scopes = client.scopes.iter().map(Deref::deref).collect::<Scopes>();

if !request_scopes.is_subset(&client_scopes) {
debug!(?client_id, "scopes aren't a subset");
if !client_scopes.can_perform(&request_scopes) {
debug!(?client_id, "client can't issue the requested scopes");
return Err(GrantError::AccessDenied);
}

Expand Down
16 changes: 3 additions & 13 deletions lib/komainu/src/flow/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ pub trait Issuer {
pub async fn perform<CE, I>(
req: http::Request<Bytes>,
client_extractor: CE,
token_issuer: I,
) -> Result<http::Response<Bytes>, flow::Error>
token_issuer: &I,
) -> Result<TokenResponse<'_>, flow::Error>
where
CE: ClientExtractor,
I: Issuer,
Expand Down Expand Up @@ -70,15 +70,5 @@ where
pkce.verify(code_verifier)?;
}

let token = token_issuer.issue_token(&authorization).await?;
let body = sonic_rs::to_vec(&token).unwrap();

debug!("token successfully issued. building response");

let response = http::Response::builder()
.status(http::StatusCode::OK)
.body(body.into())
.unwrap();

Ok(response)
token_issuer.issue_token(&authorization).await
}
16 changes: 3 additions & 13 deletions lib/komainu/src/flow/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ pub trait Issuer {
pub async fn perform<CE, I>(
req: http::Request<Bytes>,
client_extractor: CE,
token_issuer: I,
) -> Result<http::Response<Bytes>, flow::Error>
token_issuer: &I,
) -> Result<TokenResponse<'_>, flow::Error>
where
CE: ClientExtractor,
I: Issuer,
Expand All @@ -46,15 +46,5 @@ where
.extract(client_id, Some(client_secret))
.await?;

let token = token_issuer.issue_token(&client, refresh_token).await?;
let body = sonic_rs::to_vec(&token).unwrap();

debug!("token successfully issued. building response");

let response = http::Response::builder()
.status(http::StatusCode::OK)
.body(body.into())
.unwrap();

Ok(response)
token_issuer.issue_token(&client, refresh_token).await
}
1 change: 1 addition & 0 deletions lib/komainu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod error;
pub mod extract;
pub mod flow;
pub mod params;
pub mod primitive;

pub struct Authorization<'a> {
pub code: Cow<'a, str>,
Expand Down
3 changes: 3 additions & 0 deletions lib/komainu/src/primitive/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod scope;

pub use self::scope::Scopes;
82 changes: 82 additions & 0 deletions lib/komainu/src/primitive/scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use compact_str::CompactString;
use serde::Deserialize;
use std::{
collections::{hash_set, HashSet},
convert::Infallible,
str::FromStr,
};

#[derive(Default, Deserialize)]
#[serde(transparent)]
pub struct Scopes {
inner: HashSet<CompactString>,
}

impl FromStr for Scopes {
type Err = Infallible;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.split_whitespace().collect())
}
}

impl Scopes {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}

#[inline]
pub fn insert<Item>(&mut self, item: Item)
where
Item: Into<CompactString>,
{
self.inner.insert(item.into());
}

/// Determine whether `self` can be accessed by `resource`
///
/// This implies that `resource` is equal to or a superset of `self`
#[inline]
#[must_use]
pub fn can_be_accessed_by(&self, resource: &Self) -> bool {
resource.inner.is_superset(&self.inner)
}

/// Determine whether `self` is allowed to perform an action
/// for which you at least need `resource` scope
#[inline]
#[must_use]
pub fn can_perform(&self, resource: &Self) -> bool {
self.inner.is_superset(&resource.inner)
}

#[inline]
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.inner.iter().map(CompactString::as_str)
}
}

impl<Item> FromIterator<Item> for Scopes
where
Item: Into<CompactString>,
{
#[inline]
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
let mut collection = Self::new();
for item in iter {
collection.insert(item.into());
}
collection
}
}

impl IntoIterator for Scopes {
type Item = CompactString;
type IntoIter = hash_set::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
47 changes: 47 additions & 0 deletions lib/komainu/tests/scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use komainu::primitive::Scopes;
use rstest::rstest;

#[rstest]
#[case("read", "read write")]
#[case("read write", "read write")]
#[case("read write follow", "read write follow push")]
fn can_perform(#[case] request: &str, #[case] client: &str) {
let request: Scopes = request.parse().unwrap();
let client: Scopes = client.parse().unwrap();

assert!(client.can_perform(&request));
}

#[rstest]
#[case("read write", "read")]
#[case("read follow", "write")]
#[case("write push", "read")]
fn cant_perform(#[case] request: &str, #[case] client: &str) {
let request: Scopes = request.parse().unwrap();
let client: Scopes = client.parse().unwrap();

assert!(!client.can_perform(&request));
}

#[rstest]
#[case("read", "read write")]
#[case("read", "read")]
#[case("follow", "read follow")]
#[case("write follow", "follow write")]
fn can_access(#[case] endpoint: &str, #[case] client: &str) {
let endpoint: Scopes = endpoint.parse().unwrap();
let client: Scopes = client.parse().unwrap();

assert!(endpoint.can_be_accessed_by(&client));
}

#[rstest]
#[case("read write", "write")]
#[case("follow", "read write")]
#[case("write follow", "read follow")]
fn cant_access(#[case] endpoint: &str, #[case] client: &str) {
let endpoint: Scopes = endpoint.parse().unwrap();
let client: Scopes = client.parse().unwrap();

assert!(!endpoint.can_be_accessed_by(&client));
}

0 comments on commit e99002a

Please sign in to comment.