use crate::error::RejectReason;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AuthorityValue {
One(String),
Many(Vec<String>),
}
impl AuthorityValue {
pub fn validate(&self, key: &str) -> Result<(), RejectReason> {
let ok = match self {
AuthorityValue::One(s) => !s.is_empty(),
AuthorityValue::Many(v) => !v.is_empty() && v.iter().all(|s| !s.is_empty()),
};
if ok {
Ok(())
} else {
Err(RejectReason::InvalidAuthorityValue(key.to_string()))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Invariant {
pub scope: String,
pub operation: String,
pub resource_type: String,
pub resource_id: String,
}
impl Invariant {
pub fn new(
scope: impl Into<String>,
operation: impl Into<String>,
resource_type: impl Into<String>,
resource_id: impl Into<String>,
) -> Self {
Self {
scope: scope.into(),
operation: operation.into(),
resource_type: resource_type.into(),
resource_id: resource_id.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct LogicalExecution {
pub invariants: Vec<Invariant>,
pub contract: BTreeMap<String, AuthorityValue>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct LogicalAuthority {
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_context: Option<BTreeMap<String, AuthorityValue>>,
pub execution: LogicalExecution,
}
impl LogicalAuthority {
pub fn new(
identity_context: Option<BTreeMap<String, AuthorityValue>>,
invariants: Vec<Invariant>,
contract: BTreeMap<String, AuthorityValue>,
) -> Self {
Self {
identity_context,
execution: LogicalExecution {
invariants,
contract,
},
}
}
pub fn validate(&self) -> Result<(), RejectReason> {
if self.execution.contract.is_empty() {
return Err(RejectReason::EmptyExecutionContract);
}
for (k, v) in &self.execution.contract {
v.validate(k)?;
}
if let Some(identity) = &self.identity_context {
for (k, v) in identity {
v.validate(k)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::authority::indexed::{IndexedAuthorityMap, InvariantTuple, TupleValue};
#[test]
fn logical_json_matches_reference_walkthrough() {
let json = r#"{
"execution": {
"invariants": [
{
"scope": "documents:read:document-42",
"operation": "read",
"resourceType": "documents",
"resourceId": "document-42"
},
{
"scope": "storage:save",
"operation": "save",
"resourceType": "storage",
"resourceId": "*"
}
],
"contract": {
"corporation": "ACME",
"department": "sensitive-documents"
}
}
}"#;
let logical: LogicalAuthority = serde_json::from_str(json).unwrap();
assert!(logical.identity_context.is_none());
let map = IndexedAuthorityMap::from_logical(&logical).unwrap();
assert_eq!(
map.invariants[&0],
InvariantTuple(
"documents:read:document-42".into(),
"read".into(),
"documents".into(),
"document-42".into()
)
);
assert_eq!(
map.invariants[&1],
InvariantTuple(
"storage:save".into(),
"save".into(),
"storage".into(),
"*".into()
)
);
assert_eq!(
map.execution_contract[&0],
("corporation".into(), TupleValue::Text("ACME".into()))
);
assert_eq!(
map.execution_contract[&1],
(
"department".into(),
TupleValue::Text("sensitive-documents".into())
)
);
let back = serde_json::to_value(&logical).unwrap();
assert_eq!(
back["execution"]["invariants"][0]["resourceType"],
"documents"
);
assert_eq!(
back["execution"]["invariants"][0]["resourceId"],
"document-42"
);
assert_eq!(back["execution"]["contract"]["corporation"], "ACME");
}
}