use super::{Capability, Caveat};
use serde_json::{json, Value};
use std::fmt::Debug;
use url::Url;
pub trait Scope: ToString + TryFrom<Url> + PartialEq + Clone {
fn contains(&self, other: &Self) -> bool;
}
pub trait Ability: Ord + TryFrom<String> + ToString + Clone {}
#[derive(Clone, Eq, PartialEq)]
pub enum ResourceUri<S>
where
S: Scope,
{
Scoped(S),
Unscoped,
}
impl<S> ResourceUri<S>
where
S: Scope,
{
pub fn contains(&self, other: &Self) -> bool {
match self {
ResourceUri::Unscoped => true,
ResourceUri::Scoped(scope) => match other {
ResourceUri::Scoped(other_scope) => scope.contains(other_scope),
_ => false,
},
}
}
}
impl<S> ToString for ResourceUri<S>
where
S: Scope,
{
fn to_string(&self) -> String {
match self {
ResourceUri::Unscoped => "*".into(),
ResourceUri::Scoped(value) => value.to_string(),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub enum Resource<S>
where
S: Scope,
{
Resource { kind: ResourceUri<S> },
My { kind: ResourceUri<S> },
As { did: String, kind: ResourceUri<S> },
}
impl<S> Resource<S>
where
S: Scope,
{
pub fn contains(&self, other: &Self) -> bool {
match (self, other) {
(
Resource::Resource { kind: resource },
Resource::Resource {
kind: other_resource,
},
) => resource.contains(other_resource),
(
Resource::My { kind: resource },
Resource::My {
kind: other_resource,
},
) => resource.contains(other_resource),
(
Resource::As {
did,
kind: resource,
},
Resource::As {
did: other_did,
kind: other_resource,
},
) if did == other_did => resource.contains(other_resource),
_ => false,
}
}
}
impl<S> ToString for Resource<S>
where
S: Scope,
{
fn to_string(&self) -> String {
match self {
Resource::Resource { kind } => kind.to_string(),
Resource::My { kind } => format!("my:{}", kind.to_string()),
Resource::As { did, kind } => format!("as:{did}:{}", kind.to_string()),
}
}
}
pub trait CapabilitySemantics<S, A>
where
S: Scope,
A: Ability,
{
fn parse_scope(&self, scope: &Url) -> Option<S> {
S::try_from(scope.clone()).ok()
}
fn parse_action(&self, ability: &str) -> Option<A> {
A::try_from(String::from(ability)).ok()
}
fn extract_did(&self, path: &str) -> Option<(String, String)> {
let mut path_parts = path.split(':');
match path_parts.next() {
Some("did") => (),
_ => return None,
};
match path_parts.next() {
Some("key") => (),
_ => return None,
};
let value = match path_parts.next() {
Some(value) => value,
_ => return None,
};
Some((format!("did:key:{value}"), path_parts.collect()))
}
fn parse_resource(&self, resource: &Url) -> Option<ResourceUri<S>> {
Some(match resource.path() {
"*" => ResourceUri::Unscoped,
_ => ResourceUri::Scoped(self.parse_scope(resource)?),
})
}
fn parse_caveat(&self, caveat: Option<&Value>) -> Value {
if let Some(caveat) = caveat {
caveat.to_owned()
} else {
json!({})
}
}
fn parse(
&self,
resource: &str,
ability: &str,
caveat: Option<&Value>,
) -> Option<CapabilityView<S, A>> {
let uri = Url::parse(resource).ok()?;
let cap_resource = match uri.scheme() {
"my" => Resource::My {
kind: self.parse_resource(&uri)?,
},
"as" => {
let (did, resource) = self.extract_did(uri.path())?;
Resource::As {
did,
kind: self.parse_resource(&Url::parse(resource.as_str()).ok()?)?,
}
}
_ => Resource::Resource {
kind: self.parse_resource(&uri)?,
},
};
let cap_ability = match self.parse_action(ability) {
Some(ability) => ability,
None => return None,
};
let cap_caveat = self.parse_caveat(caveat);
Some(CapabilityView::new_with_caveat(
cap_resource,
cap_ability,
cap_caveat,
))
}
fn parse_capability(&self, value: &Capability) -> Option<CapabilityView<S, A>> {
self.parse(&value.resource, &value.ability, Some(&value.caveat))
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct CapabilityView<S, A>
where
S: Scope,
A: Ability,
{
pub resource: Resource<S>,
pub ability: A,
pub caveat: Value,
}
impl<S, A> Debug for CapabilityView<S, A>
where
S: Scope,
A: Ability,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Capability")
.field("resource", &self.resource.to_string())
.field("ability", &self.ability.to_string())
.field("caveats", &serde_json::to_string(&self.caveat))
.finish()
}
}
impl<S, A> CapabilityView<S, A>
where
S: Scope,
A: Ability,
{
pub fn new(resource: Resource<S>, ability: A) -> Self {
CapabilityView {
resource,
ability,
caveat: json!({}),
}
}
pub fn new_with_caveat(resource: Resource<S>, ability: A, caveat: Value) -> Self {
CapabilityView {
resource,
ability,
caveat,
}
}
pub fn enables(&self, other: &CapabilityView<S, A>) -> bool {
match (
Caveat::try_from(self.caveat()),
Caveat::try_from(other.caveat()),
) {
(Ok(self_caveat), Ok(other_caveat)) => {
self.resource.contains(&other.resource)
&& self.ability >= other.ability
&& self_caveat.enables(&other_caveat)
}
_ => false,
}
}
pub fn resource(&self) -> &Resource<S> {
&self.resource
}
pub fn ability(&self) -> &A {
&self.ability
}
pub fn caveat(&self) -> &Value {
&self.caveat
}
}
impl<S, A> From<&CapabilityView<S, A>> for Capability
where
S: Scope,
A: Ability,
{
fn from(value: &CapabilityView<S, A>) -> Self {
Capability::new(
value.resource.to_string(),
value.ability.to_string(),
value.caveat.to_owned(),
)
}
}
impl<S, A> From<CapabilityView<S, A>> for Capability
where
S: Scope,
A: Ability,
{
fn from(value: CapabilityView<S, A>) -> Self {
Capability::new(
value.resource.to_string(),
value.ability.to_string(),
value.caveat,
)
}
}