atomic_lib/
hierarchy.rs

1//! The Hierarchy model describes how Resources are structured in a tree-like shape.
2//! It deals with authorization (read / write permissions, rights, grants)
3//! See
4
5use core::fmt;
6
7use crate::{agents::ForAgent, errors::AtomicResult, storelike::Query, urls, Resource, Storelike};
8
9#[derive(Debug)]
10pub enum Right {
11    /// Full read access to the resource and its children.
12    /// [urls::READ]
13    Read,
14    /// Full edit, update, destroy access to the resource and its children.
15    /// [urls::WRITE]
16    Write,
17    /// Create new children (append to tree)
18    /// [urls::APPEND]
19    Append,
20}
21
22impl fmt::Display for Right {
23    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
24        let str = match self {
25            Right::Read => urls::READ,
26            Right::Write => urls::WRITE,
27            Right::Append => urls::APPEND,
28        };
29        fmt.write_str(str)
30    }
31}
32
33/// Looks for children relations, adds to the resource. Performs a Query, might be expensive.
34pub fn add_children(store: &impl Storelike, resource: &mut Resource) -> AtomicResult<Resource> {
35    let results = store.query(&Query::new_prop_val(urls::PARENT, resource.get_subject()))?;
36    let mut children = results.subjects;
37    children.sort();
38    resource.set(urls::CHILDREN.into(), children.into(), store)?;
39    Ok(resource.to_owned())
40}
41
42/// Throws if not allowed.
43/// Returns string with explanation if allowed.
44pub fn check_write(
45    store: &impl Storelike,
46    resource: &Resource,
47    for_agent: &ForAgent,
48) -> AtomicResult<String> {
49    check_rights(store, resource, for_agent, Right::Write)
50}
51
52/// Does the Agent have the right to read / view the properties of the selected resource, or any of its parents?
53/// Throws if not allowed.
54/// Returns string with explanation if allowed.
55pub fn check_read(
56    store: &impl Storelike,
57    resource: &Resource,
58    for_agent: &ForAgent,
59) -> AtomicResult<String> {
60    check_rights(store, resource, for_agent, Right::Read)
61}
62
63/// Does the Agent have the right to _append_ to its parent?
64/// This checks the `append` rights, and if that fails, checks the `write` right.
65/// Throws if not allowed.
66/// Returns string with explanation if allowed.
67#[tracing::instrument(skip(store), level = "debug")]
68pub fn check_append(
69    store: &impl Storelike,
70    resource: &Resource,
71    for_agent: &ForAgent,
72) -> AtomicResult<String> {
73    match resource.get_parent(store) {
74        Ok(parent) => {
75            if let Ok(msg) = check_rights(store, &parent, for_agent, Right::Append) {
76                Ok(msg)
77            } else {
78                check_rights(store, resource, for_agent, Right::Write)
79            }
80        }
81        Err(e) => {
82            if resource
83                .get_classes(store)?
84                .iter()
85                .map(|c| c.subject.clone())
86                .collect::<String>()
87                .contains(urls::DRIVE)
88            {
89                Ok(String::from("Drive without a parent can be created"))
90            } else {
91                Err(e)
92            }
93        }
94    }
95}
96
97/// Recursively checks a Resource and its Parents for rights.
98/// Throws if not allowed.
99/// Returns string with explanation if allowed.
100#[tracing::instrument(skip(store, resource))]
101pub fn check_rights(
102    store: &impl Storelike,
103    resource: &Resource,
104    for_agent_enum: &ForAgent,
105    right: Right,
106) -> AtomicResult<String> {
107    if for_agent_enum == &ForAgent::Sudo {
108        return Ok("Sudo has root access, and can edit anything.".into());
109    }
110    let for_agent = for_agent_enum.to_string();
111    if resource.get_subject() == &for_agent {
112        return Ok("Agents can always edit themselves or their children.".into());
113    }
114    if let Ok(server_agent) = store.get_default_agent() {
115        if server_agent.subject == for_agent {
116            return Ok("Server agent has root access, and can edit anything.".into());
117        }
118    }
119
120    // Handle Commits.
121    if let Ok(commit_subject) = resource.get(urls::SUBJECT) {
122        return match right {
123            Right::Read => {
124                // Commits can be read when their subject / target is readable.
125                let target = store.get_resource(&commit_subject.to_string())?;
126                check_rights(store, &target, for_agent_enum, right)
127            }
128            Right::Write => Err("Commits cannot be edited.".into()),
129            Right::Append => Err("Commits cannot have children, you cannot Append to them.".into()),
130        };
131    }
132
133    // Check if the resource's rights explicitly refers to the agent or the public agent
134    if let Ok(arr_val) = resource.get(&right.to_string()) {
135        for s in arr_val.to_subjects(None)? {
136            match s.as_str() {
137                urls::PUBLIC_AGENT => {
138                    return Ok(format!(
139                        "PublicAgent has been granted rights in {}",
140                        resource.get_subject()
141                    ))
142                }
143                agent => {
144                    if agent == for_agent {
145                        return Ok(format!(
146                            "Right has been explicitly set in {}",
147                            resource.get_subject()
148                        ));
149                    }
150                }
151            };
152        }
153    }
154
155    // Try the parents recursively
156    if let Ok(parent) = resource.get_parent(store) {
157        check_rights(store, &parent, for_agent_enum, right)
158    } else {
159        if for_agent_enum == &ForAgent::Public {
160            // resource has no parent and agent is not in rights array - check fails
161            let action = match right {
162                Right::Read => "readable",
163                Right::Write => "editable",
164                Right::Append => "appendable",
165            };
166            return Err(crate::errors::AtomicError::unauthorized(format!(
167                "This resource is not publicly {}. Try signing in",
168                action,
169            )));
170        }
171        // resource has no parent and agent is not in rights array - check fails
172        Err(crate::errors::AtomicError::unauthorized(format!(
173            "No {} right has been found for {} in this resource or its parents",
174            right, for_agent
175        )))
176    }
177}
178
179#[cfg(test)]
180mod test {
181    // use super::*;
182    use crate::{datatype::DataType, Storelike, Value};
183
184    // TODO: Add tests for:
185    // - basic check_write (should be false for newly created agent)
186    // - Malicious Commit (which grants itself write rights)
187
188    #[test]
189    fn authorization() {
190        let store = crate::Store::init().unwrap();
191        store.populate().unwrap();
192        // let agent = store.create_agent(Some("test_actor")).unwrap();
193        let subject = "https://localhost/new_thing";
194        let mut commitbuilder_1 = crate::commit::CommitBuilder::new(subject.into());
195        let property = crate::urls::DESCRIPTION;
196        let value = Value::new("Some value", &DataType::Markdown).unwrap();
197        commitbuilder_1.set(property.into(), value);
198        // let mut commitbuilder_2 = commitbuilder_1.clone();
199        // let commit_1 = commitbuilder_1.sign(&agent, &store).unwrap();
200        // Should fail if there is no self_url set in the store, and no parent in the commit
201        // TODO: FINISH THIS
202        // commit_1.apply_opts(&store, true, true, true, true).unwrap_err();
203        // commitbuilder_2.set(crate::urls::PARENT.into(), Value::AtomicUrl(crate::urls::AGENT.into()));
204        // let commit_2 = commitbuilder_2.sign(&agent, &store).unwrap();
205
206        // let resource = store.get_resource(&subject).unwrap();
207        // assert!(resource.get(property).unwrap().to_string() == value.to_string());
208    }
209
210    #[test]
211    fn display_right() {
212        let read = super::Right::Read;
213        assert_eq!(read.to_string(), super::urls::READ);
214        let write = super::Right::Write;
215        assert_eq!(write.to_string(), super::urls::WRITE);
216    }
217}