holochain_integrity_types/capability/
grant.rs

1use super::CapSecret;
2use crate::zome::FunctionName;
3use crate::zome::ZomeName;
4use holo_hash::*;
5use serde::Deserialize;
6use serde::Serialize;
7use std::collections::BTreeSet;
8
9/// Represents a _potentially_ valid access grant to a zome call.
10/// Zome call response will be Unauthorized without a valid grant.
11///
12/// The CapGrant is not always a dedicated entry in the chain.
13/// Notably AgentPubKey entries in the current chain act like root access to local zome calls.
14///
15/// A `CapGrant` is valid if it matches the function, agent and secret for a given zome call.
16///
17/// See `.is_valid()`
18#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
19#[allow(clippy::large_enum_variant)]
20pub enum CapGrant {
21    /// Grants the capability of calling every extern to the calling agent, provided the calling
22    /// agent is the local chain author.
23    /// This grant is compared to the current `Entry::Agent` entry on the source chain.
24    ChainAuthor(AgentPubKey),
25
26    /// Any agent other than the chain author is attempting to call an extern.
27    /// The pubkey of the calling agent is secured by the cryptographic handshake at the network
28    /// layer and the caller must provide a secret that we check for in a private entry in the
29    /// local chain.
30    RemoteAgent(ZomeCallCapGrant),
31}
32
33impl From<holo_hash::AgentPubKey> for CapGrant {
34    fn from(agent_hash: holo_hash::AgentPubKey) -> Self {
35        CapGrant::ChainAuthor(agent_hash)
36    }
37}
38
39/// The entry for the ZomeCall capability grant.
40/// This data is committed to the callee's source chain as a private entry.
41/// The remote calling agent must provide a secret and we source their pubkey from the active
42/// network connection. This must match the strictness of the CapAccess.
43#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
44pub struct ZomeCallCapGrant {
45    /// A string by which to later query for saved grants.
46    /// This does not need to be unique within a source chain.
47    pub tag: String,
48    /// Specifies who may claim this capability, and by what means
49    pub access: CapAccess,
50    /// Set of functions to which this capability grants ZomeCall access
51    pub functions: GrantedFunctions,
52    // @todo the payloads to curry to the functions
53    // pub curry_payloads: CurryPayloads,
54}
55
56/// The outbound DTO of a ZomeCall capability grant info request.
57/// CapAccess secrets are omitted, Access types and assignees are provided under CapAccessInfo.
58#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
59pub struct DesensitizedZomeCallCapGrant {
60    /// A string by which to later query for saved grants.
61    /// This does not need to be unique within a source chain.
62    pub tag: String,
63    /// Specifies who may claim this capability, and by what means omitting secrets
64    pub access: CapAccessInfo,
65    /// Set of functions to which this capability grants ZomeCall access
66    pub functions: GrantedFunctions,
67}
68
69impl From<ZomeCallCapGrant> for DesensitizedZomeCallCapGrant {
70    /// Create a new Desensitized ZomeCall capability grant
71    fn from(zccg: ZomeCallCapGrant) -> Self {
72        DesensitizedZomeCallCapGrant {
73            tag: zccg.tag,
74            access: CapAccessInfo {
75                access_type: zccg.access.as_variant_string().to_string(),
76                assignees: match &zccg.access {
77                    CapAccess::Assigned { assignees, .. } => Some(assignees.clone()),
78                    _ => None,
79                },
80            },
81            functions: zccg.functions,
82        }
83    }
84}
85
86impl ZomeCallCapGrant {
87    /// Constructor
88    pub fn new(
89        tag: String,
90        access: CapAccess,
91        functions: GrantedFunctions,
92        // @todo curry_payloads: CurryPayloads,
93    ) -> Self {
94        Self {
95            tag,
96            access,
97            functions,
98            // @todo curry_payloads,
99        }
100    }
101}
102
103impl From<ZomeCallCapGrant> for CapGrant {
104    /// Create a new ZomeCall capability grant
105    fn from(zccg: ZomeCallCapGrant) -> Self {
106        CapGrant::RemoteAgent(zccg)
107    }
108}
109
110impl CapGrant {
111    /// Given a grant, is it valid in isolation?
112    /// In a world of CRUD, some new entry might update or delete an existing one, but we can check
113    /// if a grant is valid in a standalone way.
114    pub fn is_valid(
115        &self,
116        given_function: &GrantedFunction,
117        given_agent: &AgentPubKey,
118        given_secret: Option<&CapSecret>,
119    ) -> bool {
120        match self {
121            // Grant is always valid if the author matches the check agent.
122            CapGrant::ChainAuthor(author) => author == given_agent,
123            // Otherwise we need to do more work…
124            CapGrant::RemoteAgent(ZomeCallCapGrant {
125                access, functions, ..
126            }) => {
127                // The checked function needs to be in the grant…
128                let granted = match functions {
129                    GrantedFunctions::All => true,
130                    GrantedFunctions::Listed(fns) => fns.contains(given_function),
131                };
132                granted
133                // The agent needs to be valid…
134                && match access {
135                    // The grant is assigned so the agent needs to match…
136                    CapAccess::Assigned { assignees, .. } => assignees.contains(given_agent),
137                    // The grant has no assignees so is always valid…
138                    _ => true,
139                }
140                // The secret needs to match…
141                && match access {
142                    // Unless the extern is unrestricted.
143                    CapAccess::Unrestricted => true,
144                    // note the PartialEq implementation is constant time for secrets
145                    CapAccess::Transferable { secret, .. } => given_secret.map(|given| secret == given).unwrap_or(false),
146                    CapAccess::Assigned { secret, .. } => given_secret.map(|given| secret == given).unwrap_or(false),
147                }
148            }
149        }
150    }
151}
152
153/// Represents access requirements for capability grants.
154#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
155#[serde(tag = "type", content = "value", rename_all = "snake_case")]
156pub enum CapAccess {
157    /// No restriction: callable by anyone.
158    Unrestricted,
159    /// Callable by anyone who can provide the secret.
160    Transferable {
161        /// The secret.
162        secret: CapSecret,
163    },
164    /// Callable by anyone in the list of assignees who possesses the secret.
165    Assigned {
166        /// The secret.
167        secret: CapSecret,
168        /// Agents who can use this grant.
169        assignees: BTreeSet<AgentPubKey>,
170    },
171}
172
173/// Implements ().into() shorthand for CapAccess::Unrestricted
174impl From<()> for CapAccess {
175    fn from(_: ()) -> Self {
176        Self::Unrestricted
177    }
178}
179
180/// Implements secret.into() shorthand for CapAccess::Transferable(secret)
181impl From<CapSecret> for CapAccess {
182    fn from(secret: CapSecret) -> Self {
183        Self::Transferable { secret }
184    }
185}
186
187/// Implements (secret, assignees).into() shorthand for CapAccess::Assigned { secret, assignees }
188impl From<(CapSecret, BTreeSet<AgentPubKey>)> for CapAccess {
189    fn from((secret, assignees): (CapSecret, BTreeSet<AgentPubKey>)) -> Self {
190        Self::Assigned { secret, assignees }
191    }
192}
193
194/// Implements (secret, agent_pub_key).into() shorthand for
195/// CapAccess::Assigned { secret, assignees: hashset!{ agent } }
196impl From<(CapSecret, AgentPubKey)> for CapAccess {
197    fn from((secret, assignee): (CapSecret, AgentPubKey)) -> Self {
198        let mut assignees = BTreeSet::new();
199        assignees.insert(assignee);
200        Self::from((secret, assignees))
201    }
202}
203
204impl CapAccess {
205    /// Return variant denominator as string slice
206    pub fn as_variant_string(&self) -> &str {
207        match self {
208            CapAccess::Unrestricted => "unrestricted",
209            CapAccess::Transferable { .. } => "transferable",
210            CapAccess::Assigned { .. } => "assigned",
211        }
212    }
213}
214
215/// Represents access info for capability grants .
216#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
217pub struct CapAccessInfo {
218    /// The access type.
219    access_type: String,
220    /// Agents who can use this grant.
221    assignees: Option<BTreeSet<AgentPubKey>>,
222}
223
224/// a single zome/function pair
225pub type GrantedFunction = (ZomeName, FunctionName);
226/// A collection of zome/function pairs
227
228#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
229#[serde(tag = "type", content = "value", rename_all = "snake_case")]
230pub enum GrantedFunctions {
231    /// grant all zomes all functions
232    All,
233    /// grant to specified zomes and functions
234    Listed(BTreeSet<GrantedFunction>),
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn cap_grant_is_valid() {
243        let agent1 = AgentPubKey::from_raw_36(vec![1; 36]);
244        let agent2 = AgentPubKey::from_raw_36(vec![2; 36]);
245        let assignees: BTreeSet<_> = [agent1.clone()].into_iter().collect();
246        let secret: CapSecret = [1; 64].into();
247        let secret_wrong: CapSecret = [2; 64].into();
248        let tag = "tag".to_string();
249
250        let g1: CapGrant = ZomeCallCapGrant {
251            tag: tag.clone(),
252            access: CapAccess::Transferable { secret },
253            functions: GrantedFunctions::All,
254        }
255        .into();
256
257        let g2: CapGrant = ZomeCallCapGrant {
258            tag: tag.clone(),
259            access: CapAccess::Assigned {
260                secret,
261                assignees: assignees.clone(),
262            },
263            functions: GrantedFunctions::All,
264        }
265        .into();
266
267        assert!(g1.is_valid(
268            &(ZomeName("zome".into()), FunctionName("fn".into())),
269            &agent1,
270            Some(&secret),
271        ));
272
273        assert!(g1.is_valid(
274            &(ZomeName("zome".into()), FunctionName("fn".into())),
275            &agent2,
276            Some(&secret),
277        ));
278
279        assert!(!g1.is_valid(
280            &(ZomeName("zome".into()), FunctionName("fn".into())),
281            &agent1,
282            Some(&secret_wrong),
283        ));
284
285        assert!(!g1.is_valid(
286            &(ZomeName("zome".into()), FunctionName("fn".into())),
287            &agent1,
288            None,
289        ));
290
291        assert!(g2.is_valid(
292            &(ZomeName("zome".into()), FunctionName("fn".into())),
293            &agent1,
294            Some(&secret),
295        ));
296
297        assert!(!g2.is_valid(
298            &(ZomeName("zome".into()), FunctionName("fn".into())),
299            &agent2,
300            Some(&secret),
301        ));
302
303        assert!(!g2.is_valid(
304            &(ZomeName("zome".into()), FunctionName("fn".into())),
305            &agent1,
306            None,
307        ));
308
309        assert!(!g2.is_valid(
310            &(ZomeName("zome".into()), FunctionName("fn".into())),
311            &agent1,
312            Some(&secret_wrong),
313        ));
314    }
315}