Skip to main content

oas_document/
visibility.rs

1// OAS Spec §5.7 — Field Visibility
2//! Visibility levels for access-controlled fields in OAS Identity Documents.
3//!
4//! DID documents are stored on the public Weave DHT. Fields marked with a
5//! visibility level other than [`Visibility::Public`] are redacted in the
6//! DHT-stored copy. The full values are served via an authenticated
7//! `OASPrivateEndpointService` service endpoint on the entity.
8//!
9//! # Access Control Model
10//!
11//! | Level          | Who Can Read                          | DHT Behavior               |
12//! |----------------|---------------------------------------|----------------------------|
13//! | `public`       | Anyone resolving the DID              | Full value stored           |
14//! | `authenticated`| Any agent proving identity via DID-Auth| Capability flag only        |
15//! | `organization` | Same AO/ENR members (lineage check)   | Capability flag only        |
16//! | `private`      | Only the entity itself                | Never stored on DHT         |
17
18use std::fmt;
19use std::str::FromStr;
20
21use serde::{Deserialize, Serialize};
22
23/// Visibility level for access-controlled fields.
24///
25/// Controls who can read a field's actual value versus seeing only a
26/// capability advertisement (e.g., "this agent has a calendar" without
27/// revealing the booking URL).
28///
29/// # Examples
30///
31/// ```
32/// use oas_document::visibility::Visibility;
33///
34/// let vis = Visibility::Organization;
35/// assert_eq!(vis.as_str(), "organization");
36/// assert!(!vis.is_public());
37/// ```
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum Visibility {
41    /// Visible to anyone resolving the DID. Full value stored on DHT.
42    #[default]
43    Public,
44
45    /// Visible to any agent that can prove identity via DID-Auth.
46    /// DHT stores only a capability flag; actual value served via
47    /// authenticated endpoint.
48    Authenticated,
49
50    /// Visible only to members of the same Autonomous Organization or
51    /// Entity Name Record (verified via lineage chain).
52    /// DHT stores only a capability flag.
53    Organization,
54
55    /// Visible only to the entity itself. Never stored on the DHT.
56    Private,
57}
58
59impl Visibility {
60    /// Returns the string representation.
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            Self::Public => "public",
64            Self::Authenticated => "authenticated",
65            Self::Organization => "organization",
66            Self::Private => "private",
67        }
68    }
69
70    /// Returns `true` if this field should be fully visible on the public DHT.
71    pub fn is_public(&self) -> bool {
72        matches!(self, Self::Public)
73    }
74
75    /// Returns `true` if this field should be redacted (capability flag only)
76    /// or hidden entirely in the DHT-stored copy.
77    pub fn is_redacted_on_dht(&self) -> bool {
78        !self.is_public()
79    }
80}
81
82impl fmt::Display for Visibility {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str(self.as_str())
85    }
86}
87
88impl FromStr for Visibility {
89    type Err = String;
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        match s {
93            "public" => Ok(Self::Public),
94            "authenticated" => Ok(Self::Authenticated),
95            "organization" => Ok(Self::Organization),
96            "private" => Ok(Self::Private),
97            _ => Err(format!(
98                "unknown visibility '{s}'; expected one of: public, authenticated, organization, private"
99            )),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn default_is_public() {
110        assert_eq!(Visibility::default(), Visibility::Public);
111    }
112
113    #[test]
114    fn public_is_not_redacted() {
115        assert!(!Visibility::Public.is_redacted_on_dht());
116    }
117
118    #[test]
119    fn non_public_is_redacted() {
120        assert!(Visibility::Authenticated.is_redacted_on_dht());
121        assert!(Visibility::Organization.is_redacted_on_dht());
122        assert!(Visibility::Private.is_redacted_on_dht());
123    }
124
125    #[test]
126    fn round_trip_all_variants() {
127        for vis in [
128            Visibility::Public,
129            Visibility::Authenticated,
130            Visibility::Organization,
131            Visibility::Private,
132        ] {
133            let s = vis.as_str();
134            let parsed = Visibility::from_str(s);
135            assert!(parsed.is_ok());
136            assert_eq!(parsed.unwrap_or(Visibility::Public), vis);
137        }
138    }
139
140    #[test]
141    fn serde_round_trip() {
142        let vis = Visibility::Organization;
143        let json = serde_json::to_string(&vis).unwrap_or_default();
144        assert_eq!(json, r#""organization""#);
145        let parsed: Visibility = serde_json::from_str(&json).unwrap_or(Visibility::Public);
146        assert_eq!(parsed, vis);
147    }
148
149    #[test]
150    fn unknown_returns_error() {
151        assert!(Visibility::from_str("secret").is_err());
152    }
153}