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}