use std::fmt;
use super::handle::CredentialRefError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Principal {
Operator,
Service(ServiceId),
}
impl fmt::Display for Principal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Operator => f.write_str("operator"),
Self::Service(id) => write!(f, "service:{id}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ServiceId(String);
impl ServiceId {
pub fn parse(text: &str) -> Result<Self, CredentialRefError> {
let as_ref = super::CredentialRef::parse(text)?;
if as_ref.qualifier().is_some() {
return Err(CredentialRefError::Shape);
}
Ok(Self(text.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ServiceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Access {
Read,
Write,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scope {
access: Access,
provider_scopes: Vec<String>,
}
impl Scope {
pub fn read() -> Self {
Self {
access: Access::Read,
provider_scopes: Vec::new(),
}
}
pub fn write() -> Self {
Self {
access: Access::Write,
provider_scopes: Vec::new(),
}
}
pub fn with_provider_scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.provider_scopes = scopes.into_iter().map(Into::into).collect();
self
}
pub fn access(&self) -> Access {
self.access
}
pub fn provider_scopes(&self) -> &[String] {
&self.provider_scopes
}
pub fn covers(&self, requested: &Scope) -> bool {
if requested.access == Access::Write && self.access == Access::Read {
return false;
}
requested
.provider_scopes
.iter()
.all(|s| self.provider_scopes.contains(s))
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let access = match self.access {
Access::Read => "read",
Access::Write => "write",
};
if self.provider_scopes.is_empty() {
f.write_str(access)
} else {
write!(f, "{access}[{}]", self.provider_scopes.join(" "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn principal_renders_its_full_shape() {
assert_eq!(Principal::Operator.to_string(), "operator");
let svc = Principal::Service(ServiceId::parse("trusty-search").unwrap());
assert_eq!(svc.to_string(), "service:trusty-search");
assert!(format!("{svc:?}").contains("trusty-search"));
}
#[test]
fn service_id_rejects_out_of_grammar_text() {
assert!(ServiceId::parse("trusty-search").is_ok());
assert!(ServiceId::parse("Trusty-Search").is_err());
assert!(ServiceId::parse("trusty_search").is_err());
assert!(ServiceId::parse("").is_err());
assert_eq!(
ServiceId::parse("trusty/search").unwrap_err(),
CredentialRefError::Shape
);
}
#[test]
fn scope_read_is_covered_by_write() {
assert!(Scope::write().covers(&Scope::read()));
assert!(Scope::write().covers(&Scope::write()));
assert!(Scope::read().covers(&Scope::read()));
assert!(!Scope::read().covers(&Scope::write()));
}
#[test]
fn provider_scopes_must_all_be_covered() {
let granted = Scope::read().with_provider_scopes(["gmail.readonly", "calendar.readonly"]);
assert!(granted.covers(&Scope::read().with_provider_scopes(["gmail.readonly"])));
assert!(granted.covers(&Scope::read()));
assert!(!granted.covers(&Scope::read().with_provider_scopes(["drive.readonly"])));
assert!(!Scope::read().covers(&Scope::read().with_provider_scopes(["gmail.readonly"])));
assert_eq!(
Scope::read()
.with_provider_scopes(["gmail.readonly"])
.to_string(),
"read[gmail.readonly]"
);
}
}