1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::collections::HashMap;
mod json;
mod tests;

/// Zero API client. Instantiate with a token, than call the `.fetch()` method to download secrets.
pub struct Zero {
    api_url: String,
    caller_name: Option<String>,
    pick: Vec<String>,
    token: String,
}

/// Constructor arguments. Defines required and optional params.
pub struct Arguments {
    pub token: String,
    pub pick: Option<Vec<String>>,
    pub caller_name: Option<String>,
}

/// The main client for accessing Zero GraphQL API.
///
/// ### Example:
/// ```rust
/// use zero_sdk::{Zero, Arguments};
///
/// let client = Zero::new(Arguments {
///     pick: Some(vec![String::from("my-secret")]),
///     token: String::from("my-zero-token"),
///     caller_name: None,
/// })
/// .unwrap();
/// ```
impl Zero {
    /// Set the URL which will be called in fetch(). The method was added mostly for convenience of testing.
    pub fn set_api_url(mut self, new_api_url: String) -> Self {
        self.api_url = new_api_url;
        return self;
    }

    // TODO Implement proper error structures with a message and a code
    // TODO Accepts an array of secrets to fetch
    /// Fetch the secrets assigned to the token.
    pub fn fetch(self) -> Result<HashMap<String, HashMap<String, String>>, String> {
        let response = if let Ok(value) = ureq::post(&self.api_url).send_json(serde_json::json!({
            "query":
                format!(
                    "query {{
                        secrets(zeroToken: \"{}\", pick: [{}]{}) {{
                            name
                            fields {{
                                name value
                            }}
                        }}
                    }}",
                    &self.token,
                    &self
                        .pick
                        .iter()
                        .map(|secret| format!("\"{}\"", &secret))
                        .collect::<Vec<String>>()
                        .join(", "),

                    // REVIEW There should be a better way for string interpolation
                    if self.caller_name.is_some() {
                        format!(", callerName: \"{}\"", &self.caller_name.unwrap_or_default())
                    } else {
                        "".to_string()
                    },
                )
        })) {
            value
        } else {
            return Err(String::from("Failed to fetch secrets due to network issue"));
        };

        let response_json = if let Ok(value) = response.into_json::<json::ResponseJson>() {
            value
        } else {
            return Err(String::from("Server returned invalid response"));
        };

        if response_json.errors.is_some() {
            return Err(String::from(&response_json.errors.unwrap()[0].message));
        }

        if response_json.data.is_none() {
            return Err(String::from(
                "Server returned invalid response (no secrets)",
            ));
        }

        // Tranform response to the following structure:
        // {nameOfTheSecret: {fieldOne: "fieldOneValue", fieldTwo: "fieldTwoValue"}}
        Ok(response_json
            .data
            .unwrap()
            .secrets
            .unwrap()
            .iter()
            .map(|secret| {
                (
                    secret.name.to_owned(),
                    HashMap::from_iter(
                        secret
                            .fields
                            .iter()
                            .map(|field| (field.name.to_owned(), field.value.to_owned())),
                    ),
                )
            })
            .collect())
    }

    /// Instantiate new Zero struct. Requires token string to be non empty, other params are optional.
    pub fn new(arguments: Arguments) -> Result<Self, &'static str> {
        if arguments.token == "" {
            return Err("Zero-token is empty");
        }

        Ok(Self {
            api_url: String::from("https://core.tryzero.com/v1/graphql"),
            caller_name: arguments.caller_name,
            pick: arguments.pick.unwrap_or(vec![]),
            token: arguments.token,
        })
    }
}