use core::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Scope {
Read,
Write,
Admin,
}
impl Scope {
pub const ALL: [Self; 3] = [Self::Read, Self::Write, Self::Admin];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
Self::Admin => "admin",
}
}
const fn bit(self) -> u128 {
match self {
Self::Read => 1,
Self::Write => 2,
Self::Admin => 4,
}
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct ScopeSet {
bits: u128,
}
impl ScopeSet {
#[must_use]
pub const fn empty() -> Self {
Self { bits: 0 }
}
#[must_use]
pub const fn all() -> Self {
Self { bits: 0b111 }
}
#[must_use]
pub const fn with(self, scope: Scope) -> Self {
Self {
bits: self.bits | scope.bit(),
}
}
#[must_use]
pub const fn contains(self, scope: Scope) -> bool {
self.bits & scope.bit() != 0
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.bits == 0
}
#[must_use]
pub const fn holds(self, required: Self) -> bool {
self.bits & required.bits == required.bits
}
#[must_use]
pub const fn difference(self, other: Self) -> Self {
Self {
bits: self.bits & !other.bits,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AuthContext {
principal: Arc<str>,
scopes: ScopeSet,
}
impl AuthContext {
#[must_use]
pub fn anonymous() -> Self {
Self {
principal: Arc::from("anonymous"),
scopes: ScopeSet::empty(),
}
}
#[must_use]
pub fn new(principal: impl Into<Arc<str>>, scopes: ScopeSet) -> Self {
Self {
principal: principal.into(),
scopes,
}
}
#[must_use]
pub fn unrestricted(principal: impl Into<Arc<str>>) -> Self {
Self::new(principal, ScopeSet::all())
}
#[must_use]
pub fn principal(&self) -> &str {
&self.principal
}
#[must_use]
pub const fn scopes(&self) -> ScopeSet {
self.scopes
}
#[must_use]
pub const fn holds(&self, required: ScopeSet) -> bool {
self.scopes.holds(required)
}
}
#[cfg(test)]
mod tests {
use super::{AuthContext, Scope, ScopeSet};
#[test]
fn the_default_holds_nothing() {
let context = AuthContext::default();
assert!(!context.holds(ScopeSet::empty().with(Scope::Read)));
assert!(context.holds(ScopeSet::empty()));
}
#[test]
fn scopes_do_not_imply_one_another() {
let write = ScopeSet::empty().with(Scope::Write);
assert!(!write.contains(Scope::Admin));
assert!(!write.contains(Scope::Read));
assert!(!write.holds(ScopeSet::all()));
}
#[test]
fn holds_is_the_subset_relation() {
let admin = ScopeSet::empty().with(Scope::Admin);
let read_write_admin = ScopeSet::empty()
.with(Scope::Read)
.with(Scope::Write)
.with(Scope::Admin);
assert!(read_write_admin.holds(admin));
assert!(admin.holds(admin));
assert!(!admin.holds(read_write_admin));
assert!(admin.holds(ScopeSet::empty()));
}
#[test]
fn difference_reports_what_is_missing() {
let missing = ScopeSet::empty()
.with(Scope::Read)
.with(Scope::Write)
.difference(ScopeSet::empty().with(Scope::Read));
assert!(missing.contains(Scope::Write));
assert!(!missing.contains(Scope::Read));
}
#[test]
fn anonymous_and_named_are_both_denial_by_default() {
let anonymous = AuthContext::anonymous();
assert_eq!(anonymous.principal(), "anonymous");
assert!(!anonymous.holds(ScopeSet::empty().with(Scope::Read)));
let named = AuthContext::new("alice", ScopeSet::empty());
assert_eq!(named.principal(), "alice");
assert!(!named.holds(ScopeSet::empty().with(Scope::Read)));
}
#[test]
fn unrestricted_holds_everything() {
let context = AuthContext::unrestricted("bob");
assert!(context.holds(ScopeSet::all()));
}
#[test]
fn every_scope_has_its_own_bit() {
for scope in Scope::ALL {
let set = ScopeSet::empty().with(scope);
assert!(set.contains(scope), "{scope} missing from its own set");
for other in Scope::ALL {
if other != scope {
assert!(!set.contains(other), "bit for {scope} also grants {other}");
}
}
}
}
}