use std::{collections::hash_set, iter::FromIterator, str::FromStr};
use ahash::AHashSet;
use aliri::jwt;
use aliri_braid::braid;
use aliri_clock::UnixTime;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InvalidScopeToken {
#[error("scope token cannot be empty")]
EmptyString,
#[error("invalid scope token byte at position {position}: 0x{value:02x}")]
InvalidByte {
position: usize,
value: u8,
},
}
#[braid(
serde,
validator,
ref_doc = "A borrowed reference to an OAuth2 [`ScopeToken`]"
)]
pub struct ScopeToken;
impl aliri_braid::Validator for ScopeToken {
type Error = InvalidScopeToken;
fn validate(s: &str) -> Result<(), Self::Error> {
if s.is_empty() {
Err(InvalidScopeToken::EmptyString)
} else if let Some((position, &value)) = s
.as_bytes()
.iter()
.enumerate()
.find(|(_, &b)| b <= 0x20 || b == 0x22 || b == 0x5C || 0x7F <= b)
{
Err(InvalidScopeToken::InvalidByte { position, value })
} else {
Ok(())
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
enum ScopeDto {
String(String),
Array(Vec<ScopeToken>),
}
impl TryFrom<Option<ScopeDto>> for Scope {
type Error = InvalidScopeToken;
fn try_from(dto: Option<ScopeDto>) -> Result<Self, Self::Error> {
if let Some(dto) = dto {
match dto {
ScopeDto::String(s) => Self::try_from(s),
ScopeDto::Array(arr) => Ok(arr.into_iter().collect()),
}
} else {
Ok(Self::empty())
}
}
}
impl From<Scope> for ScopeDto {
fn from(s: Scope) -> Self {
let x: Vec<_> = s.0.into_iter().map(ScopeToken::into_string).collect();
let y = x.join(" ");
ScopeDto::String(y)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(try_from = "Option<ScopeDto>", into = "ScopeDto")]
pub struct Scope(AHashSet<ScopeToken>);
lazy_static::lazy_static! {
static ref EMPTY_SCOPE: Scope = Scope::empty();
}
impl Scope {
#[inline]
pub fn empty() -> Self {
Self(AHashSet::new())
}
#[inline]
pub fn single(scope_token: ScopeToken) -> Self {
let mut s = Self::empty();
s.insert(scope_token);
s
}
#[inline]
pub fn and(self, scope_token: ScopeToken) -> Self {
let mut s = self;
s.insert(scope_token);
s
}
#[inline]
pub fn from_scope_tokens<I>(scope_tokens: I) -> Self
where
I: IntoIterator<Item = ScopeToken>,
{
Self::from_iter(scope_tokens)
}
#[inline]
pub fn insert(&mut self, scope_token: ScopeToken) {
self.0.insert(scope_token);
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &ScopeTokenRef> {
self.into_iter()
}
#[inline]
pub fn contains_all(&self, subset: &Scope) -> bool {
self.0.is_superset(&subset.0)
}
}
impl IntoIterator for Scope {
type Item = ScopeToken;
type IntoIter = <AHashSet<ScopeToken> as IntoIterator>::IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Clone, Debug)]
pub struct Iter<'a> {
iter: hash_set::Iter<'a, ScopeToken>,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a ScopeTokenRef;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|x| x.as_ref())
}
}
impl<'a> IntoIterator for &'a Scope {
type Item = &'a ScopeTokenRef;
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter {
iter: self.0.iter(),
}
}
}
impl<S> Extend<S> for Scope
where
S: Into<ScopeToken>,
{
#[inline]
fn extend<I>(&mut self, iter: I)
where
I: IntoIterator<Item = S>,
{
self.0.extend(iter.into_iter().map(Into::into))
}
}
impl<S> FromIterator<S> for Scope
where
S: Into<ScopeToken>,
{
#[inline]
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = S>,
{
let mut set = Self::empty();
set.extend(iter);
set
}
}
impl TryFrom<&'_ str> for Scope {
type Error = InvalidScopeToken;
#[inline]
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.split_whitespace().map(ScopeToken::new).collect()
}
}
impl TryFrom<String> for Scope {
type Error = InvalidScopeToken;
#[inline]
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::try_from(s.as_str())
}
}
impl FromStr for Scope {
type Err = InvalidScopeToken;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BasicClaimsWithScope {
#[serde(flatten)]
pub basic: jwt::BasicClaims,
pub scope: Scope,
}
impl jwt::CoreClaims for BasicClaimsWithScope {
#[inline]
fn nbf(&self) -> Option<UnixTime> {
self.basic.nbf()
}
#[inline]
fn exp(&self) -> Option<UnixTime> {
self.basic.exp()
}
#[inline]
fn aud(&self) -> &jwt::Audiences {
self.basic.aud()
}
#[inline]
fn iss(&self) -> Option<&jwt::IssuerRef> {
self.basic.iss()
}
#[inline]
fn sub(&self) -> Option<&jwt::SubjectRef> {
self.basic.sub()
}
}
pub trait HasScope {
fn scope(&self) -> &Scope;
}
impl HasScope for BasicClaimsWithScope {
#[inline]
fn scope(&self) -> &Scope {
&self.scope
}
}
impl HasScope for Scope {
#[inline]
fn scope(&self) -> &Scope {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn owned_handles_valid() {
let x = ScopeToken::new("https://crates.io/scopes/publish:crate").unwrap();
assert_eq!(x.as_str(), "https://crates.io/scopes/publish:crate");
}
#[test]
fn owned_rejects_empty() {
let x = ScopeToken::new("");
assert!(matches!(x, Err(InvalidScopeToken::EmptyString)));
}
#[test]
fn owned_rejects_invalid_quote() {
let x = ScopeToken::new("https://crates.io/scopes/\"publish:crate\"");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn owned_rejects_invalid_control() {
let x = ScopeToken::new("https://crates.io/scopes/\tpublish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn owned_rejects_invalid_backslash() {
let x = ScopeToken::new("https://crates.io/scopes/\\publish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn owned_rejects_invalid_delete() {
let x = ScopeToken::new("https://crates.io/scopes/\x7Fpublish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn owned_rejects_invalid_non_ascii() {
let x = ScopeToken::new("https://crates.io/scopes/¿publish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn owned_rejects_invalid_emoji() {
let x = ScopeToken::new("https://crates.io/scopes/🪤publish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn ref_handles_valid() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/publish:crate").unwrap();
assert_eq!(x.as_str(), "https://crates.io/scopes/publish:crate");
}
#[test]
fn ref_rejects_empty() {
let x = ScopeTokenRef::from_str("");
assert!(matches!(x, Err(InvalidScopeToken::EmptyString)));
}
#[test]
fn ref_rejects_invalid_quote() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/\"publish:crate\"");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn ref_rejects_invalid_control() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/\tpublish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn ref_rejects_invalid_backslash() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/\\publish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn ref_rejects_invalid_delete() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/\x7Fpublish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn ref_rejects_invalid_non_ascii() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/¿publish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
#[test]
fn ref_rejects_invalid_emoji() {
let x = ScopeTokenRef::from_str("https://crates.io/scopes/🪤publish:crate");
assert!(matches!(x, Err(InvalidScopeToken::InvalidByte { .. })));
}
}